面试官:HashMap 为什么选用红黑树这种数据结构优化链表?

1,875 阅读5分钟

前言

关于HashMap的详解文章请移步:

深度剖析HashMap一篇文章就够了

自平衡二叉查找树(Self-Balancing Binary Search Tree)

二叉查找树操作的运行时间与树的高度有密切关系。一个树的高度指的是从树的根开始所能到达的最长路径的长度。
计算数的高度要从叶子节点开始,首先将叶子节点高度置为0,沿着树的路径向上计算父节点的高度,以此类推直到所有树的节点高度都被标注后,则根节点的高度就是树的高度。

eg:

下图展示了几个已经计算好高度的 BST

如果树中节点的数量为 n,则一棵满足O(log2n) 渐进运行时间的 BST 树的高度应接近于比 log2n 小的最大整数。

上图的三棵树中,树 b 拥有最好的高度与节点数量的比例,树 b 的高度为 3,n(节点数量)为 8,所以 log28 = 3,正好与树的高度相等。

树 a 的 n 为10,高度为 4,log210 = 3.3219,比 3.3219 小的最大整数是 3,所以树(a)最理想的高度应该为 3,我们可以通过移动距离最远的节点到中间的某个非叶子节点,以减少树的高度,使树的高度与节点的数量比例达到最优。

树 c 的比例最差,它的节点数量是 5,所以log25 = 2.3219,则理想高度为 2,但实际上是 4

那我们的问题是如何保证 一颗 BST 的拓扑结构始终保持树的高度与节点数量的比例最佳。我们需要让新的节点插入后仍可以保持BST的平衡,这种能够始终保持平衡的BST就是平衡二叉查找树。

而 具有这种自平衡功能的 BST有很多,eg:

  • AVL树
  • 红黑树
  • 2-3树
  • 2-3-4树
  • 伸展树
  • B树
  • … …

本文着重介绍两种:红黑树和 AVL树

AVL

在 1962 年,俄罗斯数学家 G. M. Andel’son-Vel-skii 和 E. M. Landis 发明了第一种自平衡二叉查找树,叫做 AVL 树。AVL 树必须维持如下平衡条件,对每个节点 n:

  • 节点 n 的左子树的高度与右子树的高度的差至多是 1。

下图是一些 BST,其中 (a) (b) 是合法的 AVL 树, © (d) 不合法,因为树中不是所有的节点都满足 AVL 的平衡性质要求。(图片来自于网络
在这里插入图片描述
当创建一颗 AVL 树,重点在于 是 BST 的基础上,如何保证树的平衡,无论向树中添加/删除节点都要始终保持树的平衡。 AVL 树是通过"旋转"操作来保持树的平衡。

当向 AVL 树中添加一个节点后,会有两个步骤:

  • 首先,插入新节点的操作与BST树插入节点的操作相同,新的节点被放到合适的位置,以满足 BST 的性质要求
  • 满足 BST 的性质基础上,此时可能违背了 AVL 的性质,通过遍历访问路径,检查每个节点左右子树高度,如果存在某节点的左右子树高度差大于1时,则需要使用旋转操作来处理。

红黑树(R-B Tree)

在 1972 年,慕尼黑理工大学(Technical University of Munich)的计算机科学家 Rudolf Bayer 创造了红黑树(Red-Black Tree)数据结构。除了包含数据和左右孩子节点之外,红黑树的节点还包含了一项特别的信息 – 颜色。这个颜色只包含两种颜色,即红色和黑色。并且,红黑树还添加了一种特殊类型的节点,称为 NIL 节点。NIL 节点将做为红黑树的伪叶子节点出现。也就是说,所有带有关键数据的节点称为内节点,而所有其他的外节点则均指向 NIL 节点。

红黑树的性质如下:

  • 节点是红色/黑色
  • 根节点一定是黑色
  • 每个叶节点(NIL节点、空节点)是黑色的
  • 每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

假设向红黑数中插入一个新节点,会有如下操作:

  • 使用 BST 插入算法插入节点,以满足BST的性质要求
  • 通过将元素重新着色和旋转树结构以满足 R-B Tree 的性质

AVL与红黑树异同

我们可以清晰的对比出来,AVL树和红黑树插入节点时的操作有异同,相同的是都需要首先满足 BST 的性质要求,再根据各自的性质去做调整,AVL通过旋转操作来满足自身性质,而红黑树需要通过着色和旋转两个操作来满足自身性质要求。

我们来对比一下 AVL 与 R-B Tree 的性能:

红黑树的查询性能略逊色于AVL树,因为它比AVL树会稍微不平衡(最多一层),也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较。但是红黑树在插入和删除操作上的性能要优于 AVL树,AVL树每次插入/删除操作都会进行大量的平衡度计算,而红黑树为了维持红黑树的性质所做的着色和旋转的开销,相对于 AVL 为了维持严格的平衡性的开销要小的多。
这也是为什么 Hashmap 比 AVL 应用更广泛的原因。

所以,如果你的程序中,查询的次数远大于插入/删除,那么选择 AVL,如果查询,插入/删除的操作差不多,那应该选择 红黑树。

应用场景

AVL树的应用场景:

  • windows对进程地址空间的管理

红黑树的应用场景:

  • epoll在内核中的实现,用红黑树管理事件块(文件描述符)
  • Java的TreeMap实现
  • nginx中,用红黑树管理timer
  • linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块

小结

本文是作者的一些见解,如对Java集合感兴趣可继续关注本专栏。