二叉搜索树详解(Java实现)

阳光穿透心脏的1/2处 2021-06-10 20:39 573阅读 0赞

二叉搜索树定义#


二叉搜索树,是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左,右子树也分别为二叉搜索树;
  4. 没有键值相等的节点。

用Java来表示二叉树#

  1. public class BinarySearchTree
  2. { // 二叉搜索树类
  3. private class Node
  4. { // 节点类
  5. int data; // 数据域
  6. Node right; // 右子树
  7. Node left; // 左子树
  8. }
  9. private Node root; // 树根节点
  10. }

首先,需要一个节点对象的类。这个对象包含数据域和指向节点的两个子节点的引用。

其次,需要一个树对象的类。这个对象包含一个根节点root。

创建树(insert)#

  1. public void insert(int key)
  2. {
  3. Node p=new Node(); //待插入的节点
  4. p.data=key;
  5. if(root==null)
  6. {
  7. root=p;
  8. }
  9. else
  10. {
  11. Node parent=new Node();
  12. Node current=root;
  13. while(true)
  14. {
  15. parent=current;
  16. if(key>current.data)
  17. {
  18. current=current.right; // 右子树
  19. if(current==null)
  20. {
  21. parent.right=p;
  22. return;
  23. }
  24. }
  25. else //本程序没有做key出现相等情况的处理,暂且假设用户插入的节点值都不同
  26. {
  27. current=current.left; // 左子树
  28. if(current==null)
  29. {
  30. parent.left=p;
  31. return;
  32. }
  33. }
  34. }
  35. }
  36. }

创建树的时候,主要用到了parent,current来记录要插入节点的位置。哪么怎么检验自己是否正确地创建了一颗二叉搜索树呢,我们通过遍历来输出各个节点的值

遍历树(travel)#

遍历指的是按照某种特定的次序来访问二叉搜索树中的每个节点,主要有三种遍历的方法:

  1. 前序遍历,“中左右”
  2. 中序遍历,“左中右”
  3. 后续遍历,“左右中”

上面的口诀“中左右”表示的含义是,先访问根节点,再访问左子,最后访问右子。举个例子:

format_png

  • 前序遍历:39 24 23 30 64 53 60
  • 中序遍历:23 24 30 39 53 60 64
  • 后序遍历:23 30 24 60 53 64 39

你会发现,按照中序遍历的规则将一个二叉搜索树输入,结果为按照正序排列。

  1. public void preOrder(Node root)
  2. { // 前序遍历,"中左右"
  3. if (root != null)
  4. {
  5. System.out.print(root.data + " ");
  6. preOrder(root.left);
  7. preOrder(root.right);
  8. }
  9. }
  10. public void inOrder(Node root)
  11. { // 中序遍历,"左中右"
  12. if (root != null)
  13. {
  14. inOrder(root.left);
  15. System.out.print(root.data + " ");
  16. inOrder(root.right);
  17. }
  18. }
  19. public void postOrder(Node root)
  20. { // 后序遍历,"左右中"
  21. if (root != null)
  22. {
  23. postOrder(root.left);
  24. postOrder(root.right);
  25. System.out.print(root.data + " ");
  26. }
  27. }
  28. public void traverse(int traverseType)
  29. { // 选择以何种方式遍历
  30. switch (traverseType)
  31. {
  32. case 1:
  33. System.out.print("preOrder traversal ");
  34. preOrder(root);
  35. System.out.println();
  36. break;
  37. case 2:
  38. System.out.print("inOrder traversal ");
  39. inOrder(root);
  40. System.out.println();
  41. break;
  42. case 3:
  43. System.out.print("postOrder traversal ");
  44. postOrder(root);
  45. System.out.println();
  46. break;
  47. }
  48. }

以上的代码采用递归的方式实现三种遍历,为了方便我们使用,又写了一个traverse函数来实现选择哪种方式进行树的遍历。

