图书馆书架管理员的魔法:TreeMap 的奇幻之旅

一、会自动排序的魔法书架

在 Java 王国的图书馆里,有一个神奇的 "魔法书架"(TreeMap),它能自动将书按书名排序。不像普通书架(HashMap)需要贴标签找书,魔法书架会像图书馆管理员一样,把书按字母顺序整理得整整齐齐,不管什么时候找书,都能快速找到。

java

arduino 复制代码
// 创建一个魔法书架,按书名自然顺序排列
TreeMap<String, String> magicShelf = new TreeMap<>();

二、书架的内部结构:红黑树的秘密

2.1 红黑树:会平衡的书架

图书馆的核心:红黑树魔法书架 (Red-Black Tree)​

  • 这不是普通的书架,而是一棵​​会自我平衡的二叉树​​。想象一个倒挂的树:

    • 最上面是​根书架​ (root)。
    • 每个书架(节点)可以放​两本小册子​ 指向左边的子书架 (left) 和右边的子书架 (right),以及一本​记录​ 指向它的父书架 (parent)。
    • 每本书(节点)有一个特殊的​魔法书皮颜色​ (colorREDBLACK)。
  • ​核心魔法规则(红黑树五条咒语)​​:

    1. 每本书非红即黑。
    2. ​根书架的书必须是黑色的!​(馆长规定)
    3. 所有​空的位置(想象成空的子书架位置)都算作是黑色的书​
    4. ​红皮书不能挨着放!​ 一本红皮书 (RED) 两边的子书架上的书必须是黑皮书 (BLACK)。
    5. 从任何一个书架出发,走到它下面任何一个​空位置(黑色空书架)​ ,经过的​黑皮书数量必须相同​!(平衡的关键)

图书馆馆长:比较规则 (ComparatorComparable)​

  • 馆长有两种方式知道怎么排序书:

    • ​自然排序馆长 (Comparable)​ :如果书的名字(键)自带说明书(实现了 Comparable 接口,如 String, Integer),馆长就按说明书排序。TreeMap<String, Book> library = new TreeMap<>();
    • ​自定义馆长 (Comparator)​ :你可以给馆长一本​自定义的排序手册​ (传入 Comparator),馆长就按你的手册排序。TreeMap<Student, Grade> grades = new TreeMap<>(new StudentAgeComparator());
  • ​关键:所有放进图书馆的书,书名(键)必须能用馆长的方式比较大小!​ ​ 否则馆长会懵 (ClassCastException)。

魔法书架的核心是一种 "红黑树" 结构,就像一个会自己保持平衡的书架:

  • 每本书是一个节点,有书名(键)、内容(值)
  • 每个节点有左、右子节点(相邻的书)和父节点(上层书架的书)
  • 节点有红色或黑色标签,确保书架不会一边倒(红黑树性质)

java

swift 复制代码
// 红黑树节点结构
static final class Entry<K,V> {
    K key;        // 书名
    V value;      // 书内容
    Entry<K,V> left;   // 左邻书
    Entry<K,V> right;  // 右邻书
    Entry<K,V> parent; // 上层书
    boolean color = BLACK; // 颜色标签
}

2.2 书架管理员的平衡魔法

当新书插入时,管理员会做 "颜色调整" 和 "书架旋转" 来保持平衡:

  1. 新书默认贴红色标签

  2. 如果父节点是红色,根据叔叔节点颜色调整:

    • 叔叔红色:重新涂色

    • 叔叔黑色:旋转书架(左旋 / 右旋)

java

scss 复制代码
// 插入后平衡调整
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED; // 新书贴红标签
    
    while (x != null && x != root && x.parent.color == RED) {
        // 父节点是左子节点的情况
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> uncle = rightOf(parentOf(parentOf(x)));
            if (colorOf(uncle) == RED) { // 叔叔红色
                setColor(parentOf(x), BLACK);
                setColor(uncle, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else { // 叔叔黑色
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x); // 左旋
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x))); // 右旋
            }
        }
        // 父节点是右子节点的对称情况...
    }
    root.color = BLACK; // 根节点必须黑
}

三、书架管理员的日常操作

3.1 放书到书架(put)

​新书入库 (put(key, value))​​:

  1. ​找位置​ ​:馆长从根书架 (root) 开始,拿着新书名 (key) 和你定的规则 (ComparatorComparable),和书架上的书比大小。小了往左走,大了往右走,直到找到一个空位。

  2. ​放新书​ ​:把新书放到这个空位,​​书皮默认是红色的​​(新书比较显眼)。

  3. ​魔法平衡 (fixAfterInsertion)​ ​:❗️​​关键步骤!​​ 因为放了本红皮书,可能违反规则(红皮书挨着红皮书,或者黑皮书数量不对)。馆长会施展魔法:

    • ​变色​:检查新书的叔叔书架(父书架的兄弟书架)的书皮颜色。
    • ​旋转​ :如果变色解决不了,就进行​左旋 (rotateLeft)​​右旋 (rotateRight)​ ------ 想象把几个书架拎起来转个方向重新挂好。
    • ​目标​ :通过变色+旋转,让所有规则重新满足!​平均复杂度 O(log n)​,因为树是平衡的,高度大概 log2(书的总数)。

