图书馆书架管理员的魔法: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 来帮忙哦!

相关推荐
whysqwhw42 分钟前
Egloo 中Kotlin 多平台中的 expect/actual
android
用户2018792831671 小时前
《Android 城堡防御战:ProGuard 骑士的代码混淆魔法》
android
用户2018792831672 小时前
🔐 加密特工行动:Android 中的 AES 与 RSA 秘密行动指南
android
liang_jy3 小时前
Android AIDL 原理
android·面试·源码
用户2018792831673 小时前
Android开发的"魔杖"之ADB命令
android
_荒3 小时前
uniapp AI流式问答对话,问答内容支持图片和视频,支持app和H5
android·前端·vue.js
冰糖葫芦三剑客3 小时前
Android录屏截屏事件监听
android
东风西巷3 小时前
LSPatch:免Root Xposed框架,解锁无限可能
android·生活·软件需求
androidwork5 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·开发语言·kotlin