集合框架背后的数据结构,是 Java 程序员的“内功心法”。
O(1)
。假设 int 类型占 4 字节,数组起始地址为 1000
内存地址: 1000 1004 1008 1012 1016
+--------+--------+--------+--------+--------+
数组 arr: | 10 | 20 | 30 | 40 | 50 |
+--------+--------+--------+--------+--------+
索引: 0 1 2 3 4
寻址公式:元素地址 = 基地址 + (索引 × 元素大小)
例如:arr[2] 的地址 = 1000 + (2 × 4) = 1008
插入(在索引 2 处插入 25):
步骤 1:从索引 2 开始,所有元素后移
内存地址: 1000 1004 1008 1012 1016 1020
+--------+--------+--------+--------+--------+--------+
| 10 | 20 | 30 | 40 | 50 | ? |
+--------+--------+--------+--------+--------+--------+
| 10 | 20 | ? | 30 | 40 | 50 | ← 移动后
步骤 2:在索引 2 处放入 25
+--------+--------+--------+--------+--------+--------+
| 10 | 20 | 25 | 30 | 40 | 50 |
+--------+--------+--------+--------+--------+--------+
✅ 时间复杂度:
O(n)
,因为要移动n-i
个元素。
ArrayList
中的“动态扩容”ArrayList
不是固定长度,它是如何做到的?
// 当容量不够时,执行扩容
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 (即 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 创建新数组,复制数据
elementData = Arrays.copyOf(elementData, newCapacity);
}
内存变化:
扩容前:[1][2][3][4] (容量=4, size=4)
扩容后:[1][2][3][4][ ][ ] (容量=6, size=4)
✅ 代价:扩容是
O(n)
操作,所以建议初始化时指定合理容量。
假设节点 Node 定义:
class Node {
int data;
Node next; // 指向下一个节点的引用
}
内存中节点分布(地址随机):
地址 2000: +--------+--------+
| data=10|next=3000| <- head 指向这里
+--------+--------+
地址 3000: +--------+--------+
| data=20|next=1500|
+--------+--------+
地址 1500: +--------+--------+
| data=30|next=null |
+--------+--------+
逻辑结构:
head
|
v
+--------+ +--------+ +--------+
| 10 | --o--->| 20 | --o--->| 30 |null|
+--------+ +--------+ +--------+
2000 3000 1500 ← 物理地址
在头部插入 5:
步骤 1:创建新节点
地址 4000: +--------+--------+
| data=5 |next=? |
步骤 2:新节点的 next 指向原头节点
+--------+--------+
| data=5 |next=2000|
步骤 3:head 指向新节点
head
|
v
+--------+ +--------+ +--------+ +--------+
| 5 | --o--->| 10 | --o--->| 20 | --o--->| 30 |null|
+--------+ +--------+ +--------+ +--------+
4000 2000 3000 1500
✅ 时间复杂度:
O(1)
,只修改了两个指针(newNode.next
和head
)。
LinkedList
使用)每个节点多一个 prev
指针,指向前一个节点。
地址 2000: +--------+--------+--------+
|prev=null| data=10|next=3000| <- head
+--------+--------+--------+
地址 3000: +--------+--------+--------+
|prev=2000| data=20|next=1500|
+--------+--------+--------+
地址 1500: +--------+--------+--------+
|prev=3000| data=30|next=null| <- tail
+--------+--------+--------+
逻辑结构:
head tail
| |
v v
+--------+ +--------+ +--------+
|null|10|<--->|10|20|<--->|20|30|null|
+--------+ +--------+ +--------+
2000 3000 1500
✅ 优势:可以从
tail
反向遍历,removeLast()
也是O(1)
。
假设哈希表容量为 8,哈希函数 h(k) = k % 8
桶数组 (table) - 索引 0 到 7
+-----------------+
| 0 | -> |(16, "A")| -> |(24, "D")| ← 链表(冲突)
+-----------------+
| 1 | -> |(1, "B") | |
+-----------------+
| 2 | -> null | ← 空桶
+-----------------+
| 3 | -> |(3, "C") | -> |(11, "E")| -> |(19, "F")| ← 链表
+-----------------+
| 4 | -> |(4, "G") | |
+-----------------+
| 5 | -> null |
+-----------------+
| 6 | -> |(6, "H") | |
+-----------------+
| 7 | -> null |
+-----------------+
键值对插入过程:
- put(16, "A"): h(16)=16%8=0 → 放入桶 0
- put(24, "D"): h(24)=24%8=0 → 冲突!用链表串在 "A" 后面
- put(3, "C"): h(3)=3%8=3 → 放入桶 3
- put(11, "E"): h(11)=11%8=3 → 冲突!串在 "C" 后面
问题:如果链表太长(比如 100 个元素都在同一个桶),查找就退化成 O(n)
!
解决方案(JDK 8+ HashMap
):
O(log n)
,性能大幅提升。桶 3 原来是链表:
+-----------------+
| 3 | -> |(3,"C")| -> |(11,"E")| -> |(19,"F")|
+-----------------+
满足条件后,转换为红黑树:
+-----------------+
| 3 | -> (11,"E")<B> |
| | / \ |
| | (3,"C")<R> (19,"F")<R> ← 红黑树结构
+-----------------+
✅ 临界值 8 的选择:基于泊松分布,链表长度达到 8 的概率极低,说明哈希函数可能不佳或数据量大,此时转树收益大于成本。
普通二叉搜索树(BST)在有序插入时会退化成链表:
插入 1,2,3,4,5:
1
\
2
\
3
\
4
\
5 ← 查找时间 O(n),退化!
红黑树通过着色规则和旋转操作,保证树的近似平衡,查找、插入、删除均为 O(log n)
。
HashMap
为例)场景:向 HashMap
的桶中插入键 19
,导致链表转树或树结构调整。
初始状态(简化): 假设桶 3 的树结构如下(B=黑, R=红):
(11,"E")<B>
/ \
(3,"C")<R> (19,"F")<R> ← 违反规则4!两个连续红节点
修复:左旋转(Left Rotate)
步骤:
19
的左子树(假设为空)接到 11
的右子树。11
作为 19
的左子节点。旋转后:
(19,"F")<B> ← 颜色调整,根变黑
/
(11,"E")<R>
/
(3,"C")<R>
✅ 结果:恢复了红黑树性质,树更平衡。
💡
HashMap
中的红黑树:是TreeNode
类,继承自LinkedHashMap.Entry
,并包含left
,right
,parent
,red
等字段。
集合 | 核心数据结构 | 关键机制 |
---|---|---|
ArrayList | 动态数组 | 扩容(1.5倍)、Arrays.copyOf 复制 |
LinkedList | 双向链表 | first 和 last 指针,头尾操作 O(1) |
HashSet | 哈希表(HashMap 的 key 集合) | 哈希函数、拉链法、equals() 判断重复 |
HashMap | 数组 + (链表/红黑树) | 哈希函数 hash(key)、负载因子 0.75、链表长度 > 8 且桶数 ≥ 64 时转树 |
TreeMap | 红黑树 | 基于 Comparator 或自然序排序,插入/删除后通过旋转和变色保持平衡 |
PriorityQueue | 堆(完全二叉树,数组实现) | 父节点索引 i,左孩子 2i+1,右孩子 2i+2,通过“上浮/下沉”维持堆序 |
数据结构不是“纸上谈兵”,而是“内存中的舞蹈”。 每一个
new
出来的对象,每一个指针的跳转,每一次哈希的计算, 都在内存中真实地发生着。 只有当你能“看见”数组的连续、链表的指针、哈希的冲突、红黑树的旋转, 你才算真正掌握了集合框架的“灵魂”。 这篇深度解析,希望能成为你通往 Java 高手之路的坚实基石!
希望这次的深度图解和内存级剖析,能让你对数据结构有更深刻、更“可视化”的理解!