管理员放书时,会从书架顶部开始找位置:

  • 书名小的放左边,大的放右边

  • 找到位置后插入,并调整平衡

java

ini 复制代码
// 放书到书架
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) { // 空书架直接放
        root = new Entry<>(key, value, null);
        size = 1;
        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++;
    return null;
}

3.2 从书架找书(get)

​按书名找书 (get(key))​​:

  1. 馆长从根书架开始。

  2. 拿书名 (key) 和当前书架的书名比。

  3. 小了往左走,大了往右走,相等就找到!

  4. ​复杂度也是 O(log n)​​。得益于平衡的魔法书架,找书超快!想象一下在 100 万本书里找书,最多比 20 次 (2^20 ≈ 100 万)。

java

ini 复制代码
// 找书
public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null? null : p.value);
}

final Entry<K,V> getEntry(Object key) {
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null) throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0) p = p.left;
        else if (cmp > 0) p = p.right;
        else return p;
    }
    return null;
}

3.3 从书架取书(remove)

取书 (remove(key))​​:

  1. 先按书名找到那本书 (getEntry)。

  2. 情况有点复杂:

    • 这本书​没孩子​:直接拿走。
    • 这本书​有一个孩子​:让孩子顶替它的位置。
    • 这本书​有两个孩子​ :馆长会找它的​后继书​ (比它大的书里最小的那本,在它​右子树的最左边​ ),把后继书的书名和内容​复制​ 到这本书上,然后​实际删除​的是那本后继书(它最多只有一个孩子,删除变简单)。
  3. ​删除后平衡 (fixAfterDeletion)​ ​:❗️​​更复杂的魔法!​ ​ 删掉一本书(特别是黑皮书)可能破坏平衡规则。馆长又要施展变色+旋转的魔法来修复。​​复杂度也是 O(log

java

ini 复制代码
// 取书
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.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.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 p.parent.right = null;
            p.parent = null;
        }
    }
}

四、书架的特殊功能

4.1 按范围找书(subMap/headMap/tailMap)

图书馆的神奇功能 (TreeMap 特有)​

  • ​自动排序遍历​ ​:用 keySet(), values(), entrySet() 拿到的书/书名/键值对,默认就是​​按书名 (键) 排好序​​的!遍历它们就是按顺序访问。

  • ​找第一本/最后一本书​​:

    • firstEntry():根书架开始,​一直往左走到底​
    • lastEntry():根书架开始,​一直往右走到底​
    • ​O(log n)​​O(1)​ (如果缓存了)。
  • ​找邻居书​​:

    • lowerEntry(key)​严格小于​ key 的最大书。
    • floorEntry(key)​小于等于​ key 的最大书。
    • higherEntry(key)​严格大于​ key 的最小书。
    • ceilingEntry(key)​大于等于​ key 的最小书。
    • ​O(log n)​
  • ​按字母区间找书 (subMap, headMap, tailMap)​​:

    • 想找所有书名在 "Apple" 到 "Orange" 之间的书?SortedMap<String, Book> fruitBooks = library.subMap("Apple", "Orange");
    • 馆长不是真的复制这些书,而是创建一个​虚拟视图​ (SubMap),指向原图书馆里对应区域的书架。高效!​获取视图 O(1),遍历视图 O(m) (m 是区间大小)​

管理员可以按范围找书:

  • subMap:从 A 到 Z 的书

  • headMap:Z 之前的书

  • tailMap:A 之后的书

java

vbnet 复制代码
// 找从"Apple"到"Cherry"的书
NavigableMap<String, String> range = magicShelf.subMap("Apple", true, "Cherry", true);

// 找"Banana"之前的书
NavigableMap<String, String> beforeBanana = magicShelf.headMap("Banana", false);

// 找"Banana"之后的书
NavigableMap<String, String> afterBanana = magicShelf.tailMap("Banana", true);

4.2 找第一本 / 最后一本书

java

javascript 复制代码
// 找第一本书(字典序最小)
Map.Entry<String, String> firstBook = magicShelf.firstEntry();

// 找最后一本书(字典序最大)
Map.Entry<String, String> lastBook = magicShelf.lastEntry();

