这节学习一种特殊的二叉树—二叉查找树。它最大的特点是支持动态数据集合的快速插入、删除、查找操作。但是散列表也是支持这些操作的,并且散列表的这些操作比二叉查找树更高效,时间复杂度是 O(1)。

问题引入

既然有高效的散列表,二叉树的地方是不是都可以替换成散列表呢?哪些地方是散列表做不了,必须要用二叉树来做?

二叉查找树(Binary Search Tree)是二叉树中最常用的一种类型,也叫二叉搜索树。它不仅支持快速查找一个数据,还支持快速插入、删除一个数据。二叉查找树要求在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。二叉查找树支持快速查找、插入、删除操作,这三个操作是如何实现的。

1.二叉查找树的查找操作

如何在二叉查找树中查找一个节点。先取根节点,如果等于要查找的数据那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。查找的代码实现

public class BinarySearchTree {

private Node tree;

public Node find(int data) {

Node p = tree;

while (p != null) {

if (data < p.data) p = p.left;

else if (data > p.data) p = p.right;

else return p;

}

return null;

}

public static class Node {

private int data;

private Node left;

private Node right;

public Node(int data) {

this.data = data;

}

}

}

2.二叉查找树插入操作

插入过程有点类似查找。新插入数据一般都是在叶子节点上,从根节点开始依次比较要插入的数据和节点的大小关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,类推。插入的代码

public void insert(int data) {

if (tree == null) {

tree = new Node(data);

return;

}

Node p = tree;

while (p != null) {

if (data > p.data) {

if (p.right == null) {

p.right = new Node(data);

return;

}

p = p.right;

} else { // data < p.data

if (p.left == null) {

p.left = new Node(data);

return;

}

p = p.left;

}

}

}

3. 二叉查找树删除操作

删除操作就比较复杂,针对要删除节点的子节点个数不同需要分三种情况来处理。

第一种情况是,如果要删除的节点没有子节点,只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),只需要更新父节点中指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。

第三种情况是,如果要删除的节点有两个子节点。需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

public void delete(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;

}

关于二叉查找树的删除操作,还有个非常简单、取巧的方法,就是单纯将要删除的节点标记为“已删除”,但是并不真正从树中将这个节点去掉。这样原本删除的节点还需要存储在内存中,比较浪费内存空间,但是删除操作就变得简单了很多。而且,这种处理方法也并没有增加插入、查找操作代码实现的难度

4.二叉查找树的其他操作

除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。二叉查找树除了支持上面几个操作之外,还有一个重要的特性,就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。因此,二叉查找树也叫作二叉排序树。

5.支持重复数据的二叉查找树

二叉查找树除了存储数字外,在实际的软件开发中存储的,是一个包含很多字段的对象。我们利用对象的某个字段作为键值(key)来构建二叉查找树。把对象中的其他字段叫作卫星数据。前面的二叉查找树的操作,针对的都是不存在键值相同的情况。那如果存储的两个对象键值相同,这种情况该怎么处理呢?

有两种解决方法。第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。第二种方法比较不好理解,不过更加优雅。每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。

当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。

对于删除操作,也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

6.二叉查找树的时间复杂度分析

二叉查找树的插入、删除、查找操作的时间复杂度。

如何求一棵包含 n 个节点的完全二叉树的高度?树的高度就等于最大层数减一,为了方便计算,我们转换成层来表示。包含 n 个节点的完全二叉树中,第一层包含 1 个节点,第二层包含 2 个节点,第 K 层包含的节点个数就是 2^(K-1)。对于完全二叉树来说,最后一层的节点个数在 1 个到 2^(L-1) 个之间(我们假设最大层数是 L)。如果我们把每一层的节点个数加起来就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:n >= 1+2+4+8+...+2^(L-2)+1n <= 1+2+4+8+...+2^(L-2)+2^(L-1)借助等比数列的求和公式, L 的范围是[log2(n+1), log2n +1]。

完全二叉树的层数小于等于 log2n +1,完全二叉树的高度小于等于 log2n。我们需要构建一种不管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树,一种特殊的二叉查找树—平衡二叉查找树。平衡二叉查找树的高度接近 logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。

散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是 O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?

有下面几个原因:

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

综合这几点,平衡二叉查找树在某些方面还是优于散列表的,这两者的存在并不冲突。需要结合具体的需求来选择使用哪一个。

