面试爽文 :开局一张图,花十分钟了解 HashMap 的树化逻辑

👈👈👈 欢迎点赞收藏关注哟

首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164...
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca...

一. 前言

闲来无事,对 HashMap 其中比较重要的节点做了一下深入,然后尝试用最通俗易懂的说法输出出来。

老规矩 ,给节省时间的小伙伴上菜 :

二. HashMap 的树化

初级入门

之前面试别人的时候,就经常问这问题 ,问的比较多的一个点就是 HashMap 的树化 ,这个问题的标准答案也很简单 :

  • Java 1.8 之前 , HashMap 通过数组 + 链表的数据结构来处理哈希冲突。当多个键具有同一个哈希码时,他们会存储在一个哈希桶中,形成一个链表结构
  • Java 1.8 之后 , 也是数组 + 链表,但是当链表的长度超过一定阈值后,会转变成红黑树结构,这个行为就叫树化

会了这两点,一般就认为对HashMap有一定了解了,初级就算过了,但是如果是中级就会稍微问多点

中级浅谈 :

  • 如何实现的链表?

每一个写入 HashMap 的对象会被封装到 Node 对象中,这个对象中有四个属性 ,其中一个属性同样是一个 Node 对象 ,用来指向链表中下一个 Node

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    // next 即为链表中下一个 Node
    Node<K,V> next;
}
  • 如何实现的红黑树?

基于 TreeNode 对象实现, 该对象包含了 TreeNode-Left ,TreeNode-Right , TreeNode-Parent对象,用于实现树形结构

java 复制代码
// 实际每个 TreeNode 包含了数据信息
TreeNode extends Node {

    // 父节点
    TreeNode parent;
    // 左子节点
    TreeNode left;
    // 右子节点
    TreeNode right;
    // 上一节点用于生成双向链表,便于树形操作
    TreeNode prev;
    boolean red;
}
  • 为什么会有 Hash 冲突?

因为 HashMap 的底层是数组,而一个元素放在哪个数组格里面是通过 Hash & 数组长度来实现的 ,这就意味着哪怕 Hash 算法很厉害 ,但是取模下来得到同一个下标的可能性也非常大

  • 为什么选择红黑树?

红黑树能保证两边元素的平衡 , 基于这个特性 ,红黑树能保证查询的效率,无论元素多少,都能让查询一直在对数级别。

详细的红黑树原理可以看看下面这篇文章 :👇👇👇

算法 : 这可能是你看过最简洁的红黑树笔记

三. 高级原理

当然,像技术要求比较高的兄弟,可能还会继续问下去,这个时候就需要把前后的所有概念串起来了,不多说,直接上代码 :

3.1 手写逻辑

HashMap 的逻辑线性表述不是很清晰,这里手写了一段逻辑,力求逻辑更加清晰 :

  • 2个存储对象 :在树化和解除树化之间互相转换
    • Node 用来存储Map中的数据, 通过 next 引用指向下一个节点形成链表
    • TreeNode 是 Node 的扩展 , 用来形成树结构
  • 4个流程 :
    • 如果Hash获得的数组下标对应的位点没有对象,则直接放入Node对象
    • 如果存在对象,但是小于树化阈值,则 放入链表末尾
    • 如果存在对象,且大于 树化阈值(8) ,则 先转换成树链表 ,在对树链表进行树化
    • 如果插入或者删除 触发解除阈值(6),则解除树化

两个存储对象 :

java 复制代码
/**
 * 基础节点
 */
public static class Node {
    protected int hash;
    protected String key;
    protected String value;
    // 基于 next 指向下一个节点从而形成一个链表结构
    protected Node next;
}

/**
 * 树节点 ,实际上继承了 Node ,意味着每个 TreeNode 实际也包含数据对象
 */
public static class TreeNode extends Node {

    // 父节点
    TreeNode parent;
    // 左子节点
    TreeNode left;
    // 右子节点
    TreeNode right;
    // 上一节点用于生成双向链表,便于树形操作
    TreeNode prev;
    boolean red;

    public TreeNode(Node node) {
        this.key = node.key;
        this.value = node.value;
        this.next = node.next;
        this.hash = node.hash;
    }
}

核心流程操作 :

java 复制代码
public static void main(String[] args) {

    // 插入一个数据
    HashMapDemo mapDemo = new HashMapDemo();
    mapDemo.putVal("test", "1");

}

/**
 * HashMap 底层数组结构 , 每一个 Node 对应一个实际的对象
 */
Node[] table = null;
/**
 * 树化条件,超过该条件则进行红黑树转化
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * 解除树化条件,小于该条件则解除树化
 */
static final int UNTREEIFY_THRESHOLD = 6;


/**
 * 红黑树的操作一般都是基于插入操作来实现的
 */
public void putVal(String key, String value) {

    int hash = getHash(key);
    
    // S0 : 先获取 Hash 
    Node newNode = new Node(getHash(key), key, value, null);

    // S1 : 判断桶中是否存在元素, 如果不存在元素 ,直接添加
    if (table[hash] == null) {
        table[hash] = newNode;
        return;
    }

    // S2 : 先添加到链表中,判断添加后的长度是否超过最大长度
    Node currentNode = table[hash];
    boolean isAdd = addNode(currentNode, newNode);

    // S3 : 超过最大长度,进行树化操作
    if (isAdd) {
        treeifyBin(currentNode);
    }

}


