HashMap - 红黑树

原文链接:https://lyldalek.notion.site/0c2022e704f742068d90815d16f8c2c5。
 HashMap的其他知识有很多文章写过,这里就不介绍了,直接开始正题。

前置知识

  • 二叉查找树:二叉查找树( BST)是一棵二叉树,其中每个结点都含有一个 Comparable 的键(以及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。

  • 递归:掌握递归的核心就是相信递归。

红黑树介绍

红黑树是平衡查找树的一种。什么是平衡查找树呢?举个例子:我们从头构造一棵BST,从 1 到 5 按顺序插入节点,得到的BST如下:

对于这样的BST,它的查找性能就退化成与链表一致了。我们希望一个有N个节点的BST,它的查找能保持在 lgN 以内。

平衡查找树就可以达到上面的要求,但是插入的时候要保持完美的平衡代价太高了,我们退而求其次,稍微放松完美平衡的要求,红黑树就是这样的一种树。

红黑树的性质

《算法导论》描述了红黑树的如下几个性质:

  1. Every node is either red or black.

  2. The root is black.

  3. Every leaf (NIL) is black.

  4. If a node is red, then both its children are black.

  5. For each node, all simple paths from the node to descendant leaves contain the

same number of black nodes.

前面3条是固定约束,很好理解,后面2条可以推理出来。

我们画几个图来解释一下:

在红黑树上,一条边上不能出现两个红色节点,一红一黑,两黑都行。

那么,如果一个节点是红色,那么它的孩子节点只能是黑色,由于NULL节点也算黑色,所以它的孩子节点只能是黑色节点或者NULL节点,如下图:

红黑树中,计算树高,红色节点的高度为0,黑色节点的高度为 1。一个典型的红黑树如下:

可以看到,每条到叶子节点的路径上,黑色节点的个数是相等的,都为2,该值叫做黑高。

对于红黑树的每个节点来说,左子树的黑高与右子树的黑高是相等的。这里体现出来的弱平衡就是只要求黑色节点的数量一致。

红黑树的本质

为什么红黑树可以做到 lgN 的查找速度呢?

我们假想一下,红黑树最坏的情况就是红黑间隔,这样树高从 lgN 变成了 2lgN,这是在常数级别之内,并没有退化为 N。

其实红黑树的本质是 2-3-4 树,什么是 2-3-4 树呢?就是树的每个节点的孩子数量可以为 2、3、4。

由这3种节点混合构成的树就叫 2-3-4 树。

我们将红色节点与其父节点看作是一个节点,我们就得到如下变换:

看下面的一个红黑树:

转换为 2-3-4 树就是:

掌握了红黑树的本质之后,我们解析来分析其核心功能,插入与删除。

红黑树的插入

红黑树的插入与二叉查找树的插入差不多,也是分为几种情况讨论。

插入只发生在叶子节点上,且待插入的节点需要被当作是红色节点(不影响子树的黑高)。

叶子节点是黑色

这里又要分两种情况,该叶子节点是红色还是黑色。

如果是黑色,直接插入就好了,该子树的黑高并没有被破坏。

叶子节点是红色

如果是红色,插入节点后破坏了红黑树的性质:

这个时候就需要进行旋转,旋转的规则很简单:

首先,先找3个节点,我们假设待插入节点叫 x,那么叶子节点就叫p,然后找到 p 的 parent ,我们叫它 g。根据红黑树的性质,g 肯定是黑色。

然后,有了3个节点后找到 x p g 3个节点的中序遍历的中间节点,将这个节点往上提即可,如下图:

最后,移动 x p g 各自的孩子节点,保持二叉搜索树的不等式性质:left child < node < right child 。

有了这张图,再也不用搞不清楚左旋和右旋讲的他妈的是什么东西了。

所以,x p g 3个节点的位置有如四种情况,我们先看第一种:

在旋转前,我们有这样的不等式:

A < X < B < P < C < G < D

旋转后,节点 G 占了 C 的位置,那么为了保持不等式的正确,可以将 C 变成 G 的左孩子。

搞定了不等式之后,还需要搞定该子树的黑高问题。

如上图,旋转后,左孩子的黑高比右孩子的黑高少了1,所以需要重新染色

染色需要保持该子树的黑高不变,有两种可选方案,第一种是 p 染成黑色,x 与 g 染成红色。但是这样还需要处理 x 与 g 的孩子的颜色问题。

第二种方案是 p 染成 红色,x 与 g 染成黑色,这样 x 与 g 就不用考虑孩子节点的颜色问题。但是 p 需要考虑它父节点的颜色问题。这样就形成一个向上的递归。

显然第二种方案要简单。

同样的,后面3种情况如下:

插入代码展示

Node insertParent = findInsertLeaf(node);
insertLeaf(node, insertParent);

// check color if need rotate
Node x = node;
Node p = x.getParent();
Node g = p.getParent();

// 根节点是黑色,会终结循环
while (p.isRed() && !isRoot(x)) {
    x = rotate(x, p, g);
    p = x.getParent();
    g = p.getParent();

    // make x color red, left and right child black
    recolor(x, x.getLeftChild(), x.getRightChild());
}

红黑树的删除

红黑树的删除也是要分情况讨论,其中比较核心的就是双黑节点,这个会稍微麻烦点。但是代码写起来还是比较简单的。

对于书中的任意一个节点来说,它分3种情况,一种是没有孩子节点,一种是只有一个孩子节点,一种是有两个孩子节点。

没有孩子节点

我们先看最简单的,也是最复杂的情况。这里还需要再分两种,该节点是黑色还是红色。

如果该节点是红色,直接删除即可。

如果该节点是黑色,删除后,需要将该节点记下,将它看成一个双黑节点,只有这一种情况会出现双黑节点,我们后面再讨论。

有一个孩子节点

根据红黑树的性质,有一个孩子节点,那么该节点必定是一个黑色节点,且孩子节点为红色。

另一种情况是对称的,图就不上了。这里我们只需要删除该黑色节点,让红色节点补位,然后将他的颜色改成黑色即可。

代码如下:

child.setColor(Color.BLACK);
replaceNode(x, child);

有两个孩子节点

这里我们可以想一下 BST 的删除过程。

要删除该节点,首先我们需要找到该节点的后继节点,也就是在树种最接近它且比它大的节点。

这里算法导论里面又分了两种情况,但是本质其实是一样的,只不过代码写起来稍微不一样。

我们将要删除的节点记为x,其后继节点记为 s。

第一种是,x 的右孩子的左孩子为NULL,也就是说 x 的右孩子就是 s:

这种情况下,我们的不等式为 a <  x < s < b。将 x 与 s 先交换位置,不等式就变为了 a <  s < x < b,但是 x 会被删除,所以不等式最终还是真确的。

为了保持子树与其parent 的颜色正确性,所以还需要将 x 与 s 再次交换颜色。

上图最终的结果,想要删除x,其实就回到了我们上面说的只有一个孩子节点的情况了。

代码如下:

replaceNode(x, s);
s.setLeftChild(xLeft);
x.setRightChild(s.getRightChild());
x.setLeftChild(null);
s.setRightChild(x);

continue;

第二种就是上面的一种泛化情形:

x的右孩子不为NULL,这里我们可以先通过递归的方式找到 x 的后继,就是从 x 的右孩子开始,一直往左边走,知道节点的左孩子为空,该节点就是 s,如上图的最左边。

找到之后,交换位置,然后交换颜色。

然后再删除x,这里其实就是又回到了上面讨论的没有孩子节点的情况或者是只有一个孩子节点的情况

代码如下:

Node succussor = s;
while (succussor != null && succussor.getLeftChild() != null) {
    succussor = succussor.getLeftChild();
}
s = succussor;
sRight = s.getRightChild();

Node sParent = s.getParent();

x.setRightChild(sRight);
x.setLeftChild(null);
s.setLeftChild(xLeft);
s.setRightChild(xRight);

replaceNode(x, s);
sParent.setLeftChild(x);

continue;

双黑节点

上面我们讨论到,当一个黑色节点被删除的时候,该节点需要被当作一个双黑节点,目的就是为了将该黑节点转移到其他的节点上去,好让它可以直接删除。

双黑节点未删除时,左右时平衡的,删除后,也要是平衡的,双黑节点通过转移的方法,将待删除节点的黑色转移到别的地方,达到平衡。

为了更好的理解,也可以把双黑节点当作一个类似指针的标识。

调整双黑节点需要4个节点的参与:

  • 该节点 x 的父节点,p

  • p 的另一个孩子节点,也就是 x 的兄弟节点 s

  • s 的孩子节点,离 x 近的,n

  • s 的孩子节点,离 x 远的,f

其中,n f 可以是 NULL。

这4个节点,根据红黑树的性质,按照颜色的组合,一共有 9 种可能性:

PSNFHEX
01000x4
01010x5
01100x6
01110x7
10110xB
11000xC
11010xD
11100xE
11110xF

我们先看3种特殊的情况,0xF,0xB,0x7。

0xF

首先,我们需要明白,双黑节点只是我们设置了一个标识,并不是真正的改动了红黑树的节点内容。

所以上面贴了两张图,下面的一张是解释每个节点的黑高。

双黑节点往 parent 进行转移,为了维持左右平衡,右子树黑高需要减一,将 s 染成红色即可。p 成为了新的双黑节点,然后进行递归。它会变成其他的几种情况之一。

0xB

这里将 p s f 3个节点进行了旋转,然后交换 s 与 p 的颜色。就转变成了其他的情况。

0x7

这种情况比较简单,将双黑节点向 p 转移,将p染成黑色,但是右子树黑高增加了,为了平衡,将 s 染成红色。

其他6种情况

n 和 f 有一个为红色,或者都为红色,就有3种情况。

p 为 红色或者黑色,有两种情况,一共有6种情况。

这6种情况的变换都一样,都是先旋转,再交换颜色,再转移黑色。

状态转移图

上面9种情况的状态转移图如下:

代码如下:

while (db != root()) {
  switch (psfnColor) {
      case 0xf:
          // float up
          db = p;
          s.setColor(Color.RED);
          continue;
      case 0xb:
          rotate(f, s, p);
          s.setColor(Color.BLACK);
          p.setColor(Color.RED);
          continue;
      case 0x7:
          p.setColor(Color.BLACK);
          s.setColor(Color.RED);
          break;
      case 0x4:
      case 0x5:
      case 0xc:
      case 0xd:
          rotate(n, s, p);
          p.setColor(Color.BLACK);
          s.setColor(Color.BLACK);
          n.setColor(pColor);
          break;
      case 0x6:
      case 0xe:
          rotate(f, s, p);
          p.setColor(Color.BLACK);
          s.setColor(pColor);
          f.setColor(Color.BLACK);
          break;

      default:
          Check.shouldNotReachHere();
          break;
  }
  break;
}

手写红黑树

有了上面的基础,自己从0到1写一个红黑树也就不是难事了,赶紧动手巩固一下吧,毕竟只有写出代码才能真正算得上理解了。

上面简单的贴了一些代码片段,是为了说明,红黑树的代码没有想象中的那么麻烦。虽然我们讨论的情况确实很多,但是代码写起来要简单很多。

我使用 java 实现了一个自己的红黑树,核心代码在 300 行左右。项目工程已提交到了 github,欢迎多多 star,有任何问题可以提 issue。项目里面我还提供了一些测试用例,可以便于理解。

https://github.com/aprz512/red-black-tree

项目截图:

测试红黑树

代码实现完成后,我们还需要对代码进行测试。为了方便测试各种case,我们需要能够随意构造红黑树,所以使用字符串的方式来构造。

如下:

对于这样的两个字符串,我们希望它能构造出如下的红黑树:

对于节点 key 的解析,我们使用 stack 来辅助(下面的代码经过了简化,主打一个核心思路):

private Node buildTree(String treeString) {

  Stack<Node> stack = new Stack<>();

  while (i < treeString.length()) {
      ch = treeString.charAt(i);
      if (ch == '(') {

          Node node = new Node();
          stack.push(node);

          continue;
      } else if (ch == ')') {

          Node top = stack.pop();
          Node parent = stack.peek();

          parent.setChild(top);

      } else if (ch == '#') {
          parent.setChild(top);

      } else {
          i++;
          continue;
      }
  }

  return null;
}

遇到左括号,就push一个节点到 stack 中,遇到右节点,就pop出来,将他设置到 parent 的 leftChild 或者 rightChild 上。遇到叶子节点也是如此处理。

对于颜色的解析就更简单了,直接使用递归:

private int colorTreeDfs(Node tree, String colorString, int index) {
    if (tree == null) {
        Check.checkEquals('#', colorString.charAt(index));
        return index;
    }

    if (colorString.charAt(index) == 'R') {
        tree.setColor(Color.RED);
    } else if (colorString.charAt(index) == 'B') {
        tree.setColor(Color.BLACK);
    }

    index = colorTreeDfs(tree.getLeftChild(), colorString, index + 1);
    // index 传递给参数,累加
    index = colorTreeDfs(tree.getRightChild(), colorString, index + 1);

    return index;
}

就是按照前序遍历的方式染色。

红黑树构造好了之后,我们需要校验三个方面的东西:

第一就是每个节点的左右子树的黑高是否相等,

第二个就是不等式性质,left < key < right。我自己写的代码,是采用的 left < key ≤ right 的性质。由于节点会旋转,所以最终可能会出现 left ≤ key ≤ right 的情况。

第三个就是颜色约束,红色节点的child必须是黑色的。

检验如下:

private static int verifyBlackHeight(Node node, int blackHeight) {
    if (node == null) {
        return 0;
    }

    Node left = node.getLeftChild();
    Node right = node.getRightChild();

    int leftBlackHeight = verifyBlackHeight(left, blackHeight);
    int rightBlackHeight = verifyBlackHeight(right, blackHeight);

    Check.checkEquals(leftBlackHeight, rightBlackHeight);

    if (node.isRed()) {
        if (left != null) {
            Check.checkEquals(left.getColor(), Color.BLACK);
        }
        if (right != null) {
            Check.checkEquals(right.getColor(), Color.BLACK);
        }
        blackHeight = leftBlackHeight;
    } else if (node.isBlack()) {
        blackHeight = leftBlackHeight + 1;
    } else {
        Check.shouldNotReachHere("color is not red or black, color = " + node.getColor());
    }

    // 旋转之后,left child 可能与 node 相等
    if (left != null) {
        Check.checkLessEquals(left.getKey(), node.getKey());
    }

    if (right != null) {
        Check.checkLessEquals(node.getKey(), right.getKey());
    }

    return blackHeight;
}

总结

恭喜你看到这里,这说明你已经对树这种结构的理解已经到了登堂入室的境界了。

掌握红黑树是非常有必要的,因为它的使用很广泛,像虚拟内存的管理,数据库等地方都有使用到。

<think>好的,我需要回答用户关于HashMap红黑树优化的原理及实现的问题。首先,我得回顾一下用户提供的引用内容,看看里面有没有相关的信息。引用[1]提到,Java 8对红黑树的插入、删除操作进行了平衡优化,减少了时间复杂度。引用[3]指出HashMap的结构是数组加链表和红黑树,数组长度固定。 接下来,我需要组织这些信息,按照用户的要求分点回答。首先介绍红黑树的基本结构,然后说明在HashMap中的应用场景,比如当链表长度超过8时转换为红黑树。然后详细讲解优化原理,包括保持平衡、操作优化、退化和扩容机制。最后用代码示例展示树节点结构,并列出相关问题。 要注意使用正确的LaTeX格式,比如独立公式用$$...$$,行内用$...$。比如提到时间复杂度时,用$O(\log n)$这样的格式。同时,引用标识要放在相关内容的末尾,自然添加。 还要检查是否有遗漏的优化点,比如红黑树节点在扩容时的拆分优化,这在引用[2]和[3]中可能涉及。需要确保每个部分都有对应的引用支持,比如平衡优化引用[1],退化和扩容引用[3]。 最后生成相关问题,要围绕红黑树的优化、转换阈值选择、与链表的比较、扩容影响等,确保问题相关且有深度。</think>### HashMap红黑树的优化原理及实现 HashMapJava 8中引入了红黑树优化,主要解决哈希冲突时链表过长导致的查询效率下降问题。以下是其核心优化原理及实现细节: --- #### 1. **红黑树的基本结构** 红黑树是一种自平衡二叉查找树,通过以下规则保持平衡[^3]: - 每个节点为红色或黑色 - 根节点和叶子节点(NIL节点)为黑色 - 红色节点的子节点必须为黑色 - 从根节点到任意叶子节点的路径包含相同数量的黑色节点 在HashMap中,当链表长度超过阈值(默认8)时,链表会转换为红黑树,时间复杂度从$O(n)$优化到$O(\log n)$。 --- #### 2. **红黑树优化的核心原理** - **平衡性优化** 插入或删除节点时,红黑树通过旋转(左旋/右旋)和颜色调整维持平衡,确保树的高度近似$\log_2 n$,避免退化为链表[^1]。 - **操作效率优化** 在Java 8中,红黑树的插入和删除操作减少了不必要的平衡检查。例如,插入时仅需最多两次旋转即可恢复平衡[^1]。 - **退化控制** 当红黑树节点数小于阈值(默认6)时,会退化为链表,节省内存空间。 --- #### 3. **实现细节** ##### (1) 节点结构 红黑树节点继承自链表节点,增加了父节点和子节点引用: ```java static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 父节点 TreeNode<K,V> left; // 左子节点 TreeNode<K,V> right; // 右子节点 TreeNode<K,V> prev; // 前驱节点(用于快速退化为链表) boolean red; // 颜色标识 } ``` ##### (2) 扩容时的优化 HashMap扩容时,红黑树会被拆分到新桶中。Java 8优化了拆分逻辑,通过高位哈希值判断节点归属,减少树结构的重建成本[^2]。 --- #### 4. **性能对比** | 结构 | 插入复杂度 | 查询复杂度 | 适用场景 | |--------|------------|------------|------------------| | 链表 | $O(1)$ | $O(n)$ | 短链表(长度≤8) | | 红黑树 | $O(\log n)$| $O(\log n)$| 长链表(长度>8) | --- #### 5. **代码示例(红黑树插入逻辑片段)** ```java final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; // 根节点为黑色 root = x; } else { // 递归插入并平衡树 insertBalance(root, x); } } moveRootToFront(tab, root); // 确保根节点在桶首 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二手的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值