当前位置:   article > 正文

Java实现二叉树(一):二叉查找树的实现_二叉搜索树查找java

二叉搜索树查找java

Java实现二叉树(一):二叉查找树的实现

    数据结构+算法=程序,这是共识,是真理,还是学生时代卷子中的考点。但大多数程序员往往缺乏数据结构和算法的知识,或是根本没有学过,或是学过,但在工作时频繁地与业务逻辑打交道,也就逐渐忘记了有这么一回事。

    话不多说,直接开始吧,本文将介绍二叉树的基本概念,以及平衡二叉树增删改查节点的实现,由于网上关于数据结构的资料,C++相对居多,而本身作为一个Javaer,将会重新用Java实现一个平衡二叉树,仅供参考,欢迎纠正。

    一、二叉树的基本概念

    定义:二叉树,是一种特殊的树,二叉树的任意一个节点的度都不大于2,不包含度的节点称之为叶子。

    分类:二叉树又有细分,满二叉树,以及完全二叉树,等等等等,本文就不一一介绍了,本文暂且只介绍二叉查找树,也是最常见的一种数据类型。

    遍历方式:二叉树的遍历方式有三种,中序遍历,先序遍历,后序遍历:

  1. 中序遍历:先遍历左子树,再遍历父亲节点,最后遍历右子树;
  2. 先序遍历:先遍历父亲节点,再遍历左子树,最后遍历右子树;
  3. 后序遍历:先遍历父亲节点,再遍历右子树,最后遍历左子树;

    光说显得乏味,我们列一道经典的二叉树题目吧,已知一棵二叉树,如果先序遍历的节点顺序是:ADCEFGHB ,中序遍历是:CDFEGHAB ,则后序遍历结果为:

    这道题需要实现一个二叉树重构,个人的解题思路是这样的--首先,先序遍历最早遍历父亲节点,也就意味着A是根节点,而中序遍历最晚遍历右子树,所以我们可以推测出A的右边也就是B,是A的右子树。于是我们将两个去除AB,也就是DCEFGH,CDFEGH,推测父节点左子树的结果,同样的方法我们可以确定D是左子树的父亲节点,C是左子树的唯一一个左子树,从而继续去除。

    说着好像十分复杂,但是其实就是一个递归的思想,不断的确认父节点,以及拆分左子树,和右子树,完成树的重构,最后得到的树是下图:


    如此也就不难得出该树的后序遍历结果为:CFHGEDBA。

    二、二叉查找树的基本概念

    基本概念:二叉查找树是一种特殊的二叉树,要求左子树的全部节点小于父亲节点,右子树的全部节点大于父亲节点,同时,左子树和右子树也为二叉查找树,中序遍历一个二叉查找树,会得到一个有序的元素集合。

    优点:二叉查找树拥有良好的查找性能,查找次数最多为树的深度,这也就意味着二叉查找树的查找效率取决于树的深度,而满二叉树的深度为log(N+1),所以此时他的查找时间复杂度为O(logN)。

    缺点:缺点在优点中说的十分明确了,二叉查找树在极端的情况下会大大降低他的查找性能,比如说,顺序插入节点时,二叉树就会像一个链表,查找的时间复杂度将为O(N)。

    构建一个深度合理的二叉查找树本文暂不做介绍,之后的文章会讲述平衡二叉树的概念,可以关注。

    三、二叉查找树的基本操作和Java实现

    咳咳,注意,笔者要点题了。

    且不说二叉树,任何的数据结构都离不开增删改查四个步骤,就由增删改查分块讲解二叉查找树吧。

    Insert

    要求:二叉查找树插入时,要满足二叉查找树的特性,左小右大。

    实现思路:递归地去遍历一颗树,如果大于节点就遍历节点的右子树,如果小于节点就遍历节点的左子树,当节点为空时插入。

    代码:

  1. //以查找二叉树的规则插入,小的在左,大的在右
  2. public static void insert(Tree tree,int value)
  3. {    
  4. if(tree.getValue() == null)
  5. {
  6. tree.setValue(value);
  7. }
  8. else if(tree.getValue() < value)
  9. {
  10. if(tree.getrChild()!=null)
  11. {
  12. insert(tree.getrChild(),value);
  13. }
  14. else {
  15. tree.setrChild(new Tree());
  16. insert(tree.getrChild(),value);
  17. }
  18. }
  19. else if(tree.getValue() > value)
  20. {
  21. if(tree.getlChild()!=null)
  22. {
  23. insert(tree.getlChild(),value);
  24. }
  25. else {
  26. tree.setlChild(new Tree());
  27. insert(tree.getlChild(),value);
  28. }
  29. }
  30. }

    Delete

    要求:删除的要求相对复杂,因为,删除之后,还是必须要保持二叉查找树的特性。

    实现思路:先找到要删除的节点,这时会有三种情况:

  1. 删除节点为(左或者右)叶子--此时直接将节点的父亲节点的(左或者右)节点置为空;
  2. 删除节点只包含一个(左或者右)子树--此时,判断删除节点是父节点的左子树,还是右子树,用删除节点的唯一那一个“儿子”去替换自己;
  3. 删除节点既有左子树又有右子树--此时,可以找到右子树中最小的节点,替换删除节点,再递归地删除了它(做法不唯一,同样也可以用左子树中最大的节点去替换删除节点);

    代码(有些长...):

  1. public static void delete(Tree tree,int value)
  2. {
  3. //查询到的节点的父节点
  4. Tree parent = null;
  5. //查询到的节点
  6. Tree searchTree = tree;
  7. //当节点不为空的时候开始循环
  8. while(searchTree != null)
  9. {
  10. //如果节点的值等于查询值,跳出循环,之后的逻辑就是一个树查找的逻辑
  11. if(searchTree.getValue()==value)
  12. {
  13. break;
  14. }
  15. else if(searchTree.getValue() > value)
  16. {
  17. parent = searchTree;
  18. searchTree = searchTree.getlChild();
  19. }
  20. else if(searchTree.getValue() < value)
  21. {
  22. parent = searchTree;
  23. searchTree = searchTree.getrChild();
  24. }
  25. }
  26. //查询到的节点是父节点的左节点与否
  27. boolean parentLeftFlag = false;
  28. //查询到的节点的父节点是否存在
  29. boolean hasParent = true;
  30. //查询到的节点的左子树存在与否
  31. boolean leftFlag = false;
  32. //查询到的节点的右子树存在与否
  33. boolean rightFlag = false;
  34. if(parent == null)
  35. {
  36. hasParent = false;
  37. }
  38. else if(parent.getrChild() == searchTree)
  39. {
  40. parentLeftFlag = false;
  41. }
  42. else{
  43. parentLeftFlag = true;
  44. }
  45. if(searchTree == null)
  46. {
  47. return;
  48. }
  49. if(searchTree.getlChild() != null){
  50. leftFlag = true;
  51. }
  52. if(searchTree.getrChild() != null){
  53. rightFlag = true;
  54. }
  55. if(!leftFlag && !rightFlag)
  56. {
  57. if(hasParent)
  58. {
  59. if(parentLeftFlag)
  60. {
  61. parent.setlChild(null);
  62. }
  63. else{
  64. parent.setrChild(null);
  65. }
  66. }
  67. else{
  68. searchTree.setValue(null);
  69. }
  70. }
  71. else if(!leftFlag || !rightFlag)
  72. {
  73. if(hasParent)
  74. {
  75. if(parentLeftFlag)
  76. {
  77. if(leftFlag)
  78. {
  79. parent.setlChild(searchTree.getlChild());
  80. }
  81. else {
  82. parent.setlChild(searchTree.getrChild());
  83. }
  84. }
  85. else {
  86. if(leftFlag)
  87. {
  88. parent.setrChild(searchTree.getlChild());
  89. }
  90. else {
  91. parent.setrChild(searchTree.getrChild());
  92. }
  93. }
  94. }
  95. else
  96. {
  97. if(leftFlag){
  98. searchTree.setValue(searchTree.getlChild().getValue());
  99. searchTree.setrChild(searchTree.getlChild().getrChild());
  100. searchTree.setlChild(searchTree.getlChild().getlChild());
  101. }
  102. else
  103. {
  104. searchTree.setValue(searchTree.getrChild().getValue());
  105. searchTree.setlChild(searchTree.getrChild().getlChild());
  106. searchTree.setrChild(searchTree.getrChild().getrChild());
  107. }
  108. }
  109. }
  110. else if(leftFlag && rightFlag){
  111. Tree minTree = searchTree.getrChild();
  112. while (minTree.getlChild() != null)
  113. {
  114. minTree = minTree.getlChild();
  115. }
  116. Integer minTreeValue = minTree.getValue();
  117. delete(searchTree,minTreeValue);
  118. searchTree.setValue(minTreeValue);
  119. }
  120. }

    Update

    要求:修改的话其实就是查找并修改,并无太大要求。

    实现思路:当大于节点查找右子树,当小于节点查找左子树,当等于节点,返回节点。

    代码(此处仅仅放出查找的代码):

  1. public static Tree findNode(Tree tree,int value)
  2. {
  3. if(tree.getValue() == value)
  4. {
  5. return tree;
  6. }
  7. else if(tree.getValue() < value){
  8. if(tree.getrChild() != null)
  9. {
  10. return findNode(tree.getrChild(),value);
  11. }
  12. else {
  13. return null;
  14. }
  15. }
  16. else if(tree.getValue() > value){
  17. if(tree.getlChild() != null)
  18. {
  19. return findNode(tree.getlChild(),value);
  20. }
  21. else {
  22. return null;
  23. }
  24. }
  25. return null;
  26. }

    Read

    上文已经把查找的代码和实现思路写出来了,这里写一下中序遍历的两种实现方式和思路吧(先序遍历和后序遍历大同小异,由于是二叉查找树,只实现中序遍历)。

    第一种方式--递归方式:

    实现思路:先递归的遍历左子树,再输出父节点的值,最后再递归地遍历右子树,跳出递归条件:当左子树或者右子树不为空时。

    代码:

  1. //中序遍历
  2. public static void middleSearch(Tree tree)
  3. {
  4. if(tree.getlChild()!=null)
  5. {middleSearch(tree.getlChild());}
  6. System.out.print(tree.getValue());
  7. if(tree.getrChild()!=null)
  8. {middleSearch(tree.getrChild());}
  9. }

    第二种方式--非递归方式:

    实现思路:手动的建立一个栈,循环地去轮询左子树,直到左子树为空,将还未输出的父亲的节点依次压入栈中,当轮询到空树时,开始弹出栈内的节点,输出节点的值,并将节点的右子树作为轮询的值压入栈。

    代码:

  1. //使用非递归方式中序遍历
  2. public static void midddleSearchUseStack(Tree tree)
  3. {
  4. Stack<Tree> stack = new Stack<>();
  5. Tree rollTree = tree;
  6. while(rollTree != null || stack.size() != 0)
  7. {
  8. if(rollTree != null)
  9. {
  10. stack.push(rollTree);
  11. rollTree = rollTree.getlChild();
  12. }else
  13. {
  14. rollTree = stack.pop();
  15. System.out.print(rollTree.getValue());
  16. rollTree = rollTree.getrChild();
  17. }
  18. }
  19. }

    总结:其实两种方式大致无差别,其实递归也是使用方法栈的方式将每一个方法压入栈中,时间复杂度两者没有差异。但是考虑到方法栈的深度限制,和方法栈更多的内存开销(方法栈会将局部变量等信息压入栈中),还有方法调用的开销,显然非递归方式效率是更加高的。但是递归方式有个优点,可读性高,简单,易实现。

    好了,就到这,本文的Java代码并没有全量的放在博客中,笔者将实现的全部代码上传到了GitHub上,需要的同学可以上去下载(记得给星哦)。

    GitHub下载地址:https://github.com/liufangqi/treeTestRealOne

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

闽ICP备14008679号