这会儿就可以写单元测试了,我们首先创建一个二叉搜索树,然后分别使用“前序”,“中序”,“后序”来遍历输出树的所有节点。

  1. public static void main(String[] args) //unit test
  2. {
  3. BinarySearchTree tree=new BinarySearchTree();
  4. tree.insert(39);
  5. tree.insert(24);
  6. tree.insert(64);
  7. tree.insert(23);
  8. tree.insert(30);
  9. tree.insert(53);
  10. tree.insert(60);
  11. tree.traverse(1);
  12. tree.traverse(2);
  13. tree.traverse(3);
  14. }

运行该单元测试,可以看到如下的结果:

format_png 2

查找节点(find)#

  1. public Node find(int key)
  2. { // 从树中按照关键值查找元素
  3. Node current = root;
  4. while (current.data != key)
  5. {
  6. if (key > current.data)
  7. current = current.right;
  8. else
  9. current = current.left;
  10. if (current == null) return null;
  11. }
  12. return current;
  13. }
  14. public void show(Node node)
  15. { //输出节点的数据域
  16. if(node!=null)
  17. System.out.println(node.data);
  18. else
  19. System.out.println("null");
  20. }

查找节点比较简单,如果找到节点则返回该节点,否则返回null。为了方便在控制台输出,我们有添加了一个show函数,用来输出节点的数据域。

删除节点(delete)#

删除节点是二叉搜索树中,最复杂的一种操作,但是也不是特别难,我们分类讨论:

  • 要删除节点有零个孩子,即叶子节点

format_png 3

如图所示,只需要将parent.left(或者是parent.right)设置为null,然后Java垃圾自动回收机制会自动删除current节点。

  • 要删除节点有一个孩子

format_png 4

如图所示,只需要将parent.left(或者是parent.right)设置为curren.right(或者是current.left)即可。

aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTQ1OTE3OS8yMDE5MDUvMTQ1OTE3OS0yMDE5MDUwNjEyMDM0NzM5MC0xOTAwNTA0MTguZ2lm

  • 要删除节点有两个孩子

这种情况比较复杂,首先我们引入后继节点的概念,如果将一棵二叉树按照中序周游的方式输出,则任一节点的下一个节点就是该节点的后继节点。例如:上图中24的后继节点为25,64的后继节点为70.找到后继节点以后,问题就变得简单了,分为两种情况:

1.后继节点为待删除节点的右子,只需要将curren用successor替换即可,注意处理好current.left和successor.right.

注意:这种情况下,successor一定没有左孩子,一但它有左孩子,哪它必然不是current的后继节点。

format_png 5

aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTQ1OTE3OS8yMDE5MDUvMTQ1OTE3OS0yMDE5MDUwNjEzMTAxMzU3NC04MTg1NDUyMDIuZ2lm

2.后继节点为待删除结点的右孩子的左子树,这种情况稍微复杂点,请看动态图片演示。

format_png 6

aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTQ1OTE3OS8yMDE5MDUvMTQ1OTE3OS0yMDE5MDUwNjEzMTYzNTM2OC0xNzgxNTAyMjcxLmdpZg

算法的步骤是:

  1. successorParent.left=successor.right
  2. successor.left=current.left
  3. parent.left=seccessor

