当前位置:   article > 正文

《算法导论》学习笔记之Chapter12二叉树基本特点,及二叉搜索树(查找树)_考虑一个二值搜索树 ,它的键是不同的。表明,如果 中的一个节点 的右子树

考虑一个二值搜索树 ,它的键是不同的。表明,如果 中的一个节点 的右子树

第12章 二叉搜索树

在学习二叉搜索树之前,我准备先预习一下二叉树的概念和相关算法。

二叉树拥有结合有序数组和链表的优点,查找数据和在数组中查找一样快,插入删除数据则有和链表一样高效的性能,所以是面试中必问的知识点。

二叉树的基本概念

结点的度-结点所拥有子树的个数称为该结点的度

叶结点-度为0的结点称为叶结点,或者终端节点;

分枝结点-度不为0的结点称为分枝结点,或者非终端结点。一棵树的结点除叶结点外,其余均为分枝结点;

左孩子、右孩子、双亲-树中一个结点的子树的根节点称为这个结点的孩子。这个结点称为他孩子节点的双亲。具有同一个双亲的孩子结点互称为兄弟;

路径、路径长度-如果一棵树的一串结点n1,n2,...,nk有如下关系:结点ni是ni+n1的父结点(1<=i<=k),就把n1,n2,...,nk称为一条由n1至nk的路径。这条路径的长度是k-1;

祖先、子孙-在树中,如果有一条路径从结点M到结点N,那么结点M就成为N的祖先,而N称为M的子孙;

结点的层数-规定树的根结点的层数为1,其余结点的层数等于他的双亲结点的层数加1;

树的深度-树中所有结点的最大层数就是树的深度

树的度-树中各结点度的最大值称为该树的度,叶子结点的度为0。

满二叉树-在一棵二叉树中,如果所有分枝结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树

