hashmap中用红黑树不用其他树_极致 HashMap(6)1.8 版引入红黑树

4cdf635db24bedb1f876ba3cff67b3b2.png

1.8 是目前运行的稳定版本之一。其 HashMap 的数据结构发生很多变化。先说一个小变化,Entry 改名成了 Node。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

HashMap 的数据结构还有很多大变化,其增加了三个重要的参数:

/**
 * 链表达到8之后就要树化,这个数在2到8之间。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 树的节点小于6就反树化
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 表的容量如果不达到这个上限就不允许节点树化,这个值至少是 4 * TREEIFY_THRESHOLD
 * 在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

引入了树主要是解决 1.7 中出现的一个桶里的 hash 碰撞极为严重导致了链表特别长的问题。这个阈值 8 的理由在上节之中有过不明显的解释,核心就是 Java 认为 HashMap 认为不太会有大于8的链表。

(百万分之六)


但是, hashcode 函数是可以由用户实现的,如果 hashcode 函数实现的比较差,大于 8 的链表会经常出现并且引发严重的性能问题。

极限情况下就是 HashMap 直接退化成一条极长的链表。

HashMap 对于这个问题的解决方案是引入红黑树。

那为什么要用红黑树呢?而且 Java 底层实现还大量地依赖红黑树。


首先要思考的是由于 hash 碰撞产生的较长链表中如何快速定位数据的,该链表长度大于8。一定要用链地址法吗?换别的方式不就不产生长链表了,一劳永逸。

—— 是。只能用这个。

HashMap 产生较长链表是因为它选择了链地址法而不是开放地址法,这是因为 HashMap 用的是小表 + 扩容,数组长度永远是 2 的幂。

这种数组长度在进行线性探测或二次探测时会产生大量的跳空,因此不适合使用开放地址法。

使用开放地址法的时候,如果数组长度不是质数,那么总有一个比原数小比1大的数是长度的因子。当我们按照这个因子去移动时,会使得这种移动探测跳过某些空位,变成只在固定的几个位置上进行检测。如果长度是质数就会避免这种情况。

那么一定要把链表转化成树吗?把这个链表转化成一份新的哈希表行不行?

—— 不太行,因为这样就是要无限循环了

那为什么就是树?为什么就是二叉树呢?

—— 需要尽可能高的查找性能,所以选择树。

查找树有非常多种,为什么是红黑树?

—— 这要涉及二叉搜索树的发展历史了。


首先说二叉搜索树 Binary Search Tree,简称 BST。

二叉搜索树,又名二叉排序树。它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点均小于根
  • 若它的右子树不空,则右子树上所有结点均大于根
  • 左右子树也为二叉排序树

如果把一组数字构建成一个 BST,这个 BST 的中序遍历将是升序的。它的查找就是二分查找,最大查找次数就是树的高度。

然而 BST 存在一个问题,

最差情况下它将退化为顺序查找度。这是无法接受的。当然,造成这种情况的原因就是 BST 不够平衡 ( 左右子树高度差太大 ) 。


因为 BST 不够平衡,AVL 树诞生了。

AVL 树也被称为高度平衡树,其得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。

AVL 树是最早的自平衡二叉树 BBST ( Balanced Binary Search Tree ),相比于后来出现的(自)平衡二叉树(红黑树,treap,splay树)而言,它现在应用较少,但研究 AVL 树对于了解后面出现的常用(自)平衡二叉树具有重要意义。

在 AVL 树中任何节点的两个儿子子树的高度最大差为一。它查找、插入和删除在平均和最坏情况下都是O(log (n))。但是付出的代价是节点增加和删除时需要通过一次或多次树旋转来重新平衡。

AVL 树满足以下的条件:

  • 左右子树都是 AVL 树
  • 左右子树的高度差不超过 1

AVL 树能保持这种性质,是因为其有树旋转。树旋转具体在做什么?

—— AVL 树引入了一个平衡因子 bf ,默认为 0,0 表示空树就是平衡二叉树。AVL 插入节点时,如果插左则平衡因子减 1 ,右则加 1 。

248cf8b72d24405700a80422082f9660.png

理论上已经足够了,但是行业内又提出了红黑树( RB-Tree) ,B 树 ,B+ 树 ,Trie 树(字典树)

0b2f0e8529c7ce963329daa6cb5817b4.png
AVL 树是平衡二叉搜索树的鼻祖,它的平衡度也最好,左右高度差可以保证在「-1,0,1」。基于它的平衡性,它的查询时间复杂度可以保证是O(log n)。但每个节点要额外保存一个平衡值,或者说是高度差。这种树是二叉树的经典应用,现在最主要是出现在教科书中。AVL 的平衡算法比较麻烦,需要左右两种 rotate 交替使用,需要分四种情况,是数据结构课的最理想课后作业之一。

AVL 的平衡算法比较麻烦。反过来看,其实树不平衡最关键的问题是什么?

—— 根节点选择的不好,如果这个根节点是最小或最大的,那么这个树的平衡性一定很差。

构建一棵平衡的二叉搜索树的关键在于选取“正确”的根节点。

那么如何在每次构建平衡二叉搜索树时都能选取合适的根节点呢?

这里要用到一种重要的树:2-3 树(读作二三树)。


先看 2-3树 的定义,

所谓2-3树,是即满足二叉搜索树的性质,且节点可以存放一个元素或者两个元素,每个节点有两个或三个孩子的树。

  • 满足二叉搜索树的性质
  • 节点可以存放一个或两个元素
  • 每个节点有两个或三个子节点

e112e3521b5f5e1fa48038bf25b9953f.png

为了保持平衡性,2-3 树的引入了树分裂和合并。


可以能看出业界解决这个问题的思路,

一方面 BST的确不行,另一方面 AVL 带来的树旋转操作实在是成本太高了,所以引入2-3 树。然而,如果节点可以无限存储,这玩意就原地退化成链表,所以必须要给个限制。2-3 树的限制就是要求一个节点最多三个叶子。

那可以给一个限制4吗?

—— 可以的,2-3-4树。

2-3-4树,每个节点可以保存一个、两个或者三个数据项。

已经不太常用了,因为代码的复杂度有点过。


再看红黑树,

红黑树就是 2-3 树。

先看红黑树的定义,

  • 每个节点或者是黑色,或者是红色。
  • 根节点是黑色。
  • 每个叶子节点是黑色。
  • 如果一个节点是红色的,则它的子节点必须是黑色的
  • 从任意一个节点到叶子节点,经过的黑色节点是一样的。

260ed4a3808e35963c33d4efdf044b16.png
  • 2-3 树的 1 节点对应于红黑树的黑色节点。
  • 2-3 树的 2 节点对应于红黑树黑色的父节点和红色的左孩子节点。
  • 2-3 树的 3 节点对应于红色的父节点和黑色的左右孩子节点。

这一节中介绍了 BST,AVL 的不足,引入了 2-3 树,红黑树。下一节详细说明红黑树的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值