二叉排序树
为什么要使用树这种存储结构?
首先要对数组和链表存储结构的优缺点进行分析:
数组
对于未排序的数组:插入速度快(直接在尾部插入),但是查询速度慢(时间复杂度为O(n)),需要依次遍历。
对于有序数组:查询时间通过二分查找,时间复杂度为O(logn)。可是当查找到查找到值后,需要将后续的值依次后移,时间复杂度为O(n)。
对于有序数组的插入时间复杂度证明如下:查找O(logn)+后移O(n) = O(n)
链表
对于无序链表:查找的时间复杂度为O(n),插入的时间复杂度为O(1)
对于有序链表:查找和插入的时间复杂度均为O(n)
而数组和链表的删除操作时间复杂度均为O(n)
而对于树存储结构来说:平均查找,插入和删除的时间复杂度均为O(log(n)),并且构建容易,使用稳定。
我们通常采用二叉排序树来解决上述问题
二叉排序树需要满足这样的条件:对于树中任意一个非叶子节点,左子节点一定比当前节点值小,右子节点一定比当前节点值大。
like this:
基于Java完成二叉排序树的构建
首先创建节点类:
属性如下:
class Node{ int value;//值 Node left;//该结点的左子结点 Node right;//该结点的右子节点 }
实现插入方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public void add(Node node){ if (node == null ){ return ; } if ( this .value > node.value){ if ( this .left == null ){ this .left = node; } else { this .left.add(node); } } else { if ( this .right == null ){ this .right = node; } else { this .right.add(node); } } } |
中序遍历:
public void infixOrder(){ if(this.left != null){ this.left.infixOrder(); } System.out.println(this.value); if(this.right!=null){ this.right.infixOrder(); } }
根据value搜索相应的结点:
/* 思想类似于二分查找。比如:如果搜索的值比当前结点的值大,则往其右子树递归查找。 */ public Node search(int value){ if(this.value == value){ return this; }else if(this.value < value){ if(this.right == null){ return null; }else{ return this.right.search(value); } }else{ if(this.left == null){ return null; }else{ return this.left.search(value); } } }
根据value值搜索相应结点的父节点:
在进行搜索的时候会有几种情况需要分类讨论:
搜索值:value 当前值:this.*.value *代表left或right
case0: value = this.*.value
case1:value>this.*.value,右子结点存在时,需往右递归
case2: value<this.*.value,左子结点存在时,需往左递归
case3: 其他情况均不满足,比如value>this.*.value,而右子结点不存在,左子结点存在时。
/** * * @param value * @return 如果该结点的左子结点或右子结点的值等于value则返回 */ public Node getParentNode(int value){ if((this.left != null && this.left.value == value ) ||(this.right != null && this.right.value == value)){ return this; }else if(this.value < value && this.right != null) { return this.right.getParentNode(value); }else if(this.value >= value && this.left != null){ return this.left.getParentNode(value); }else{ return null;//没有对应的父节点 } }
寻找当前结点右子树中value最小的结点:
该方法是为了删除方法作铺垫
/* 因为左子结点的值一定小于右子结点的值,所以当前结点的右子树中最小值需要一直往左递归得到 结合图看思路很清晰 */ public int getRightMinValueNode(){ Node target = this; while(target.left != null){ target = this.left; } return target.value; }
接着构造二叉搜索树:
public class BinarySortTreeDemo { private Node root;//根结点
}
通过调用Node对象的方法实现二叉搜索树的search,getParent,delRightMinValueNode(删除右子树最小结点):
public Node search(int value){ if(root ==null){ return null; }else{ return root.search(value); } } public Node getParent(int value){ if(root == null){ return null; }else{ return root.getParentNode(value); } } public int delRightMinValueNode(Node node){ Node target = node; while(target.left != null){ target = target.left; } delNode(target.value); return target.value; }
树添加新结点和中序遍历的方法:
public void add(Node node){ if(root == null){ root = node; }else { root.add(node); } } public void infixOrder(){ if(root == null){ return; }else{ root.infixOrder(); } }
其实二叉排序树种最难处理的方法也就是删除方法。
需要考虑以下情况:
设删除的结点为:cur
其父结点为 :parent
根结点 :root
由易到难,我们首先考虑根节点的特殊情况:
case0 : root = null;直接返回
case1:只存在root一个结点,则root.left = null; root.right = null;
接着考虑更一般化的情况:
case2: 当结点为叶子结点时,无需考虑叶子结点的左右结点,只需要判断parent.left.value == value 还是
parent.right.value == value 为true;相应地,将parent.left设置为null即可完成删除操作
case3:当结点有且仅有一个子结点(左结点或者右节点),那么分类讨论:
1.当该结点存在左子结点时:
(a) 如果cur为父结点的左子结点,那么通过parent.left = cur.left完成删除
(b) 如果cur为父节点的右子节点时,因为cur右子树的value一定大于cur的value也大于parent.value,
所以通过parent.right = cur.left完成删除
2.当该结点存在右子结点时,该类情况与1类似,不做展开。
case4: 当结点的左右结点均存在时。需要在该结点的子树中找到一个结点来替换。
如下图所示,假如删除的node.value为15,我们需要在红框内找一个结点,其值能完美替代15。
删除的node的value需要满足比右子树所有值小,比左子树所有值大
那么可以选择左子树中值最大的结点 :一直向右递归,得到node1
或者选择右子树中值最小的结点:一直向左递归,得到node1
找到上述结点node1后,将其值赋给要删除的结点,并删除结点node1完成删除操作
public void delNode(int value) { //case0 if (root == null) { return; } //case1 if (root.left == null && root.right == null) { root = null; return; } Node cur = root.search(value); Node parent = root.getParentNode(value); //case2 if (cur.left == null && cur.right == null) { if (parent.right != null && parent.right.value == value) { parent.right = null; } else if (parent.left != null && parent.left.value == value) { parent.left = null; } //case4 } else if (cur.right != null && cur.left != null) { int replacedValue = delRightMinValueNode(cur.right); cur.value = replacedValue; } else { //case3 type(a) if (cur.left != null) { if (parent.left.value == value) { parent.left = cur.left; } else { parent.right = cur.left; } //case3 type(b) } else { if (parent.right.value == value) { parent.right = cur.right; } else if (parent.left.value == value) { parent.left = cur.right; } } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)