// 模拟Hash冲突 , 只有 Hash 冲突时才会触发
private static int hash(String key) {
    return 10;
}

private static boolean addNode(Node head, Node newNode) {
    int length = 0;
    Node lastNode = head;
    // S2-1 :循环整个链表, 把数据加入链表最后一位
    for (; lastNode != null; lastNode = lastNode.next) {
        length++;
    }
    lastNode.next = newNode;

    // S2-2 :假代码,模拟当前链表长度超过树化阈值
    return length > TREEIFY_THRESHOLD;
}

private static void treeifyBin(Node head) {
    TreeNode treeNode = null;
    
    // S3-1 : 执行树华,先将节点转换为树节点 , 本质上形成的还是一个树形链表
    for (Node currentNode = head; currentNode != null; currentNode = currentNode.next) {
        if (treeNode == null) {
            treeNode = new TreeNode(currentNode);
        } else {
            treeNode.next = new TreeNode(currentNode);
        }
    }
    
    // S3-2 : 执行树华逻辑
    treeify(treeNode);
}

private static void treeify(TreeNode treeNode) {
    // S3-3 : 树形转换,此处略过
}

3.2 高级问题集锦

什么时候红黑树会解除树化 ?

  • 桶中的元素数量下降到一定阈值以下(UNTREEIFY_THRESHOLD=6)时,如果再触发删除节点时,则会解除树化。
  • 当整体容量缩容的时候 ,Hash 取模会发生变化,(resize)此时也有可能触发解除树华

3.3 红黑树转换精华

红黑树的代码很有意思,但是全记住不容易,掌握其中最重要的几点就足够了 :

树链表转红黑树 : treeify(Node<K,V>[] tab)

  • S1 : 先预设 Root 节点为 null ,然后开始遍历所有的节点
  • S2 : 将每个树节点从树链表中抽离出来,为其初始化左右子节点(null)
  • S3 : 如果是第一个节点,则直接设置为root ,如果不是第一个节点,则基于大小比较放在对应的节点侧
  • S4 : 调用 balanceInsertion 方法对树进行结构调整
  • S5 : 返回调整后的root节点,移动到数组中,作为入口的首位

平衡的原理 :

平衡的这一段代码是整个红黑树的精华,单独拉出来写一篇文章都足够了,篇幅有限,只说几个关键点 :

  • S1 : 先把插入节点定义为红色 ,然后按照大小插入指定位置
  • S2 : 从当前节点开始回到根节点 , 在这个过程中进行节点的位移和染色 ,通常包括2种位置 ,3种情况
    • 2 种位置 : 当前节点的父节点是左子节点还是右子节点 (用于确定是左旋还是右旋)
    • 3 种情况 : 树节点 (当前父节点的兄弟节点)的颜色和位置
      • 叔节点是红色 (修改颜色,上移当前节点)
      • 叔节点是黑色,且 x 是右子节点 (进行旋转)
      • 叔节点是黑色,且 x 是左子节点 (修改颜色,进行旋转)

如果知道了这个流程,和具体旋转的方式,那么代码就很容易看清楚了 ,无非就像下面的这种方式

再细节就不深入了,等后面出针对性的文章吧,这一块不自己写个始终差点意思。

java 复制代码
x.parent.red = false; // 将父节点设为黑色 
y.red = false; // 将叔节点设为黑色 
x.parent.parent.red = true; // 将祖父节点设为红色 
x = x.parent.parent; // 将 x 上移到祖父节点

rotateRight(root, x.parent.parent); // 右旋转
rotateLeft(root, x.parent.parent); // 左旋转

解除树化 :

原因 :节省空间,上面也能看的出来 ,TreeNode 包含了 pre ,next ,parent 等信息,会占用大量内存

解除树化主要在 resize 和 removeNode 逻辑中进行 ,resize 主要为了控制容量,通常在以下场景中可能会触发 Resize 操作 :

  • 插入元素时 :在插入数据的时候,如果触发了阈值, 则会触发resize ,包括不限定于插入,合并,等等
  • 直接手动调用或者 Map 初始化时

在 untreeify 中进行解除树化操作,处理逻辑很简单 :

  • S1 : for 循环节点的 next 属性,把属性转换为 Node 对象
  • S2 : 把 Node 按照 Next 属性连接的方式生成链表

PS :哪怕转换成了 TreeNode ,但是由于TreeNode 继承了 Node ,所以 next 属性是保留的,也可以通过 next 循环所有的变量

总结

看完这篇还能被问住,那不怪你,那是面试官的问题。

相关推荐
码出极致9 分钟前
Redisson分布式缓存与数据一致性保障
后端
用户7903490337112 分钟前
springboot集成redisson实现redis分布式锁
后端
陈随易16 分钟前
程序员的新玩具,MoonBit(月兔)编程语言科普
前端·后端·程序员
码出极致22 分钟前
Redisson秒杀系统中的分布式锁应用
后端
Shimiy29 分钟前
第四章 数组
java
间彧29 分钟前
什么是JVM Young GC
java·jvm
xiaok30 分钟前
@Param注解的作用
java·后端
脑袋大大的33 分钟前
钉钉企业应用开发技巧:查询表单实例数据新版SDK指南
java·钉钉·企业应用开发
爱学习的茄子33 分钟前
JavaScript闭包实战:防抖的优雅实现
前端·javascript·面试
ZsTs11935 分钟前
面试被 JS 基础问懵?11 个核心模块笔记帮你吃透(含考点陷阱)
面试