文章目录

  • 数据结构的定义
  • 非递归增加节点
  • 递归增加节点
  • 非递归删除节点
  • 递归删除节点
  • 非递归搜索
  • 递归搜索
  • 先序遍历-递归
  • 中序遍历-递归
  • 后序遍历-递归
  • 先序遍历-非递归
  • 中序遍历-非递归
  • 后序遍历-非递归
  • 返回bst树的高度/层数
  • 层序遍历
  • 返回bst树中所有节点个数

搜索二叉树是二叉树的一种特殊情况,特殊在于搜索二叉树有一个特点:左子树都比当前节点的值小,右子树都比当前节点的值大。
先通过一幅图来了解一下,下面这张图就是一个搜索二叉树:

数据结构的定义

先定义节点的数据结构:

class BSTNode<T extends Comparable<T>>{private T data;private BSTNode<T> left;private BSTNode<T> right;public BSTNode(T data, BSTNode<T> left, BSTNode<T> right) {this.data = data;this.left = left;this.right = right;}public T getData() {return data;}public void setData(T data) {this.data = data;}public BSTNode<T> getLeft() {return left;}public void setLeft(BSTNode<T> left) {this.left = left;}public BSTNode<T> getRight() {return right;}public void setRight(BSTNode<T> right) {this.right = right;}
}

上面这个是树的节点的定义,树的节点应该有数据域和两个孩子域,孩子域是递归定义的。底下的方法就是一些get/set方法。
下面定义树的数据结构:

class BST<T extends  Comparable<T>> {private BSTNode<T> root;public BST() {this.root = null;}
}

可以看到树的数据结构就是封装了一个特殊的节点——根节点。

非递归增加节点

 //非递归实现插入节点public void non_insert(T data){// 1.判断如果root是null,如果root是null,直接生成根节点,让root指向,结束if(root == null){this.root = new BSTNode<>(data, null, null);return;}// 2.如果root不为空,从根节点开始搜索一个合适的位置BSTNode<T> cur = this.root;BSTNode<T> parent = null; // this.rootwhile(cur != null){if(cur.getData().compareTo(data) > 0){parent = cur;cur = cur.getLeft();} else if(cur.getData().compareTo(data) < 0){parent = cur;cur = cur.getRight();} else {return;}}// 3.生成新节点,把节点的地址,写入父节点相应的地址域if(parent.getData().compareTo(data) > 0){parent.setLeft(new BSTNode<T>(data, null, null));} else {parent.setRight(new BSTNode<T>(data, null, null));}}

插入的思想主要分为三个步骤:首先要判断插入的是否为根,为根直接直接给root赋值,如果不为根,则通过循环去找合适的位置,最后插入数据。
注意 :如果要增加数据,那么必须知道父节点,也就是说找到合适的位置后,该位置也就为null了,而要在这个位置插入数据,在循环的时候我们必须去维护一个父节点parent。

递归增加节点

 //递归实现节点的插入public void insert(T data) {this.root = insert(root , data);}private BSTNode<T> insert(BSTNode<T> root, T data) {//找到位置if(root == null) {return new BSTNode<>(data,null,null);}if(data.compareTo(root.getData())<0) {//比当前节点小,往左走root.setLeft(insert(root.getLeft(),data));} else if (data.compareTo(root.getData())>0) {//比当前节点大,往右走root.setRight(insert(root.getRight(),data));}//回溯过程中重复设置节点,返回当前节点return root;}

递归的思路和非递归是一样的,但是递归的插入节点是在回溯过程中插入,这就要求递归函数需要有一个返回值。如果root等于null,说明找到了位置,返回一个新创建的data节点,如果不为null,就去根据大小递归左右,而设置节点是在回溯的过程中设置的,也就是每一层递归结束返回后,由上一层节点来设置左右孩子。
注意:这里会有疑问,我需要设置的仅仅是新创建data的节点,如果这样写,那不是相当于每一个节点都设置了一遍?是的,确实是每一个节点都设置了一遍,是一个重复设置的过程,但是如果加上判断,就可以不用重复设置,但是判断也是需要时间的,效率不比重复设置要好,所以避免代码冗余,就重复设置一下。