五、多管理员问题:线程安全

  • ​单线程馆规 (非线程安全)​ ​:同一时间只能一个人操作书架。多人同时放书/下架书,书架 (TreeMap) 可能会乱套!解决方案:

    • ​加锁​ :自己用 synchronized 保护关键操作。
    • ​用线程安全的分馆​ConcurrentSkipListMap (基于跳表)。
  • ​性能权衡​​:

    • ​优点​ :排序、范围查询、邻居查找强大!​增删查改平均 O(log n)​
    • ​缺点​ :比 HashMap (平均 O(1)) 慢。排序和平衡需要额外开销。
  • ​内存开销​ ​:每本书(节点)要存书名、内容、三个指针(左、右、父)和颜色,比 HashMap 的条目开销大。

  • ​键不能为 null ​:馆长不知道怎么比较 null 书名的大小!

  • ​替代品选择​​:

    • 只要排序:TreeMap
    • 不要排序,只要快:HashMap
    • 要插入顺序或访问顺序:LinkedHashMap
    • 高并发 + 排序:ConcurrentSkipListMap

5.1 单管理员 vs 多管理员

魔法书架默认单管理员操作:

  • 多管理员同时操作会混乱(非线程安全)

5.2 线程安全的替代书架

使用 "并发跳表书架"(ConcurrentSkipListMap):

  • 支持多管理员同时操作

  • 内部用跳表结构,适合高并发

java

arduino 复制代码
// 并发安全的书架
import java.util.concurrent.ConcurrentSkipListMap;

ConcurrentSkipListMap<String, String> safeShelf = new ConcurrentSkipListMap<>();
safeShelf.put("Book1", "内容1");
// 多线程同时操作安全

六、魔法书架的应用场景

6.1 图书排行榜

按评分排序的图书榜:

java

less 复制代码
// 图书评分排行榜
TreeMap<Integer, String> bookRanking = new TreeMap<>();
bookRanking.put(95, "Java编程思想");
bookRanking.put(88, "Effective Java");
bookRanking.put(92, "算法导论");

// 按评分升序输出
for (Integer score : bookRanking.keySet()) {
    System.out.println(score + ": " + bookRanking.get(score));
}

// 按评分降序输出
for (Integer score : bookRanking.descendingKeySet()) {
    System.out.println(score + ": " + bookRanking.get(score));
}

6.2 时间区间查询

按时间查询图书借阅记录:

java

ini 复制代码
// 时间-借阅记录映射
TreeMap<Long, String> borrowRecords = new TreeMap<>();
borrowRecords.put(1680000000L, "Alice借走Java书");
borrowRecords.put(1680000100L, "Bob借走算法书");
borrowRecords.put(1680000200L, "Charlie借走数据库书");

// 查询上午8:00到8:05的记录(假设时间戳对应)
long start = 1680000000L;
long end = 1680000300L;
NavigableMap<Long, String> morningRecords = borrowRecords.subMap(start, true, end, true);

6.3 有序字典

实现简单的有序字典:

java

arduino 复制代码
// 英语字典,按单词排序
TreeMap<String, String> englishDict = new TreeMap<>();
englishDict.put("apple", "苹果");
englishDict.put("banana", "香蕉");
englishDict.put("cherry", "樱桃");

// 查找以"a"开头的单词
for (String word : englishDict.tailMap("a").headMap("b")) {
    System.out.println(word + ": " + englishDict.get(word));
}

七、魔法书架 vs 普通书架

特性 TreeMap(魔法书架) HashMap(普通书架)
排序 自动按键排序 无顺序
查找速度 O(log n) 平均 O (1),最坏 O (n)
插入速度 O(log n) 平均 O (1)
适合场景 需要有序查询 快速存取
线程安全

八、书架管理员的小贴士

  1. 自定义排序规则:如果书不是按字母排序,提供比较器:

java

arduino 复制代码
// 按书名长度排序
TreeMap<String, String> shelfByLength = new TreeMap<>(
    (a, b) -> a.length() - b.length()
);
  1. 避免频繁调整:预估书籍数量,减少平衡操作:

java

arduino 复制代码
// 预估有1000本书,创建合适大小的书架
TreeMap<String, String> largeShelf = new TreeMap<>();
  1. 只查首尾书:用 firstEntry/lastEntry 代替全遍历:

java

javascript 复制代码
// 找字典里第一本和最后一本书
Map.Entry<String, String> first = magicShelf.firstEntry();
Map.Entry<String, String> last = magicShelf.lastEntry();

九、总结:魔法书架的魔力

Java TreeMap 就像会自动整理的魔法书架,基于红黑树实现,能按键排序,提供高效的插入、删除和查找操作(O (log n))。它适合需要有序数据的场景,如排行榜、区间查询等。虽然不如 HashMap 快,但有序性让它在特定场景中不可或缺。

理解 TreeMap 的原理,就像掌握了图书馆管理员的魔法,能让你的程序数据井然有序,快速找到需要的信息。下次遇到需要排序的数据时,记得让 TreeMap 来帮忙哦!

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android