📜  Java中 TreeMap 的内部工作(1)

📅  最后修改于: 2023-12-03 15:16:19.304000             🧑  作者: Mango

Java中 TreeMap 的内部工作

Java中的TreeMap是一种基于红黑树数据结构实现的映射表。TreeMap的特点是可以保证键值对的有序性,同时增删改查的时间复杂度都可以做到O(logn)。

红黑树

为了更好地理解TreeMap的内部工作原理,我们需要先来介绍一下红黑树。

红黑树是一种自平衡二叉搜索树,它保证了树的深度最多是其他二叉搜索树的1.5倍。红黑树中每个节点要么是黑色,要么是红色,同时满足以下规则:

  • 根节点是黑色的
  • 所有叶子节点都是黑色的空节点(即NULL节点)
  • 如果一个节点是红色的,则其子节点必须是黑色的
  • 从任意节点到每个叶子的所有路径都包含相同数目的黑色节点。
TreeMap的实现原理

在Java中,TreeMap的底层数据结构就是红黑树。TreeMap的元素被组织成一棵红黑树,每个元素在树中的位置由其键决定。TreeMap中的每个节点都包含一个键和一个值,键用于标识节点的位置,值则存储着与该键相关的数据。

插入操作

当向TreeMap中插入一个键值对时,程序会先将该键值对封装成一个节点并将其插入到红黑树中。插入新节点时,程序会遵循以下规则:

  • 新节点为红色。
  • 从根节点开始,如果当前节点比新节点的键大,则继续遍历当前节点的左子树,否则遍历右子树。
  • 如果遍历到空节点,则把新节点插入到这个“空”节点的位置,并按照红黑树的规则进行修复(即通过变色或者旋转操作,保持红黑树的性质)。

具体的插入操作可以看下面的代码实现:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // 利用二叉搜索树找到新节点的正确位置
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    } else {
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 插入新节点,并通过旋转和重着色操作保证红黑树的性质
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
删除操作

删除操作的实现比插入操作要复杂一些。当从TreeMap中删除某个键值对时,程序会先找到该键对应的节点,然后根据节点的颜色和位置进行删除操作。

删除一个节点时,需要考虑以下几点:

  1. 如果待删除的节点只有一个子节点,可以直接用子节点替换被删除的节点。
  2. 如果待删除节点有两个子节点,就需要找到它的后继节点(即中序遍历中的下一个节点),然后用后继节点替换待删除节点。
  3. 如果被删除的节点是红色的,那么直接删除,不会影响红黑树的性质。
  4. 如果被删除节点是黑色的,则需要进行调整,避免删除这个节点后违背红黑树的性质。

具体的删除操作可以看下面的代码实现:

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // 如果待删除节点有两个子节点,则找到其后继节点
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    }

    // 此时待删除节点最多只有一个子节点
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    // 如果待删除节点有一个子节点
    if (replacement != null) {
        // 把replacement接到p的父节点上
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // 清理p的所有链接
        p.left = p.right = p.parent = null;

        // 如果待删除节点是黑色的,需要进行调整,避免破坏红黑树性质
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {
        // 删除的是根节点
        root = null;
    } else {
        // 待删除节点没有子节点,并且本身是黑色的,需要进行调整。
        if (p.color == BLACK)
            fixAfterDeletion(p);

        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib,  RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else {
            // symmetric
        }
    }

    // 把根节点染黑
    setColor(x, BLACK);
}
总结

通过以上的介绍,我们可以清楚的理解TreeMap底层的实现原理以及数据结构的关系,同时我们也能深度理解红黑树的基本性质和相关操作。这些都是作为Java程序员需要了解的基本知识点,也是日常工作中所需要运用的。