非递归删除节点

先来理清思路:删除节点有两种情况,一种是该节点有一个孩子或者没有孩子,一种是该节点有两个孩子,为什么这样分?
先来看第一种情况,如果该节点有一个孩子
eg:要删除12这个节点,该节点只有一个孩子

这种情况就可以直接把23的左孩子置为18,也就是让18这个孩子去顶替12的位置,就是下图这样:

这样还是一棵搜索二叉树。
如果没有孩子的情况呢:
eg:要删除18这个节点,该节点没有孩子

可以认为18有一个孩子,为null,还是用上面的规则,用null替换18这个位置,也就相当于直接删除了这个节点。

所以将这两种归为一种情况。

第二种情况比较复杂,有两个孩子节点
eg:删除23这个节点,该节点有两个孩子

在上面这幅图中提到了前驱节点和后继节点

  • 前驱节点是当前节点左子树中最大的节点,也就是说当前节点先往左走,如果能往右走就一直往右走,这样得到的就是前驱节点。前驱节点代码:
     BSTNode<T> cur = node.getLeft();while(cur.getRight()!=null) {cur = cur.getRight();}
  • 后继节点是当前节点右子树中最小的节点,也即是说当前节点先往右走,然后能往左走就一直往左走,这样得到的就是后继节点。后继节点代码:
     BSTNode<T> cur = node.getRight();while(cur.getLeft()!=null) {cur = cur.getLeft();}

为什么要提出前驱节点和后继节点?当该节点有两个孩子的时候,用前驱节点或者后继节点来替换他,这样就能保证搜索二叉树性质的稳定。这里的替换可以直接设置数据,设置完后,删除前驱节点或者后继节点。

所以整个代码的流程是这样的,首先需要找到值为data的节点,如果没有找到,那直接返回。
找到节点后需要判断该节点有几个孩子节点,如果有两个孩子节点,就用前驱节点或者后继节点来替换该节点,替换完后删除前驱节点或后继节点。 如果只有一个孩子节点或者没有孩子节点,直接让孩子节点来顶替该节点的位置。

public void non_remove(T data) {// 1. 先搜索BST树,找到待删除的节点BSTNode<T> cur = root;BSTNode<T> parent = null;while (cur != null) {if(cur.getData().compareTo(data) < 0) {parent = cur;cur = cur.getRight();} else if(cur.getData().compareTo(data) > 0) {parent = cur;cur = cur.getLeft();} else {//找到,跳出循环break;}}if(cur==null) {//找到最下层为null时返回,说明没有找到System.out.println("没找到");return;}// 2. 判断删除节点是否有两个孩子,如果有,用前驱的值代替,删除前驱节点if(cur.getLeft()!=null && cur.getRight()!=null) {BSTNode<T> old = cur;parent = cur;cur = cur.getLeft();while(cur.getRight()!=null) {parent = cur;cur = cur.getRight();}old.setData(cur.getData());}// 3. 删除有一个孩子的节点,或者没有孩子的节点(看作有一个孩子,孩子是null)//如果进入了情况2,那么这里应该删除的是前驱节点//综合起来,这里需要删除cur节点BSTNode<T> child = cur.getLeft();if(child==null) {child=cur.getRight();}if(parent==null) {root = child;} else {if(parent.getLeft()==cur) {parent.setLeft(child);} else {parent.setRight(child);}}
}

注意:代码中cur代表找到的当前节点,如果有两个孩子的情况下,设置完数据后,cur代表的是要删除的前驱或者后继节点,如果有一个孩子的情况下,cur代表的就是当前要删除的数据节点。后面的代码是公用代码,删除cur节点。
同时要注意要维护一个parent节点,原理和增加数据相同。

递归删除节点