完全二叉树-一颗深度为K的有n个结点的二叉树,对树中的结点按从上至下,从左至右的顺序进行编号,如果编号为i(1<=i<=n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,那么这棵二叉树被称为完全二叉树。完全二叉树的特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。注意:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树


二叉树的基本性质:

一颗非空二叉树的第i层上最多有2.^(i-1)个结点(i>=1);

一颗深度为k的二叉树中,最多具有2.^k-1个结点,最少有k个结点;

对于一颗非空二叉树,度为0的结点(即叶子结点),总是比度为2的结点多1个,如果叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;

具有n个结点的完全二叉树的深度为[log2n]+1;

对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的顺序对二叉树所有结点从1开始顺序编号,则对于任意的序号为i的结点,有1:如果i>1,那么序号为i的结点的双亲结点的序号为i/2;如果i=1,那么序号为i的结点为根节点。2:如果2i<=n,那么序号为i的结点的左孩子结点为2i,如果2i>n,那么序号为i的结点无左孩子。3:如果2i+1<=n,那么序号为i的结点的右孩子结点为2i+1,如果2i+1>n,那么序号为i的结点无右孩子。

注:如果对二叉树的根结点从0开始编号,那么相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的结点为2i+2


二叉排序树:又称二叉查找树,二叉查找树。在不为空的前提下,具有如下性质:1如果左子树不为空,那么左子树上所有结点的值均小于它的根结点的值;2如果右子树不为空,那么右子树上所有结点的值均大于根结点的值;3左右子树也分别为二叉排序树。

下面用代码实现如何构建二叉排序树,以及对构建的二叉树进行多种方式的遍历(中序遍历,先序遍历,后序遍历和层序遍历):

  1. import java.util.LinkedList;
  2. import java.util.Queue;;
  3. public class BinaryTree {
  4. private Node root;
  5. public BinaryTree() {
  6. root = null;
  7. }
  8. // 将数据data插入到排序二叉树中
  9. public void insert(int data) {
  10. Node newNode = new Node(data);
  11. if (root == null) {
  12. root = newNode;
  13. } else {
  14. Node cur = root;
  15. Node parent;
  16. while (true) {
  17. parent = cur;
  18. if (data < cur.data) {
  19. cur = cur.left;
  20. if (cur == null) {
  21. parent.left = newNode;
  22. return;
  23. }
  24. } else {
  25. cur = cur.right;
  26. if (cur == null) {
  27. parent.right = newNode;
  28. return;
  29. }
  30. }
  31. }
  32. }
  33. }
  34. // 将数值输入构建二叉树
  35. public void buildBinaryTree(int[] data) {
  36. for (int i = 0; i < data.length; i++) {
  37. insert(data[i]);
  38. }
  39. }
  40. /**
  41. * 构建二叉树之后,下一个知识点就是对二叉树进行遍历,遍历输出二叉树中的数据 分为中序遍历:顺序为左结点-中间结点-右结点
  42. * 先序遍历顺序:中间结点-左结点-右结点 后序遍历顺序为:左结点-右结点-中间结点 其实可以看出,遍历顺序是按照中间结点被遍历的顺序来划分的
  43. * 最后还有一种:层序遍历,一层一层的遍历二叉树中的结点数据
  44. *
  45. * @param args
  46. */
  47. // 中序遍历,中序遍历的二叉搜索树输出是单调的,即已经排好序的序列
  48. public void inOrder(Node localRoot) {
  49. if (localRoot != null) {
  50. inOrder(localRoot.left);
  51. System.out.println(localRoot.data + " ");
  52. inOrder(localRoot.right);
  53. }
  54. }
  55. public void inOrder() {
  56. this.inOrder(this.root);
  57. }
  58. // 先序遍历
  59. public void preOrder(Node localRoot) {
  60. if (localRoot != null) {
  61. System.out.println(localRoot.data + " ");
  62. preOrder(localRoot.left);
  63. preOrder(localRoot.right);
  64. }
  65. }
  66. public void preOrder() {
  67. this.preOrder(this.root);
  68. }
  69. // 后序遍历
  70. public void postOrder(Node localRoot) {
  71. if (localRoot != null) {
  72. postOrder(localRoot.left);
  73. postOrder(localRoot.right);
  74. System.out.println(localRoot.data + " ");
  75. }
  76. }
  77. public void postOrder() {
  78. this.postOrder(this.root);
  79. }
  80. /**
  81. * 二叉树的层序遍历:思想-借助队列的先入先出属性,将根结点放入队列 检查是否有子结点,将子结点依次也放入队列,逐步从队列中取出元素进行打印
  82. *
  83. * @param args
  84. */
  85. public void layerOrder() {
  86. if (this.root == null) {
  87. return;
  88. }
  89. Queue<Node> q = new LinkedList<Node>();
  90. q.add(this.root);
  91. while (!q.isEmpty()) {
  92. Node n = q.poll();
  93. System.out.println(n.data + " ");
  94. if (n.left != null) {
  95. q.add(n.left);
  96. }
  97. if (n.right != null) {
  98. q.add(n.right);
  99. }
  100. }
  101. }
  102. public static void main(String[] args) {
  103. // TODO Auto-generated method stub
  104. BinaryTree bTree = new BinaryTree();
  105. int[] data = { 2, 8, 7, 4, 6, 2, 9, 1, 5 };
  106. bTree.buildBinaryTree(data);
  107. System.out.println("中序遍历:");
  108. bTree.inOrder();
  109. System.out.println("先序遍历:");
  110. bTree.preOrder();
  111. System.out.println("后序遍历:");
  112. bTree.postOrder();
  113. }
  114. }
  115. class Node {
  116. public int data;
  117. public Node left;
  118. public Node right;
  119. public Node(int data) {
  120. this.data = data;
  121. this.left = null;
  122. this.right = null;
  123. }
  124. }

上面是遍历的递归实现,这里给出一个参考链接,给出的有非递归遍历的实现,使用栈来保存结点。http://blog.csdn.net/lr131425/article/details/60755706

下面再介绍一个二叉树的应用:如何求二叉树中结点的最大距离,思想:首先求左子树距离根结点的最大距离leftMaxDis,之后求右子树距离根结点的最大距离rightMaxDis,二叉树中结点间的最大距离为:leftMaxDis+rightMaxDis。代码实现如下:

  1. private int max(int a, int b) {
  2. return a > b ? a : b;
  3. }
  4. public int findMaxDis(Node root) {
  5. if (root == null) {
  6. return 0;
  7. }
  8. if (root.left == null) {
  9. root.leftMaxDis = 0;
  10. }
  11. if (root.right == null) {
  12. root.rightMaxDis = 0;
  13. }
  14. if (root.left != null) {
  15. findMaxDis(root.left);
  16. }
  17. if (root.right != null) {
  18. findMaxDis(root.right);
  19. }
  20. // 计算左子树距离根结点的距离
  21. if (root.left != null) {
  22. root.leftMaxDis = max(root.left.leftMaxDis, root.left.rightMaxDis) + 1;
  23. }
  24. // 计算左子树距离根结点的距离
  25. if (root.right != null) {
  26. root.rightMaxDis = max(root.right.leftMaxDis, root.right.rightMaxDis) + 1;
  27. }
  28. // 获取二叉树所有结点最大距离
  29. if (root.leftMaxDis + root.rightMaxDis > MaxLen) {
  30. MaxLen = root.leftMaxDis + root.rightMaxDis;
  31. }
  32. return MaxLen;
  33. }

该代码是是在上面的代码基础上添加的。运行之后结果是正确的:

中序遍历:









先序遍历:









后序遍历:








//最大结点距离
6


BST之所以存在是因为人们想让他干更多的事:主要目的是同时保证动态插入,删除,查询复杂度为O(log n),同时保证操作之后得到的树还是有序的。而排序就不能做到这一点。

对应的代价就是BST编码复杂度更高,同时运行时还有更高的常数复杂度。所以当用不上BST提供的那些厉害的功能的时候,就不要用BST,不然就是拜拜浪费你和你的CPU的时间。
下面给出在二叉搜索树上进行查找操作的代码:
  1. // 针对二叉搜索树的查找操作,通过递归实现查找,时间复杂度为O(logn)=O(h),h为树的高度
  2. public Node search(Node root, int k) {
  3. if (root.data == k || root == null) {
  4. return root;
  5. }
  6. if (root.data > k) {
  7. return search(root.left, k);
  8. } else {
  9. return search(root.right, k);
  10. }
  11. }
  12. // 下面是使用迭代版本的搜索操作
  13. public Node searchOfIterative(Node root, int k) {
  14. while (root != null && k != root.data) {
  15. if (k < root.data) {
  16. root = root.left;
  17. } else {
  18. root = root.right;
  19. }
  20. }
  21. return root;
  22. }

下面是查找二叉搜索树的最大和最小元素结点:
  1. // 搜索最大关键字元素和最小关键字元素
  2. public int MinOfTree(Node root) {
  3. while (root.left != null) {
  4. root = root.left;
  5. }
  6. return root.data;
  7. }
  8. public int MaxOfTree(Node root) {
  9. while (root.right != null) {
  10. root = root.right;
  11. }
  12. return root.data;
  13. }

下面介绍查找任一节点的前驱结点和后继结点的方法:
前驱节点:是该节点的左子树中的最大节点。
后继节点:是该节点的右子树中的最小节点。

  1. public int successor(Node x) {
  2. // 如果结点x有右子树,则右子树的最小值就是节点x的后继
  3. if (x.right != null) {
  4. return MinOfTree(x.right);
  5. }
  6. // 如果x没有右子树。则x有以下两种可能:
  7. // (01) x是"一个左子树",则"x的后继结点"为 "它的父结点"。
  8. // (02) x是"一个右子树",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
  9. Node y = x.parent;
  10. while (y != null && x == y.right) {
  11. x = y;
  12. y = y.parent;
  13. }
  14. return y.data;
  15. }

二叉搜索树的删除操作:
  1. // 删除树中的结点z
  2. public void delete(BinaryTree T, Node z) {
  3. if (z.left == null) {
  4. transplant(T, z, z.right);
  5. } else if (z.right == null) {
  6. transplant(T, z, z.left);
  7. } else {
  8. Node y = MinOfTree(z.right);
  9. if (y.parent != z) {
  10. transplant(T, y, y.right);
  11. y.right = z.right;
  12. y.right.parent = y;
  13. }
  14. transplant(T, z, y);
  15. y.left = z.left;
  16. y.left.parent = y;
  17. }
  18. }
  19. //重新布局二叉树
  20. public void transplant(BinaryTree T, Node u, Node v) {
  21. if (u.parent == null) {
  22. T.root = v;
  23. } else if (u == u.parent.left) {
  24. u.parent.left = v;
  25. } else {
  26. u.parent.right = v;
  27. }
  28. if (v != null) {
  29. v.parent = u.parent;
  30. }
  31. }

删除函数处理4种情况:1-2行处理结点z没有左孩子的情况,3-4行处理z有一个左孩子没有右孩子的情况.5-12行处理剩下的两种情况:z有两个孩子的情形。第5行查找结点y,是z的后继结点。因为z的右子树非空,所以后继一定是这个子树中具有最小关键字的结点,因此就调用MinOfTree(z.right)。此时,y没有左孩子(因为他是z的后继结点),将y移出原来位置进行拼接,并替换树中的z。如果y是z的右孩子,那么第10-12行用y替换z并成为z的双亲的一个孩子,之后将z的左孩子转变为y的左孩子。如果y不是z的左孩子,第7-9行用y的右孩子替换y成为y的双亲的一个孩子,之后将z的右孩子变为y的右孩子。
以上除了第5行,调用MinOfTree(z.right)之外,删除操作都只花费常数时间,所以,一颗高度为h的二叉树,删除操作时间为O(h)=O(logn)。插入操作一样。





声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/634948
推荐阅读
相关标签
  

闽ICP备14008679号