MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java HashMap中链表与红黑树的转换原理

2021-10-254.9k 阅读

Java HashMap中链表与红黑树的转换原理

一、HashMap基础概述

在深入探讨链表与红黑树的转换原理之前,我们先来回顾一下HashMap的基本结构。HashMap是Java集合框架中的一员,它以键值对(key - value)的形式存储数据,允许null键和null值。HashMap基于哈希表实现,其内部维护了一个数组,数组的每个元素称为桶(bucket)。

当我们向HashMap中put一个键值对时,首先会根据键的哈希值计算出在数组中的索引位置。如果该位置没有元素,就直接将键值对放入该位置;如果该位置已经有元素,就会发生哈希冲突。为了解决哈希冲突,HashMap采用了链地址法,即每个桶可以是一个链表或红黑树。

二、链表在HashMap中的应用

  1. 链表结构 在HashMap中,当多个键值对计算出的哈希值相同(哈希冲突)时,这些键值对就会以链表的形式存储在同一个桶中。链表节点的结构在JDK源码中定义如下:
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

可以看到,Node类包含了键、值、哈希值以及指向下一个节点的引用。

  1. 链表的插入操作 当发生哈希冲突,需要向链表中插入新节点时,HashMap采用的是尾插法。在JDK 8之前,采用的是头插法,但在JDK 8引入红黑树后改为尾插法,这主要是为了避免在多线程环境下扩容时可能出现的环形链表问题。 以下是简化的向链表插入节点的代码示例:
// 假设存在一个桶,桶中是链表结构
Node<K, V> bucket = hashMap.table[index];
Node<K, V> newNode = new Node<>(hash, key, value, null);
if (bucket == null) {
    hashMap.table[index] = newNode;
} else {
    Node<K, V> current = bucket;
    while (current.next != null) {
        current = current.next;
    }
    current.next = newNode;
}

三、红黑树在HashMap中的应用

  1. 红黑树的特性 红黑树是一种自平衡的二叉查找树,它具有以下特性:
  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 每个叶子节点(NIL节点,空节点)是黑色的。
  • 如果一个节点是红色的,则它的两个子节点都是黑色的。
  • 从任意一个节点到其每个叶子节点的所有路径上包含相同数目的黑色节点。

这些特性保证了红黑树的高度相对平衡,从而使得查找、插入和删除操作的时间复杂度为O(log n),n为树中节点的数量。

  1. 红黑树在HashMap中的节点结构 在HashMap中,红黑树节点继承自链表节点Node,其结构如下:
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;           // 节点颜色

    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
}

可以看到,TreeNode类除了继承Node类的属性外,还增加了父节点、左右子节点、前驱节点以及节点颜色的属性,以满足红黑树的结构和操作需求。

四、链表与红黑树的转换条件

  1. 链表转红黑树 在HashMap中,当链表的长度达到一定阈值时,会将链表转换为红黑树,以提高查找效率。这个阈值在JDK源码中定义为8,即当链表长度大于等于8时,并且当前HashMap的容量大于等于64时,会触发链表到红黑树的转换。
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

上述代码中,首先检查HashMap的容量是否小于64,如果是则进行扩容操作。如果容量满足条件且链表存在,则将链表节点转换为红黑树节点,并调用treeify方法构建红黑树。

  1. 红黑树转链表 当红黑树的节点数量减少到一定程度时,会将红黑树转换回链表。这个阈值在JDK源码中定义为6,即当红黑树节点数量小于等于6时,会触发红黑树到链表的转换。
static final int UNTREEIFY_THRESHOLD = 6;

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

上述代码将红黑树节点转换为链表节点,并重新构建链表结构。

五、链表转红黑树的具体过程

  1. 节点转换 当满足链表转红黑树的条件时,首先会将链表中的节点逐个转换为红黑树节点。如前面提到的replacementTreeNode方法,它会将普通的链表节点Node转换为红黑树节点TreeNode
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}
  1. 构建红黑树 在节点转换完成后,会调用treeify方法构建红黑树。treeify方法首先会将所有节点按顺序插入到红黑树中,然后通过左旋、右旋以及颜色调整等操作来维护红黑树的特性。
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 {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0)? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