public void remove(T data) {remove(root,data);
}
//将删除节点的孩子进行返回
private BSTNode<T> remove(BSTNode<T> root, T data) {//没有找到数据为data的节点if(root == null) {return null;}if(root.getData().compareTo(data) < 0) {root.setRight(remove(root.getRight(),data));} else if (root.getData().compareTo(data) > 0) {root.setLeft(remove(root.getLeft(),data));} else {//找到了该节点if(root.getLeft()!=null && root.getRight()!=null) {//有两个孩子,找前驱节点BSTNode<T> cur = root.getLeft();while(cur.getRight()!=null) {cur = cur.getRight();}root.setData(cur.getData());//删除cur:前驱节点root.setLeft(remove(root.getLeft(),cur.getData()));} else {//有一个孩子或者没有孩子if(root.getLeft()!=null) {return root.getLeft();} else if (root.getRight()!=null) {return  root.getRight();} else {return null;}}}return root;
}

这段递归代码同样是在回溯的时候进行设置节点的,其他的原理和非递归删除一样。

非递归搜索

public boolean non_query(T data){BSTNode<T> cur = this.root;while(cur != null) {if(cur.getData().compareTo(data) > 0){cur = cur.getLeft();} else if(cur.getData().compareTo(data) < 0){cur = cur.getRight();} else {return true;}}return false;
}

递归搜索

public boolean query(T data) {return query(this.root,data);
}private boolean query(BSTNode<T> root, T data) {if(root == null) {return false;}if(root.getData().compareTo(data) == 0) {return true;}boolean left = false;boolean right = false;if(root.getData().compareTo(data) < 0) {left = query(root.getRight(),data);} else if(root.getData().compareTo(data) > 0) {right = query(root.getLeft(),data);}return left || right ;
}

递归搜索和非递归搜素都利用了搜索二叉树的性质:左子树的所有节点都比该节点小,右子树的所有节点都比该节点大。小了就往左搜索,大了就往右搜索。

先序遍历-递归

public void preOrder() {System.out.print("前序遍历:");preOrder(root);
}private void preOrder(BSTNode<T> root) {if(root!=null) {System.out.print(root.getData()+" ");preOrder(root.getLeft());preOrder(root.getRight());}
}

中序遍历-递归

public void inOrder() {System.out.print("中序遍历:");inOrder(root);
}private void inOrder(BSTNode<T> root) {if(root!=null) {inOrder(root.getLeft());System.out.print(root.getData()+" ");inOrder(root.getRight());}
}

后序遍历-递归

public void postOrder() {System.out.print("后序遍历:");postOrder(root);
}private void postOrder(BSTNode<T> root) {if(root!=null) {inOrder(root.getLeft());inOrder(root.getRight());System.out.print(root.getData()+" ");}
}

这三种遍历方式就是访问节点的时间不同,画图结合代码很好理解,重点看一下非递归如何去实现

先序遍历-非递归

  非递归肯定是需要使用到循环来进行的,但是循环变量只有一个,而遍历方向有两个,即左子树和右子树,这种情况下,我们可以固定向左遍历,如果无法走通,则回退向右走一步,典型的dfs思想。而如果要进行回退,那么肯定需要用到栈。先序遍历是第一次碰到节点就进行打印,一起看一下代码

    //非递归先序遍历public void non_preOreder() {LinkedList<Entry> stack = new LinkedList<>();Entry cur = this.root;while (true) {//如果当前节点为空,而且栈里面也没有了,说明遍历完了if(cur == null && stack.size() == 0) {return;}//这里是指当前节点为空,栈里面不为空,则出栈,当前节点置为出栈节点的右子树if(cur == null) {cur = stack.pop().right;continue;}//打印当前节点,入栈System.out.print(cur.data+" ");stack.push(cur);cur = cur.left;}}

中序遍历-非递归

  中序遍历的框架其实和先序遍历框架基本相同,只是打印的位置不同,也就是说,中序遍历是在回退时进行访问,说白了也就是出栈的时候进行访问

    //非递归中序遍历public void non_inOrder() {LinkedList<Entry> stack = new LinkedList<>();Entry cur = this.root;while (true) {//如果当前节点为空,而且栈里面也没有了,说明遍历完了if(cur == null && stack.size() == 0) {return;}//这里是指当前节点为空,栈里面不为空,则出栈,当前节点置为出栈节点的右子树if(cur == null) {Entry pop = stack.pop();System.out.print(pop.data+" ");cur = pop.right;continue;}stack.push(cur);cur = cur.left;}}