弄懂原理后,我们来看具体的代码实现:

  1. private Node getSuccessor(Node delNode) //寻找要删除节点的中序后继结点
  2. {
  3. Node successorParent=delNode;
  4. Node successor=delNode;
  5. Node current=delNode.right;
  6. //用来寻找后继结点
  7. while(current!=null)
  8. {
  9. successorParent=successor;
  10. successor=current;
  11. current=current.left;
  12. }
  13. //如果后继结点为要删除结点的右子树的左子,需要预先调整一下要删除结点的右子树
  14. if(successor!=delNode.right)
  15. {
  16. successorParent.left=successor.right;
  17. successor.right=delNode.right;
  18. }
  19. return successor;
  20. }
  21. public boolean delete(int key) // 删除结点
  22. {
  23. Node current = root;
  24. Node parent = new Node();
  25. boolean isRightChild = true;
  26. while (current.data != key)
  27. {
  28. parent = current;
  29. if (key > current.data)
  30. {
  31. current = current.right;
  32. isRightChild = true;
  33. }
  34. else
  35. {
  36. current = current.left;
  37. isRightChild = false;
  38. }
  39. if (current == null) return false; // 没有找到要删除的结点
  40. }
  41. // 此时current就是要删除的结点,parent为其父结点
  42. // 要删除结点为叶子结点
  43. if (current.right == null && current.left == null)
  44. {
  45. if (current == root)
  46. {
  47. root = null; // 整棵树清空
  48. }
  49. else
  50. {
  51. if (isRightChild)
  52. parent.right = null;
  53. else
  54. parent.left = null;
  55. }
  56. return true;
  57. }
  58. //要删除结点有一个子结点
  59. else if(current.left==null)
  60. {
  61. if(current==root)
  62. root=current.right;
  63. else if(isRightChild)
  64. parent.right=current.right;
  65. else
  66. parent.left=current.right;
  67. return true;
  68. }
  69. else if(current.right==null)
  70. {
  71. if(current==root)
  72. root=current.left;
  73. else if(isRightChild)
  74. parent.right=current.left;
  75. else
  76. parent.left=current.left;
  77. return true;
  78. }
  79. //要删除结点有两个子结点
  80. else
  81. {
  82. Node successor=getSuccessor(current); //找到要删除结点的后继结点
  83. if(current==root)
  84. root=successor;
  85. else if(isRightChild)
  86. parent.right=successor;
  87. else
  88. parent.left=successor;
  89. successor.left=current.left;
  90. return true;
  91. }
  92. }

大家注意哪个私有函数getSuccessor的功能,它不仅仅是用来找后继结点的。

总结#