上述代码中,通过循环将节点逐个插入到红黑树中,并调用balanceInsertion方法进行红黑树的平衡调整。

六、红黑树转链表的具体过程

  1. 节点转换 当满足红黑树转链表的条件时,首先会将红黑树中的节点逐个转换为链表节点。如前面提到的replacementNode方法,它会将红黑树节点TreeNode转换为普通的链表节点Node
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
    return new Node<>(p.hash, p.key, p.value, next);
}
  1. 构建链表 在节点转换完成后,会将这些节点按顺序连接成链表。如前面的untreeify方法,它通过遍历红黑树节点,将其转换为链表节点并构建链表结构。

七、链表与红黑树转换对性能的影响

  1. 查找性能 链表的查找时间复杂度在最坏情况下为O(n),其中n为链表的长度。而红黑树的查找时间复杂度为O(log n),因此当链表长度较长时,转换为红黑树可以显著提高查找性能。例如,在一个包含大量元素且哈希冲突较多的HashMap中,如果使用链表结构,查找某个元素可能需要遍历整个链表,时间开销较大;而转换为红黑树后,查找操作可以通过树的二分查找特性,快速定位到目标元素,大大减少了查找时间。

  2. 插入和删除性能 链表的插入和删除操作在平均情况下时间复杂度为O(1),但在最坏情况下(如在链表头部或尾部插入/删除)也可能达到O(n)。红黑树的插入和删除操作时间复杂度为O(log n),虽然在平均情况下链表的插入和删除性能可能优于红黑树,但在链表长度较长时,红黑树的稳定性更好。而且,由于红黑树的自平衡特性,其插入和删除操作后的调整过程虽然相对复杂,但能保证树的高度不会过度增长,从而维持较好的性能。

八、实际应用场景与建议

  1. 应用场景 在实际应用中,如果数据量较小且哈希冲突较少,HashMap使用链表结构就可以满足需求,因为链表的插入和删除操作相对简单,性能也较好。例如,在一些轻量级的缓存场景中,数据量有限且访问频率不高,此时使用链表结构的HashMap即可。

而当数据量较大且哈希冲突频繁时,为了保证查找性能,应尽量避免链表过长,及时触发链表到红黑树的转换。例如,在一些大数据处理场景中,如统计海量日志数据中的某些关键字出现次数,使用红黑树结构的HashMap可以提高查找效率,从而加快整个处理过程。

  1. 优化建议 为了充分发挥HashMap的性能,在设计和使用时可以考虑以下几点:
  • 合理设置初始容量:根据预估的数据量设置合适的初始容量,避免频繁扩容。扩容操作不仅会增加时间开销,还可能导致哈希冲突加剧。
  • 选择合适的哈希函数:一个好的哈希函数可以减少哈希冲突的发生,从而减少链表长度,降低转换为红黑树的概率。可以根据数据的特点设计或选择合适的哈希函数。
  • 监控和调整:在应用运行过程中,可以监控HashMap中链表的长度,当发现链表长度过长时,及时调整相关参数或优化数据结构,以保证性能。

通过深入理解HashMap中链表与红黑树的转换原理,并结合实际应用场景进行优化,可以充分发挥HashMap的性能优势,提高程序的运行效率。在实际开发中,我们需要根据具体的业务需求和数据特点,灵活运用HashMap的特性,以达到最佳的性能表现。无论是链表结构还是红黑树结构,都是为了更好地存储和访问数据,只有在合适的场景下选择合适的结构,才能让程序更加高效地运行。同时,对于HashMap内部结构和转换机制的深入理解,也有助于我们在面对复杂的性能问题时,能够迅速定位和解决问题,提升代码的质量和可靠性。

以上就是关于Java HashMap中链表与红黑树转换原理的详细介绍,希望通过本文的讲解,读者能够对这一机制有更深入的理解,并在实际开发中更好地运用HashMap。在日常编程中,不断优化数据结构的使用,是提高程序性能的重要途径之一,而对HashMap这样常用集合类的深入掌握,无疑是其中关键的一环。希望读者可以通过实际的代码实践,进一步加深对链表与红黑树转换原理的理解和应用能力。