后序遍历-非递归

  后续遍历其实也一样,不过后续遍历的访问是在回退第二次进行访问,画个图来理解第二次

以23举例,黄色圈是第一次回退,也就是第一次出栈,但是这时并不访问,而是继续将其入栈,第二次回退绿色圈进行访问

所以思路就很清晰了,只需要判断是否是第二次回退即可,很多人实现的思路是在节点中加上标志位,这种就修改了原来定义的节点,所以我的实现方式是定义一个数组,如果第一次回退,把该元素加入到数组中,如果是第二次回退,则发现数组中有,这样就能分清了,看下代码:

 //非递归后序遍历public void non_postOrder() {LinkedList<Entry> stack = new LinkedList<>();ArrayList<Entry> list = new ArrayList<>();Entry cur = this.root;while (true) {if(cur == null && stack.size() == 0) {return;}if(cur == null) {//回溯的过程,判断是从左边回来还是从右边回来Entry pop = stack.pop();if(!list.contains(pop)) {list.add(pop);stack.push(pop);} else {//第二次回退,即从右边回来System.out.print(pop.data+" ");continue;}cur = pop.right;continue;}//访问当前cur节点stack.push(cur);cur = cur.left;}}

返回bst树的高度/层数

public int level() {return level(root);
}private int level(BSTNode<T> node) {if(node != null) {int left = level(node.getLeft());int right = level(node.getRight());//当前节点的高度是左子树和右子树较大的高度加1return Math.max(left,right) + 1;}return 0;
}

这里要搞清楚当前节点的高度是左子树和右子树较大的高度加1

层序遍历

public void levelOrder() {int hight = level();for (int i = 0; i < hight; i++) {levelOrder(root,i);}
}private void levelOrder(BSTNode<T> root,int level) {if(root!= null) {if(level==0) {System.out.print(node.getData()+" ");}levelOrder(root.getLeft(),level-1);levelOrder(root.getRight(),level-1);}
}

层序遍历是分层次进行遍历的,每一层都要进行遍历。相当于每一个层数i,都要开始一次新的深度遍历(DFS),最终组合起来的就是层序遍历,也就是(BFS:广度优先搜索)

也可以通过一个队列来进行层序遍历

public static void print(Entry root) {//定义队列LinkedList<Entry> queue = new LinkedList<>();//定义本次打印的最后一个节点Entry last = null;if(root != null) {queue.offer(root);last = root;while (queue.size() != 0) {Entry cur = queue.poll();System.out.print(cur.val+" ");if(last == cur) {System.out.println();}boolean isLast = (cur == last);if(cur.left != null) {queue.offer(cur.left);if(isLast) {last = cur.left;}}if(cur.right != null) {queue.offer(cur.right);if(isLast) {last = cur.right;}}}}}

这里注意要按行进行打印的话,我需要去记录一下我每行的最后一个节点,然后不断更新每行最后的节点,访问节点的时候,判断是否是最后一个节点,如果是最后一个节点的,则换行

返回bst树中所有节点个数

public int number() {return number(root);
}private int number(BSTNode<T> node) {if(node!=null) {return number(node.getLeft())+number(node.getRight())+1;}return 0;
}

和返回高度类似,当前树的节点个数等于左子树的节点个数和右子树的节点个数加上该树的根节点

Java实现BST:搜索二叉树相关推荐

  1. java递归方法建立搜索二叉树,具备查找关键字,插入新节点功能

    二叉排序树的定义: 二叉排序树满足以下三个性质(BST性质): <1>若它的左子树非空,则左子树上所有节点的值均小于根节点的值 <2>若它的右子树非空,则右子树上所有节点的值均 ...

  2. 《程序员代码面试指南》第二章 链表问题 搜索二叉树转换为双向链表

    样例 树的中序遍历:1 2 3 4 5 6 7 ,转换后双向链表的遍历:1 2 3 4 5 6 7 java代码 /*** @Description:搜索二叉树转换为双向链表* @Author: li ...

  3. 左神算法:将搜索二叉树转换成双向链表(Java版)

    本题来自左神<程序员代码面试指南>"将搜索二叉树转换成双向链表"题目. 题目 对二叉树的节点来说,有本身的值域,有指向左孩子节点和右孩子节点的两个指针:对双向链表的节点 ...

  4. 左神算法:调整搜索二叉树中两个错误的节点(Java版)

    本题来自左神<程序员代码面试指南>"调整搜索二叉树中两个错误的节点"题目. 题目 原问题: 一棵二叉树原本是搜索二叉树,但是其中有两个节点调换了位置,使得这棵二叉树不再 ...

  5. 判断一棵二叉树是否为搜索二叉树、完全二叉树、平衡二叉树(java)

    平衡二叉树的解法:主要是求出二叉树的高度,若根节点的左子树的高度与右子树的高度差小于等于1,则表示该二叉树为平衡二叉树 public static class Node{public int valu ...

  6. 判断一个数列是不是搜索二叉树后续遍历输出的结果

    剑平面阿里被问到这个,刚开始画了下看有什么性质,乱蒙了几个都被推翻了,初始感觉就是要O(n)的,因为印象中BST的构树都可以O(nlogn)搞定.然后剑平说最后一个数肯定是根节点,一下反应过来了,就是 ...

  7. 算法练习day11——190329(平衡二叉树、搜索二叉树、完全二叉树)

    1.平衡二叉树 判断一棵树是否为平衡二叉树 1.1 分析 首先需要得到一个节点的以下四个信息: 左子树是否平衡: 右子树是否平衡: 左子树的高度: 右子树的高度. 接着,设计递归: 应该返回以此节点为 ...

  8. 将搜索二叉树转换为链表_将给定的二叉树转换为双链表(DLL)

    将搜索二叉树转换为链表 Given a Binary tree and we have to convert it to a Doubly Linked List (DLL). 给定二叉树,我们必须将 ...

  9. 用java设计一个二叉树类的结构,在JAVA中实现的二叉树结构

    在JAVA中实现的二叉树结构 [日期:2008-12-16] 来源:IT专家网 作者:竹一 [字体:大 中 小] * * 讲解: * 二个方法函数,一个寻找关键字--searchkey 另一个是插入一 ...

最新文章

  1. egg mysql 连表查询_Egg中使用Sequelize框架关联查询Mysql数据库
  2. 启动Kafka失败Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c0000000, 107
  3. ABAP Smart Help调试截图
  4. 《标准普通话教程》中对平舌音的发音方法的说明
  5. Windows动态定义模板类对象
  6. java elementtext_java命名空间javax.xml.stream接口xmlstreamreader成员方法: getelementtext定义参考...
  7. 如何看注解的源码_我们为什么要看源码、应该如何看源码?
  8. SAE SENT单边半字节传输协议里的CRC4与CRC6检验码编程
  9. 485通讯温湿度传感器工作原理
  10. 天猫精灵家居对接第三方设备(详细版)
  11. 计算机教室标语6个字,教室标语大全
  12. 大数据、人工智能带来的危机:科技巨头会毁掉我们的生活吗?
  13. 什么叫做正向代理和什么叫做反向代理
  14. Qt设置鼠标光标样式
  15. java连接phoenix
  16. 制作自己的iconfont 图片转iconfont
  17. Gossip算法详解
  18. Tungsten Fabric(6):部署更高版本的TF
  19. 【GDOI2019Day1模拟2019.4.28】爱乐之城
  20. 科技创业企业密集关注网络电话等通信产业

热门文章

  1. 树莓派与双色Led模块的那些事儿
  2. 我是初来的,请多多指教
  3. Wins服务器删除和逻辑删除记录
  4. Vue-i18n,非常好用的前端国际化插件,智能切换中英文
  5. KITTI数据集(概念版)
  6. 冷王为新冠疫苗仓储及运输提供冷链解决方案
  7. 1265: [蓝桥杯2015决赛]四阶幻方 C/C++
  8. Android模拟器获取IP的方法
  9. excel的100种便捷用法(1):vlookup如何匹配两列数据
  10. 使用satis 搭建 自己composer 代码库 教程02