一、会自动排序的魔法书架
在 Java 王国的图书馆里,有一个神奇的 "魔法书架"(TreeMap),它能自动将书按书名排序。不像普通书架(HashMap)需要贴标签找书,魔法书架会像图书馆管理员一样,把书按字母顺序整理得整整齐齐,不管什么时候找书,都能快速找到。
java
arduino
// 创建一个魔法书架,按书名自然顺序排列
TreeMap<String, String> magicShelf = new TreeMap<>();
二、书架的内部结构:红黑树的秘密
2.1 红黑树:会平衡的书架
图书馆的核心:红黑树魔法书架 (Red-Black Tree
)
-
这不是普通的书架,而是一棵会自我平衡的二叉树。想象一个倒挂的树:
- 最上面是根书架 (
root
)。 - 每个书架(节点)可以放两本小册子 指向左边的子书架 (
left
) 和右边的子书架 (right
),以及一本记录 指向它的父书架 (parent
)。 - 每本书(节点)有一个特殊的魔法书皮颜色 (
color
:RED
或BLACK
)。
- 最上面是根书架 (
-
核心魔法规则(红黑树五条咒语):
- 每本书非红即黑。
- 根书架的书必须是黑色的!(馆长规定)
- 所有空的位置(想象成空的子书架位置)都算作是黑色的书。
- 红皮书不能挨着放! 一本红皮书 (
RED
) 两边的子书架上的书必须是黑皮书 (BLACK
)。 - 从任何一个书架出发,走到它下面任何一个空位置(黑色空书架) ,经过的黑皮书数量必须相同!(平衡的关键)
图书馆馆长:比较规则 (Comparator
或 Comparable
)
-
馆长有两种方式知道怎么排序书:
- 自然排序馆长 (
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 书架管理员的平衡魔法
当新书插入时,管理员会做 "颜色调整" 和 "书架旋转" 来保持平衡:
-
新书默认贴红色标签
-
如果父节点是红色,根据叔叔节点颜色调整:
-
叔叔红色:重新涂色
-
叔叔黑色:旋转书架(左旋 / 右旋)
-
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)
):
-
找位置 :馆长从根书架 (
root
) 开始,拿着新书名 (key
) 和你定的规则 (Comparator
或Comparable
),和书架上的书比大小。小了往左走,大了往右走,直到找到一个空位。 -
放新书 :把新书放到这个空位,书皮默认是红色的(新书比较显眼)。
-
魔法平衡 (
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)
):
-
馆长从根书架开始。
-
拿书名 (
key
) 和当前书架的书名比。 -
小了往左走,大了往右走,相等就找到!
-
复杂度也是 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)
):
-
先按书名找到那本书 (
getEntry
)。 -
情况有点复杂:
- 这本书没孩子:直接拿走。
- 这本书有一个孩子:让孩子顶替它的位置。
- 这本书有两个孩子 :馆长会找它的后继书 (比它大的书里最小的那本,在它右子树的最左边 ),把后继书的书名和内容复制 到这本书上,然后实际删除的是那本后继书(它最多只有一个孩子,删除变简单)。
-
删除后平衡 (
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 是区间大小)。
- 想找所有书名在 "Apple" 到 "Orange" 之间的书?
管理员可以按范围找书:
-
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) |
适合场景 | 需要有序查询 | 快速存取 |
线程安全 | 否 | 否 |
八、书架管理员的小贴士
- 自定义排序规则:如果书不是按字母排序,提供比较器:
java
arduino
// 按书名长度排序
TreeMap<String, String> shelfByLength = new TreeMap<>(
(a, b) -> a.length() - b.length()
);
- 避免频繁调整:预估书籍数量,减少平衡操作:
java
arduino
// 预估有1000本书,创建合适大小的书架
TreeMap<String, String> largeShelf = new TreeMap<>();
- 只查首尾书:用 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 来帮忙哦!