二叉搜索树其实不是特别难,理解以后,多练习几次,应该可以掌握。以下是全部的代码:

  1. package org.yahuian;
  2. public class BinarySearchTree
  3. { // 二叉搜索树类
  4. private class Node
  5. { // 节点类
  6. int data; // 数据域
  7. Node right; // 右子树
  8. Node left; // 左子树
  9. }
  10. private Node root; // 树根节点
  11. public void insert(int key)
  12. {
  13. Node p = new Node(); // 待插入的节点
  14. p.data = key;
  15. if (root == null)
  16. {
  17. root = p;
  18. }
  19. else
  20. {
  21. Node parent = new Node();
  22. Node current = root;
  23. while (true)
  24. {
  25. parent = current;
  26. if (key > current.data)
  27. {
  28. current = current.right; // 右子树
  29. if (current == null)
  30. {
  31. parent.right = p;
  32. return;
  33. }
  34. }
  35. else // 本程序没有做key出现相等情况的处理,暂且假设用户插入的节点值都不同
  36. {
  37. current = current.left; // 左子树
  38. if (current == null)
  39. {
  40. parent.left = p;
  41. return;
  42. }
  43. }
  44. }
  45. }
  46. }
  47. public void preOrder(Node root)
  48. { // 前序遍历,"中左右"
  49. if (root != null)
  50. {
  51. System.out.print(root.data + " ");
  52. preOrder(root.left);
  53. preOrder(root.right);
  54. }
  55. }
  56. public void inOrder(Node root)
  57. { // 中序遍历,"左中右"
  58. if (root != null)
  59. {
  60. inOrder(root.left);
  61. System.out.print(root.data + " ");
  62. inOrder(root.right);
  63. }
  64. }
  65. public void postOrder(Node root)
  66. { // 后序遍历,"左右中"
  67. if (root != null)
  68. {
  69. postOrder(root.left);
  70. postOrder(root.right);
  71. System.out.print(root.data + " ");
  72. }
  73. }
  74. public void traverse(int traverseType)
  75. { // 选择以何种方式遍历
  76. switch (traverseType)
  77. {
  78. case 1:
  79. System.out.print("preOrder traversal ");
  80. preOrder(root);
  81. System.out.println();
  82. break;
  83. case 2:
  84. System.out.print("inOrder traversal ");
  85. inOrder(root);
  86. System.out.println();
  87. break;
  88. case 3:
  89. System.out.print("postOrder traversal ");
  90. postOrder(root);
  91. System.out.println();
  92. break;
  93. }
  94. }
  95. public Node find(int key)
  96. { // 从树中按照关键值查找元素
  97. Node current = root;
  98. while (current.data != key)
  99. {
  100. if (key > current.data)
  101. current = current.right;
  102. else
  103. current = current.left;
  104. if (current == null) return null;
  105. }
  106. return current;
  107. }
  108. public void show(Node node)
  109. { //输出节点的数据域
  110. if(node!=null)
  111. System.out.println(node.data);
  112. else
  113. System.out.println("null");
  114. }
  115. private Node getSuccessor(Node delNode) //寻找要删除节点的中序后继结点
  116. {
  117. Node successorParent=delNode;
  118. Node successor=delNode;
  119. Node current=delNode.right;
  120. //用来寻找后继结点
  121. while(current!=null)
  122. {
  123. successorParent=successor;
  124. successor=current;
  125. current=current.left;
  126. }
  127. //如果后继结点为要删除结点的右子树的左子,需要预先调整一下要删除结点的右子树
  128. if(successor!=delNode.right)
  129. {
  130. successorParent.left=successor.right;
  131. successor.right=delNode.right;
  132. }
  133. return successor;
  134. }
  135. public boolean delete(int key) // 删除结点
  136. {
  137. Node current = root;
  138. Node parent = new Node();
  139. boolean isRightChild = true;
  140. while (current.data != key)
  141. {
  142. parent = current;
  143. if (key > current.data)
  144. {
  145. current = current.right;
  146. isRightChild = true;
  147. }
  148. else
  149. {
  150. current = current.left;
  151. isRightChild = false;
  152. }
  153. if (current == null) return false; // 没有找到要删除的结点
  154. }
  155. // 此时current就是要删除的结点,parent为其父结点
  156. // 要删除结点为叶子结点
  157. if (current.right == null && current.left == null)
  158. {
  159. if (current == root)
  160. {
  161. root = null; // 整棵树清空
  162. }
  163. else
  164. {
  165. if (isRightChild)
  166. parent.right = null;
  167. else
  168. parent.left = null;
  169. }
  170. return true;
  171. }
  172. //要删除结点有一个子结点
  173. else if(current.left==null)
  174. {
  175. if(current==root)
  176. root=current.right;
  177. else if(isRightChild)
  178. parent.right=current.right;
  179. else
  180. parent.left=current.right;
  181. return true;
  182. }
  183. else if(current.right==null)
  184. {
  185. if(current==root)
  186. root=current.left;
  187. else if(isRightChild)
  188. parent.right=current.left;
  189. else
  190. parent.left=current.left;
  191. return true;
  192. }
  193. //要删除结点有两个子结点
  194. else
  195. {
  196. Node successor=getSuccessor(current); //找到要删除结点的后继结点
  197. if(current==root)
  198. root=successor;
  199. else if(isRightChild)
  200. parent.right=successor;
  201. else
  202. parent.left=successor;
  203. successor.left=current.left;
  204. return true;
  205. }
  206. }
  207. public static void main(String[] args) // unit test
  208. {
  209. BinarySearchTree tree = new BinarySearchTree();
  210. tree.insert(39);
  211. tree.insert(24);
  212. tree.insert(64);
  213. tree.insert(23);
  214. tree.insert(30);
  215. tree.insert(53);
  216. tree.insert(60);
  217. tree.traverse(1);
  218. tree.traverse(2);
  219. tree.traverse(3);
  220. tree.show(tree.find(23));
  221. tree.show(tree.find(60));
  222. tree.show(tree.find(64));
  223. tree.delete(23);
  224. tree.delete(60);
  225. tree.delete(64);
  226. tree.show(tree.find(23));
  227. tree.show(tree.find(60));
  228. tree.show(tree.find(64));
  229. }
  230. }

动态图片来自于:https://visualgo.net/en/bst

发表评论

表情:
评论列表 (有 0 条评论,573人围观)

还没有评论,来说两句吧...

相关阅读

    相关 搜索实现

    本文给出二叉搜索树介绍和实现 首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。 那这个结构有什么有用呢? 首先可以快速二分查找。还可以中

    相关 java实现搜索

    二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。定义如下 > (1)若左子树不空,则左子树上所有结点的