一、数组(Array)
1. 特点
- 内存连续:元素在内存中连续存储。
- 固定长度:初始化时必须指定大小,且无法动态扩容。
- 随机访问:通过索引直接访问元素,时间复杂度为 O(1)。
- 类型统一:所有元素类型相同(基本类型或对象)。
2. 代码示例
// 初始化数组
int[] array = new int; // 固定长度
String[] names = {"Alice", "Bob"};
3. 优点
- 高效随机访问:通过索引直接定位元素。
- 内存紧凑:无额外空间开销(仅存储元素本身)。
- 缓存友好:连续内存利用 CPU 缓存预加载机制,访问效率高。
4. 缺点
- 固定大小:无法动态扩容,需手动复制数据到新数组。
- 插入/删除低效:在中间插入或删除元素时,需移动后续元素,时间复杂度 O(n)。
- 内存浪费:若预先分配过大空间,可能浪费内存;若分配不足,需频繁扩容。
二、链表(LinkedList)
1. 特点
- 内存不连续:元素通过节点(Node)的指针连接,存储分散。
- 动态长度:无需预先分配内存,可动态增删节点。
- 顺序访问:查找元素需从头节点遍历,时间复杂度 O(n)。
- 支持复杂结构:可实现单链表、双向链表、循环链表等。
2. 代码示例(单链表节点)
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
// 创建链表
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
3. 优点
- 动态扩容:无需预先分配内存,适合数据规模不确定的场景。
- 高效插入/删除:在已知节点位置时,插入或删除只需修改指针,时间复杂度 O(1)。
- 灵活结构:可轻松实现栈、队列、图等复杂数据结构。
4. 缺点
- 随机访问低效:必须从头节点遍历,时间复杂度 O(n)。
- 内存开销大:每个节点需存储数据和指针,空间利用率低。
- 缓存不友好:内存分散导致 CPU 缓存命中率低。
三、数组与链表的对比
特性 | 数组 | 链表 |
---|---|---|
内存分配 | 连续内存 | 非连续内存(节点分散存储) |
长度限制 | 固定长度 | 动态扩展 |
访问方式 | 随机访问(O(1)) | 顺序访问(O(n)) |
插入/删除效率 | 低效(需移动元素,O(n)) | 高效(修改指针,O(1)) |
内存开销 | 仅存储数据 | 每个节点额外存储指针 |
适用场景 | 查询多、增删少 | 增删多、查询少 |
缓存友好性 | 高 | 低 |
四、适用场景
1. 优先选择数组的场景
- 数据规模已知且变化少(如存储月份名称)。
- 需要频繁按索引访问元素(如矩阵运算)。
- 内存敏感型应用(如嵌入式系统)。
2. 优先选择链表的场景
- 数据规模动态变化(如实时日志记录)。
- 需要频繁在头部或中间插入/删除(如实现队列、LRU 缓存)。
- 无法预估最大容量(如文件系统的块管理)。
五、Java 集合类的实现
1. 基于数组的结构
- ArrayList:动态数组,自动扩容(底层是
Object[]
)。 - Vector:线程安全的动态数组(已过时,建议用
CopyOnWriteArrayList
)。 - ArrayDeque:基于数组的双端队列。
2. 基于链表的结构
- LinkedList:双向链表实现,支持快速头尾操作。
- LinkedHashMap:链表维护插入顺序或访问顺序。
- LinkedBlockingQueue:线程安全的链表阻塞队列。
六、性能对比示例
1. 随机访问(100万次)
// 数组
int[] array = new int[1_000_000];
long start = System.nanoTime();
int value = array[500_000]; // O(1)
long end = System.nanoTime();
// 链表(需遍历到中间)
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i);
start = System.nanoTime();
list.get(500_000); // O(n)
end = System.nanoTime();
结果:数组的随机访问速度比链表快 1000 倍以上。
2. 头部插入(1万次)
// 数组(需频繁扩容和复制)
ArrayList<Integer> arrayList = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 10_000; i++) arrayList.add(0, i); // O(n)
long end = System.nanoTime();
// 链表
LinkedList<Integer> linkedList = new LinkedList<>();
start = System.nanoTime();
for (int i = 0; i < 10_000; i++) linkedList.addFirst(i); // O(1)
end = System.nanoTime();
结果:链表的头部插入速度比数组快 100 倍以上。
七、总结
- 数组:内存高效,适合查询多、增删少的场景。
- 链表:灵活性高,适合增删多、查询少的场景。
- 选择依据:根据数据操作的频率(增删 vs 查询)和内存限制综合权衡。