24 | 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?相关推荐

  1. 散列表的设计与实现_python基础之字典与集合实现

    更多内容,欢迎关注微信公众号: tmac_lover 上一篇介绍python中字典和集中时,提到,字典和集合的优势在于增删改查非常高效,而高效的原因就在于,python中字典和集合这两个数据结构是基于 ...

  2. linux下epoll如何实现高效处理

    linux下epoll如何实现高效处理 作者 digoal 日期 2016-11-10 标签 Linux , 内核 , epoll , 网络编程 , 高并发 背景 本文转自 http://www.cn ...

  3. 数据结构与算法一 - 二叉树基础

    前言 树是数据结构中的重中之重,尤其以各类二叉树为学习的难点.一直以来,对于树的掌握都是模棱两可的状态,现在希望通过写一个关于二叉树的专题系列.在学习与总结的同时更加深入的了解掌握二叉树.本系列文章将 ...

  4. 计算机二级二叉树基础知识,2017年计算机二级公共基础知识学习教程:树与二叉树...

    (六)树与二叉树 1.树的基本概念 树是一种简单的非线性结构.在树结构中,数据元素之间有着明显的层次结构.在树的图形表示中,用直线连接两端的结点,上端点为前件,下端点为后件. 在树结构中,每一个结点只 ...

  5. 《恋上数据结构第1季》二叉树基础、真二叉树、满二叉树、完全二叉树、二叉树的遍历(重点)

    二叉树(Binary Tree) 树(Tree)的基本概念 有序树.无序树.森林 二叉树(Binary Tree) 二叉树的性质 真二叉树(Proper Binary Tree) 满二叉树(Full ...

  6. 【Python数据结构系列】☀️《树与二叉树-基础知识》——知识点讲解+代码实现☀️

    文章目录 数据结构之树和二叉树 第一部分 树和二叉树的基础知识 1.树和二叉树的定义 1.1 树的定义 1.2 树的基本术语 1.3 二叉树的定义 2.二叉树的性质和存储结构 2.1 二叉树的性质 2 ...

  7. 【数据结构】二叉树(下)

    文章目录 二叉树的链式结构 二叉树的遍历 前序遍历(先根遍历) 中序遍历(中根遍历) 后序遍历(后根遍历) 层序遍历 二叉树的构建 二叉树的节点数 二叉树的叶子数 二叉树的高度 二叉树K层节点数 查找 ...

  8. UE官方教程笔记02-实时渲染基础下

    对官方教程视频[官方培训]02-实时渲染基础下 | 陈拓 Epic的笔记 没听懂的地方就瞎写 反射 实时渲染中反射是一个非常有挑战的特性 UE中有多种不同的方案,各有各的优势和缺点 反射捕获 屏幕空间 ...

  9. 结合二叉树和Graham扫描技术的高效Delaunay三角网构建算法

    结合二叉树和Graham扫描技术的高效Delaunay三角网构建算法 摘 要:为了提高不规则三角网的构建速度,提出了一种高效构建Delaunay三角网算法.首先对平面上的离散点集按一定的阈值进行分块 ...

最新文章

  1. 【论文理解】ArcFace: Additive Angular Margin Loss for Deep Face Recognition(InsightFace)
  2. 神经网络为什么要归一化
  3. Python运行异常 Original error was: DLL load failed:
  4. 使用apache的HttpClient进行http通讯,隐藏的HTTP请求头部字段是如何自动被添加的
  5. spring -mvc 将对象封装json返回时删除掉对象中的属性注解方式
  6. 鸿蒙系统打通iOS,库克真的做到了!正式官宣确认截胡鸿蒙OS系统:软硬件生态全打通...
  7. UI控件之UISlider
  8. 【OpenCV】OpenCV实战从入门到精通之 -- 离散傅里叶变换相关函数详解
  9. 动态规划——最长公共子序列
  10. jquery学习笔记四:ajax
  11. 第26讲 js函数调用过程内存分析 js函数细节
  12. web端四方支付 只有安卓可以跳转
  13. TunesKit Video Cutter for mac(视频分割编辑器)
  14. T6 v6.2puls1 安装了最新补丁之后采购发票结算之后入库单价税合计有1分差额
  15. 解决windows 10电脑插入耳机无声音的问题
  16. 静态路由 直连路由 拓扑
  17. vga接口和hdmi接口的区别
  18. Finger.01 - ESP8266模块STA模式调试
  19. Qemu gdb 调试 Liteos realview-pbx-a9 工程
  20. X2Go Client下载与使用(新手向)

热门文章

  1. 前端学习(2181):vue-router导航守卫的补充
  2. 前端学习(1662):前端系列实战课程之div跟随鼠标移动
  3. 前端学习(1397):项目包含的知识点cookie和session2
  4. Windows Subsystem for Linux(WSL)安装emqx
  5. 移动端click延迟和tap事件
  6. 云计算第二阶段shell脚本
  7. 手写springmvc
  8. 李宏毅机器学习课程---2、Regression - Case Study
  9. fiddler手机端抓包配置
  10. 配置HAProxy支持https协议