Java 数据结构 - 二叉查找树:有了高效的哈希表,为什么还需要二叉树

数据结构与算法之美目录(https://www.cnblogs.com/binarylei/p/10115867.html)

在学习二叉查找树之前,我们先看一下,目前已经接触的几种高效的数据结构的时间复杂度:

  • 有序数组:查找的时间复杂度为 O(logn),但如果数据发生变化,查找前就需要重新排序,时间复杂度就升为 O(nlogn)。所以动态数据不适合使用二分法查找,也就是不支持动态数据。
  • 跳表:通过缓存索引来实现链表的二分法查找,其查找的时间复杂度 O(logn)。另外,在插入和删除时也需要先查找到该结点,因此时间复杂度也是 O(logn)。支持动态数据。
  • 哈希表:通过散列函数和数组的随机访问,实现插入、删除、查找的时间复杂度都是 O(1),是一种非常高效的数据结构。支持动态数据。当然散列函数、散列冲突、加载因子的选择直接决定了哈希表的性能。但哈希表的最大缺点是不支持顺序访问,这也是工程中经常将哈希表和链表一起使用的原因。

1. 基本操作

二叉查找树(Binary Search Tree)也叫排序树或有序树或搜索树,它是为实现快速查找而生。二叉查找树的左子树的节点都小于它的父节点,右子树中的节点都大于它的父节点,因此若按中序遍历则从小到大的排序。

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树,它的严格定义如下:

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

二叉排序树在搜索中的应用非常广泛,同时二叉排序树的一个变种(红黑树)是 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)

  1. 被删除结点没有子结点:直接将其父结点对应的左子树或右子树设置为 null。如删除结点 4 时,pp.left = null 或 pp.right = null。
  2. 被删除结点只有一个子结点:直接将其父结点指向被删除结点的左子树或右子树。如删除结点 7 时,pp.left = p.left 或 pp.right = p.right。
  3. 被删除结点有两个子结点:此时删除比较复杂,需要先从被删除结点的右子树查找最小值结点 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),那为什么还要使用二叉树呢?

  1. 散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
  2. 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定。而尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
  3. 尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
  4. 散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

参考:

  1. 《平衡二叉树的操作》:https://www.cnblogs.com/zhangbaochong/p/5164994.html
  2. 《实现一个自己的二叉查找树》:https://www.cnblogs.com/Dylansuns/p/6793032.html

每天用心记录一点点。内容也许不重要,但习惯很重要!

Java数据结构和算法(二)树的基本操作的更多相关文章

  1. Java数据结构和算法(二)--队列

    上一篇文章写了栈的相关知识,而本文会讲一下队列 队列是一种特殊的线性表,在尾部插入(入队Enqueue),从头部删除(出队Dequeue),和栈的特性相反,存取数据特点是:FIFO Java中queu ...

  2. Java数据结构和算法 - 什么是2-3-4树

    Q1: 什么是2-3-4树? A1: 在介绍2-3-4树之前,我们先说明二叉树和多叉树的概念. 二叉树:每个节点有一个数据项,最多有两个子节点. 多叉树:(multiway tree)允许每个节点有更 ...

  3. Java数据结构和算法(七)B+ 树

    Java数据结构和算法(七)B+ 树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 我们都知道二叉查找树的查找的时间复杂度是 ...

  4. Java数据结构和算法(一)树

    Java数据结构和算法(一)树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 前面讲到的链表.栈和队列都是一对一的线性结构, ...

  5. Java数据结构和算法(七)--AVL树

    在上篇博客中,学习了二分搜索树:Java数据结构和算法(六)--二叉树,但是二分搜索树本身存在一个问题: 如果现在插入的数据为1,2,3,4,5,6,这样有序的数据,或者是逆序 这种情况下的二分搜索树 ...

  6. 【Java数据结构学习笔记之二】Java数据结构与算法之栈(Stack)实现

      本篇是java数据结构与算法的第2篇,从本篇开始我们将来了解栈的设计与实现,以下是本篇的相关知识点: 栈的抽象数据类型 顺序栈的设计与实现 链式栈的设计与实现 栈的应用 栈的抽象数据类型   栈是 ...

  7. Java数据结构和算法(四)赫夫曼树

    Java数据结构和算法(四)赫夫曼树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 赫夫曼树又称为最优二叉树,赫夫曼树的一个 ...

  8. java数据结构和算法07(2-3-4树)

    上一篇我们大概了解了红黑树到底是个什么鬼,这篇我们可以看看另外一种树-----2-3-4树,看这个树的名字就觉得很奇怪.... 我们首先要知道这里的2.3.4指的是任意一个节点拥有的子节点个数,所以我 ...

  9. Java数据结构和算法(十四)——堆

    在Java数据结构和算法(五)——队列中我们介绍了优先级队列,优先级队列是一种抽象数据类型(ADT),它提供了删除最大(或最小)关键字值的数据项的方法,插入数据项的方法,优先级队列可以用有序数组来实现 ...

  10. Java数据结构和算法 - 堆

    堆的介绍 Q: 什么是堆? A: 这里的“堆”是指一种特殊的二叉树,不要和Java.C/C++等编程语言里的“堆”混淆,后者指的是程序员用new能得到的计算机内存的可用部分 A: 堆是有如下特点的二叉 ...

随机推荐

  1. 掩膜操作手写+API(第二天)

    1.1首先是用到的理论知识: 上面是一个通用的公式,光知道上面写程序还是有点麻烦的,下面公式画的有点丑,可以表达我的观点. 1.2用到的知识点:可以边看程序边看用到的知识点: CV_Assert(); ...

  2. python pip使用报错:Fatal error in launcher: Unable to create process using '"'

    在一个系统中共存Python2.python3的时候,pip.pip2.pip3使用的时候会报错: c:\Python35\Scripts>pip3Fatal error in launcher ...

  3. leetcode941

    public class Solution { public bool ValidMountainArray(int[] A) { bool findTop = false; ) { return f ...

  4. Linux SWAP 交换分区配置说明(转)

    一.SWAP 说明 1.1 SWAP 概述 当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用.那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被 ...

  5. Mysql 获取当天,昨天,本周,本月,上周,上月的起始时间

    转自: http://www.cppblog.com/tx7do/archive/2017/07/19/215119.html -- 今天 SELECT DATE_FORMAT(NOW(),'%Y-% ...

  6. Web Deploy

    Web Deploy 服务器安装设置与使用 Win2008R2配置WebDeploy Visual Studio 使用Web Deploy发布项目

  7. gulp 用法 小结

    前端们,gulp该用起来了,简单的demo入门 gulp.grunt前端自动化工具,只有用过才知道多么重要. 作者:一文不提来源:博客园|2015-05-28 10:35 移动端 收藏 分享 gulp ...

  8. Linux 如何将一个文件夹的所有内容授权给某一个用户

    我们可以使用chown命令,ch这里代表change(改变)的意思,own代表英文单词的owner(拥有者),连在一起就是 change owner ,改变某个文件或者文件夹的拥有者. 一般只有roo ...

  9. 磁盘IO过高时的处理办法

    针对系统中磁盘IO负载过高的指导性操作 主要命令:echo deadline > /sys/block/sda/queue/scheduler 注:以下的内容仅是提供参考,如果磁盘IO确实比较大 ...

  10. ADO数据库编程入门

    ADO 是目前在Windows环境中比较流行的客户端数据库编程技术. ADO是建立在OLE DB底层技术之上的高级编程接口,因而它兼具有强大的数据处理功能(处理各种不同类型的数据源.分布式的数据处理等 ...