Java数据结构和算法(二)树的基本操作
Java 数据结构 - 二叉查找树:有了高效的哈希表,为什么还需要二叉树
数据结构与算法之美目录(https://www.cnblogs.com/binarylei/p/10115867.html)
在学习二叉查找树之前,我们先看一下,目前已经接触的几种高效的数据结构的时间复杂度:
- 有序数组:查找的时间复杂度为 O(logn),但如果数据发生变化,查找前就需要重新排序,时间复杂度就升为 O(nlogn)。所以动态数据不适合使用二分法查找,也就是不支持动态数据。
- 跳表:通过缓存索引来实现链表的二分法查找,其查找的时间复杂度 O(logn)。另外,在插入和删除时也需要先查找到该结点,因此时间复杂度也是 O(logn)。支持动态数据。
- 哈希表:通过散列函数和数组的随机访问,实现插入、删除、查找的时间复杂度都是 O(1),是一种非常高效的数据结构。支持动态数据。当然散列函数、散列冲突、加载因子的选择直接决定了哈希表的性能。但哈希表的最大缺点是不支持顺序访问,这也是工程中经常将哈希表和链表一起使用的原因。
1. 基本操作
二叉查找树(Binary Search Tree)也叫排序树或有序树或搜索树,它是为实现快速查找而生。二叉查找树的左子树的节点都小于它的父节点,右子树中的节点都大于它的父节点,因此若按中序遍历则从小到大的排序。
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树,它的严格定义如下:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 左、右子树也分别为二叉排序树;
- 没有键值相等的节点。
二叉排序树在搜索中的应用非常广泛,同时二叉排序树的一个变种(红黑树)是 java 中 TreeMap 和 TreeSet 的实现基础。我们看一下二叉查找树的查找、插入、删除这些基本的操作,重点关注其时间复杂度,也就明白了为什么要使用平衡二叉查找树。
1.1 查找
二叉查找树的查找的代码比较简单。
public Object search(Node tree, int value) {
Node p = tree;
while (p != null) {
if (value < p.data) {
p = p.left;
} else if (value > p.data) {
p = p.right;
} else {
return p.data;
}
}
return null;
}
说明: 我们来分析一下二叉查找树的查找的时间复杂度,如果二叉树比较平衡,时间复杂度就是 O(logn)。但如果二叉树退化成了链表,那么时间复杂度就降为 O(n)。
1.2 插入
二叉查找树的插入也很简单,因为只能插入到叶子结点上。
private Node tree;
public void add(int value) {
if (tree == null) {
tree = new Node(value);
return;
}
Node p = tree;
while (p != null) {
if (value < p.data) {
if (p.left == null) {
p.left = new Node(value);
break;
}
p = p.left;
} else {
if (p.right == null) {
p.right = new Node(value);
break;
}
p = p.right;
}
}
}
说明: 在插入前也需要查找结点所在的位置,因此平衡二叉树的最好时间复杂度为 O(logn),但如果退化为链表则最坏时间复杂度为 O(n)。
1.3 删除

删除元素则会比较复杂一点,初删除的结点可能是叶子结点、左子树或右子树只存在一个、左子树或右子树两个都存在。前两种情况比较简单一点(被删除结点 p,删除结点的父结点 pp)
- 被删除结点没有子结点:直接将其父结点对应的左子树或右子树设置为 null。如删除结点 4 时,pp.left = null 或 pp.right = null。
- 被删除结点只有一个子结点:直接将其父结点指向被删除结点的左子树或右子树。如删除结点 7 时,pp.left = p.left 或 pp.right = p.right。
- 被删除结点有两个子结点:此时删除比较复杂,需要先从被删除结点的右子树查找最小值结点 p2 赋值给被删除结点。这样就变成删除这个右子树查找最小值结点 p2 的问题,同第一种情况完全一样。如删除结点 20 时间,需要在其右子树中查找一个最小的值结点 25,将 20 对应结点的值替换成 25,然后再删除 25。
public void remove(int data) {
Node p = tree; // p指向要删除的节点,初始化指向根节点
Node pp = null; // pp记录的是p的父节点
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
// 没有找到
if (p == null) return;
// 要删除的节点有两个子节点
if (p.left != null && p.right != null) { // 查找右子树中最小节点
Node minP = p.right;
Node minPP = p; // minPP表示minP的父节点
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 将minP的数据替换到p中
p = minP; // 下面就变成了删除minP了
pp = minPP;
}
// 删除节点是叶子节点或者仅有一个子节点
Node child; // p的子节点
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child; // 删除的是根节点
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
说明: 删除时首先也要查找这个被删除的结点,时间复杂度为 O(logn)。如果有两个子结点时,还需要查找其右子树的最小值,时间复杂度也是 O(logn)。最后删除这个结点。因此总的时间复杂度也是 O(logn)。但如果退化成链表,时间复杂度还是 O(n)。
总结: 二叉查找树的时间复杂度都和树的高度息息相关,也就是 O(height)。如果满足平衡二叉树时树高度为 logn,如果退化成链表时树高度为 n。因此,实际工程使用中,我们使用的是平衡二叉查找树,但为什么又不是完全满足平衡二叉树的红黑树呢?我们在下节继续分析。
2. 二叉查找树 vs 哈希表
前面也说了,哈希表的插入、删除、查找的时间复杂度都是 O(1),而平衡二叉查找树的插入、删除、查找的时间复杂度都是 O(logn),那为什么还要使用二叉树呢?
- 散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
- 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定。而尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
- 尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
- 散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。
参考:
- 《平衡二叉树的操作》:https://www.cnblogs.com/zhangbaochong/p/5164994.html
- 《实现一个自己的二叉查找树》:https://www.cnblogs.com/Dylansuns/p/6793032.html
每天用心记录一点点。内容也许不重要,但习惯很重要!
Java数据结构和算法(二)树的基本操作的更多相关文章
- Java数据结构和算法(二)--队列
上一篇文章写了栈的相关知识,而本文会讲一下队列 队列是一种特殊的线性表,在尾部插入(入队Enqueue),从头部删除(出队Dequeue),和栈的特性相反,存取数据特点是:FIFO Java中queu ...
- Java数据结构和算法 - 什么是2-3-4树
Q1: 什么是2-3-4树? A1: 在介绍2-3-4树之前,我们先说明二叉树和多叉树的概念. 二叉树:每个节点有一个数据项,最多有两个子节点. 多叉树:(multiway tree)允许每个节点有更 ...
- Java数据结构和算法(七)B+ 树
Java数据结构和算法(七)B+ 树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 我们都知道二叉查找树的查找的时间复杂度是 ...
- Java数据结构和算法(一)树
Java数据结构和算法(一)树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 前面讲到的链表.栈和队列都是一对一的线性结构, ...
- Java数据结构和算法(七)--AVL树
在上篇博客中,学习了二分搜索树:Java数据结构和算法(六)--二叉树,但是二分搜索树本身存在一个问题: 如果现在插入的数据为1,2,3,4,5,6,这样有序的数据,或者是逆序 这种情况下的二分搜索树 ...
- 【Java数据结构学习笔记之二】Java数据结构与算法之栈(Stack)实现
本篇是java数据结构与算法的第2篇,从本篇开始我们将来了解栈的设计与实现,以下是本篇的相关知识点: 栈的抽象数据类型 顺序栈的设计与实现 链式栈的设计与实现 栈的应用 栈的抽象数据类型 栈是 ...
- Java数据结构和算法(四)赫夫曼树
Java数据结构和算法(四)赫夫曼树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 赫夫曼树又称为最优二叉树,赫夫曼树的一个 ...
- java数据结构和算法07(2-3-4树)
上一篇我们大概了解了红黑树到底是个什么鬼,这篇我们可以看看另外一种树-----2-3-4树,看这个树的名字就觉得很奇怪.... 我们首先要知道这里的2.3.4指的是任意一个节点拥有的子节点个数,所以我 ...
- Java数据结构和算法(十四)——堆
在Java数据结构和算法(五)——队列中我们介绍了优先级队列,优先级队列是一种抽象数据类型(ADT),它提供了删除最大(或最小)关键字值的数据项的方法,插入数据项的方法,优先级队列可以用有序数组来实现 ...
- Java数据结构和算法 - 堆
堆的介绍 Q: 什么是堆? A: 这里的“堆”是指一种特殊的二叉树,不要和Java.C/C++等编程语言里的“堆”混淆,后者指的是程序员用new能得到的计算机内存的可用部分 A: 堆是有如下特点的二叉 ...
随机推荐
- java技术-重点方向
多线程 锁 事务 缓存 hashmap 并发编程
- python学习笔记一和PHP的一些对比
python和php一样是 解释性语言 php和php一样 定义变量不需要指定类型,C语言中的print函数 在python中也是适用的 python编码 适用缩进 4个空格,通常存储为UTF-8模 ...
- centos6.5 64练手安装memcached,PHP调试
思路 先安装 memcached 然后安装php的基于扩展libmemcache ,然后安装php memcache扩展包,然后把扩展添加到php.ini 1 yum安装 简单方便 yum ins ...
- Hive 简介
hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供简单的sql查询功能,可以将sql语句转换为MapReduce任务进行运行. 其优点是学习成本低,可以通过 ...
- 10. 数据模型(ER图)转为sql脚本,要求导出的字段都是大写的
1.进入Tools-->Execute Commands-->Edit/Run Script,执行以下脚本即可实现所有字段转为大写; Option Explicit ValidationM ...
- storyboard中UIButton setframe 不起作用
将storyboard的autolayout选项关掉!(暂时没发现具体什么原因.)
- box2d 易错
1.动态刚体与一个与静态刚体重叠的小的感应刚体在contactBegin时,有些时候无法侦测到 2.bodyA.GetContactList()应用非动态刚体,会找不到另一非动态刚体的接触,非动态刚体 ...
- python生成器(转)
生成器是一种特殊的迭代器,内部支持了生成器协议,不需要明确定义__iter__()和next()方法.生成器通过生成器函数产生,生成器函数可以通过常规的def语句来定义,但是不用return返回,而是 ...
- oracle与DB2
1.体系结构,DB2的实例和数据库分开的做法,我个人还是比较喜欢的,因为实例可以创建多个,数据库的恢复直接恢复到实例下就可以了,相对ORACLE简单多了. 2.管理工具,DB2的管理工具做得太简陋了, ...
- centos7 端口转发
firewall-cmd --add-masquerade firewall-cmd --add-forward-port=port=3001:proto=tcp:toaddr=172.17.18 ...