目录
[1.1 什么是线性表?](#1.1 什么是线性表?)
[1.2 顺序表 vs 链表](#1.2 顺序表 vs 链表)
[2.1 List接口概述](#2.1 List接口概述)
[2.2 为什么要用List接口?](#2.2 为什么要用List接口?)
[3.1 ArrayList核心特性](#3.1 ArrayList核心特性)
[3.2 ArrayList扩容机制(面试高频!)](#3.2 ArrayList扩容机制(面试高频!))
[3.3 ArrayList常用操作时间复杂度](#3.3 ArrayList常用操作时间复杂度)
[3.4 ArrayList遍历方式](#3.4 ArrayList遍历方式)
[4.1 LinkedList核心结构](#4.1 LinkedList核心结构)
[4.2 LinkedList时间复杂度分析](#4.2 LinkedList时间复杂度分析)
[4.3 LinkedList特有的双端队列操作](#4.3 LinkedList特有的双端队列操作)
[五、ArrayList vs LinkedList:如何选择?](#五、ArrayList vs LinkedList:如何选择?)
[5.1 性能对比总结](#5.1 性能对比总结)
[5.2 选择指南](#5.2 选择指南)
[6.1 完整扑克牌示例](#6.1 完整扑克牌示例)
[7.1 ArrayList优化](#7.1 ArrayList优化)
[7.2 LinkedList优化](#7.2 LinkedList优化)
一、线性表:数据结构的基础
1.1 什么是线性表?
线性表是n个具有相同特性数据元素的有限序列。它在逻辑上是连续的一条直线,但物理存储上并不一定连续。
常见线性表实现:
-
顺序表(数组实现)
-
链表(链式存储)
-
栈、队列(特殊线性表)
1.2 顺序表 vs 链表
| 特性 | 顺序表(数组) | 链表 |
|---|---|---|
| 存储方式 | 物理地址连续 | 节点通过引用链接 |
| 访问速度 | O(1)随机访问 | O(n)顺序访问 |
| 插入删除 | O(n)需要搬移元素 | O(1)修改指针 |
| 空间利用 | 可能浪费或需要扩容 | 按需申请,无容量概念 |
二、List接口:集合框架的核心
2.1 List接口概述
List是Java集合框架中的重要接口,继承自Collection接口,代表一个有序、可重复的元素序列。
java
public interface List<E> extends Collection<E> {
// 核心方法
boolean add(E e);
void add(int index, E element);
E remove(int index);
E get(int index);
E set(int index, E element);
int indexOf(Object o);
List<E> subList(int fromIndex, int toIndex);
// ...
}
2.2 为什么要用List接口?
java
// 良好的编程实践:面向接口编程
List<String> list = new ArrayList<>(); // √
ArrayList<String> list = new ArrayList<>(); // ×(不够灵活)
// 可以轻松切换实现
List<String> list = new LinkedList<>(); // 只需改一行代码
三、ArrayList:基于数组的动态顺序表
3.1 ArrayList核心特性
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable {
// 底层存储数组
transient Object[] elementData;
// 元素个数
private int size;
}
关键特点:
-
实现RandomAccess接口,支持快速随机访问
-
线程不安全(多线程环境需同步)
-
动态扩容,初始容量10
3.2 ArrayList扩容机制(面试高频!)
java
// 简化版扩容流程
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 1. 检查容量
elementData[size++] = e; // 2. 添加元素
return true;
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容规则总结:
-
首次添加元素时,容量从0扩容到10
-
后续按1.5倍(oldCapacity + oldCapacity >> 1)扩容
-
若预估容量超过1.5倍,按实际需求扩容
-
最大容量为
Integer.MAX_VALUE - 8
3.3 ArrayList常用操作时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| get(index) | O(1) | 数组随机访问 |
| add(E e) | 平摊O(1) | 尾部添加,可能触发扩容 |
| add(index, E) | O(n) | 需要搬移元素 |
| remove(index) | O(n) | 需要搬移元素 |
| contains(Object) | O(n) | 需要遍历查找 |
3.4 ArrayList遍历方式
java
List<Integer> list = new ArrayList<>();
// 1. for循环 + 下标(最高效)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 2. foreach语法糖
for (Integer num : list) {
System.out.println(num);
}
// 3. 迭代器
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
四、LinkedList:基于双向链表的实现
4.1 LinkedList核心结构
java
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable {
// 双向链表节点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
// 头尾指针
transient Node<E> first;
transient Node<E> last;
transient int size = 0;
}
关键特点:
-
实现双向链表,同时实现List和Deque接口
-
不支持RandomAccess,顺序访问
-
插入删除高效,特别适合头尾操作
4.2 LinkedList时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| addFirst/addLast | O(1) | 直接修改头尾指针 |
| removeFirst/removeLast | O(1) | 直接修改头尾指针 |
| get(index) | O(n) | 需要从头或尾遍历 |
| add(index, E) | O(n) | 需要找到插入位置 |
| remove(Object) | O(n) | 需要遍历查找 |
4.3 LinkedList特有的双端队列操作
java
LinkedList<Integer> deque = new LinkedList<>();
// 作为队列使用(FIFO)
deque.offer(1); // 入队
deque.poll(); // 出队
// 作为栈使用(LIFO)
deque.push(1); // 入栈
deque.pop(); // 出栈
// 双端队列操作
deque.addFirst(1); // 头部添加
deque.addLast(2); // 尾部添加
deque.removeFirst(); // 头部移除
deque.removeLast(); // 尾部移除
五、ArrayList vs LinkedList:如何选择?
5.1 性能对比总结
| 考量维度 | ArrayList | LinkedList |
|---|---|---|
| 随机访问 | ⭐⭐⭐⭐⭐ O(1) | ⭐⭐ O(n) |
| 头部插入 | ⭐ O(n) | ⭐⭐⭐⭐⭐ O(1) |
| 尾部插入 | ⭐⭐⭐⭐ O(1)平摊 | ⭐⭐⭐⭐⭐ O(1) |
| 内存占用 | 较少(仅存储数据) | 较多(额外存储指针) |
| 缓存友好性 | ⭐⭐⭐⭐⭐(连续内存) | ⭐(内存碎片) |
| 中间插入 | ⭐ O(n) | ⭐⭐ O(n)找位置+O(1)插入 |
5.2 选择指南
选择ArrayList当:
-
需要频繁随机访问元素
-
数据量相对稳定,少在中间插入删除
-
内存空间有限
选择LinkedList当:
-
需要频繁在头部或尾部插入删除
-
实现队列、栈或双端队列
-
数据量变化大,频繁增删
java
// 实际应用示例
// 场景1:需要快速随机访问
List<Student> studentList = new ArrayList<>(); // 按学号快速查找
// 场景2:实现最近浏览记录(LRU Cache)
List<PageView> recentViews = new LinkedList<>(); // 频繁在头部插入删除
六、实战应用:扑克牌游戏
6.1 完整扑克牌示例
java
// 扑克牌类
class Card {
public int rank; // 牌面值
public String suit; // 花色
@Override
public String toString() {
return String.format("[%s %d]", suit, rank);
}
}
// 扑克牌操作
public class CardDemo {
private static final String[] SUITS = {"♥", "♠", "♦", "♣"};
// 买一副牌
private static List<Card> buyDeck() {
List<Card> deck = new ArrayList<>(52);
for (int i = 0; i < 4; i++) {
for (int j = 1; j <= 13; j++) {
Card card = new Card();
card.suit = SUITS[i];
card.rank = j;
deck.add(card);
}
}
return deck;
}
// 洗牌(Fisher-Yates算法)
private static void shuffle(List<Card> deck) {
Random random = new Random();
for (int i = deck.size() - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
// 交换位置
Card temp = deck.get(i);
deck.set(i, deck.get(j));
deck.set(j, temp);
}
}
// 发牌
public static void main(String[] args) {
List<Card> deck = buyDeck();
System.out.println("新牌:" + deck);
shuffle(deck);
System.out.println("洗牌后:" + deck);
// 三人玩牌,每人5张
List<List<Card>> hands = new ArrayList<>();
for (int i = 0; i < 3; i++) {
hands.add(new ArrayList<>());
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
// 从牌堆顶部取牌
Card card = deck.remove(0);
hands.get(j).add(card);
}
}
System.out.println("玩家A手牌:" + hands.get(0));
System.out.println("剩余牌数:" + deck.size());
}
}
七、性能优化建议
7.1 ArrayList优化
java
// 1. 预估容量,避免多次扩容
List<Integer> list = new ArrayList<>(1000); // 指定初始容量
// 2. 批量添加使用addAll
list.addAll(anotherList); // 比循环add高效
// 3. 适时trimToSize释放多余空间
list.trimToSize();
7.2 LinkedList优化
java
// 1. 使用合适的遍历方式
// 从头开始遍历
for (int i = 0; i < list.size(); i++) {
list.get(i); // 效率低!每次都要从头遍历
}
// 使用迭代器
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
it.next(); // 效率高
}
// 2. 利用头尾操作优势
linkedList.addFirst(item); // O(1)
linkedList.removeLast(); // O(1)
总结
ArrayList和LinkedList是Java集合框架中最基础、最常用的两个List实现:
-
ArrayList :数组实现,空间连续 ,适合读多写少 、随机访问频繁的场景
-
LinkedList :双向链表实现,空间分散 ,适合增删频繁 、特别是头尾操作的场景
记住黄金法则:
-
当你不知道选什么时,先用ArrayList
-
当需要频繁在头部插入删除时,考虑LinkedList
-
当需要实现队列、栈或双端队列时,选择LinkedList
作为Java后端开发者,深入理解这些集合类的实现原理,不仅能帮助你在面试中脱颖而出,更能让你在实际开发中写出更高效、更健壮的代码。
动手实践建议:尝试自己实现一个简化版的ArrayList和LinkedList,亲自体验数组扩容和链表指针操作的过程,这会让你的理解更加深刻!