什么是数据结构?

数据结构(Data Structure)是计算机存储、组织数据的方式。一个好的数据结构可以让程序运行得更快、占用更少的内存。选择合适的数据结构,是编写高效程序的关键。

数据结构的分类

数据结构按照不同的维度可以分为多种类型:

按存储结构分类

1. 线性结构

数据元素之间存在一对一的线性关系。

  • 数组 (Array)
  • 链表 (LinkedList)
  • (Stack)
  • 队列 (Queue)

2. 非线性结构

数据元素之间存在一对多或多对多的关系。

  • (Tree)
  • (Graph)
  • (Heap)

按逻辑结构分类

  • 集合结构: 数据元素之间除了”同属一个集合”外,没有其他关系
  • 线性结构: 数据元素之间是一对一的关系
  • 树形结构: 数据元素之间是一对多的层次关系
  • 图形结构: 数据元素之间是多对多的关系

线性数据结构

数组 (Array)

定义: 数组是最基础的数据结构,用连续的内存空间存储相同类型的元素。

特点:

  • ✅ 支持快速随机访问 (时间复杂度 O(1))
  • ✅ 内存连续,缓存友好
  • ❌ 插入/删除效率低 (需要移动元素)
  • ❌ 大小固定,不易扩展

Java 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 静态数组
int[] array = new int[10];
array[0] = 1;
array[1] = 2;

// 动态数组 - ArrayList (基于数组实现)
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

System.out.println("元素访问: " + list.get(0)); // O(1)
list.remove(0); // O(n) - 需要移动后续元素

时间复杂度:

  • 访问: O(1)
  • 查找: O(n)
  • 插入: O(n) (平均)
  • 删除: O(n) (平均)

应用场景:

  • 需要快速随机访问元素
  • 数据量固定或变化不大
  • 频繁读取,很少修改

ArrayList 扩容机制:

1
2
3
4
5
6
7
8
9
// ArrayList 默认初始容量为 10
// 当容量不足时,会扩容为原来的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

// 示例:扩容过程
// 初始容量: 10
// 第一次扩容: 10 + 5 = 15
// 第二次扩容: 15 + 7 = 22
// 第三次扩容: 22 + 11 = 33

链表 (LinkedList)

定义: 链表是由一系列节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。

分类:

  1. 单向链表: 每个节点只有一个指向下一个节点的指针
  2. 双向链表: 每个节点有两个指针,分别指向前后节点
  3. 循环链表: 尾节点指向头节点,形成环

特点:

  • ✅ 插入/删除效率高 (O(1))
  • ✅ 大小动态可变
  • ❌ 不支持随机访问
  • ❌ 需要额外的内存存储指针

单向链表实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 链表节点
class Node {
int data;
Node next;

public Node(int data) {
this.data = data;
this.next = null;
}
}

// 单向链表
class LinkedList {
private Node head;

// 在头部插入 - O(1)
public void insertAtHead(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
}

// 在尾部插入 - O(n)
public void insertAtTail(int data) {
Node newNode = new Node(data);

if (head == null) {
head = newNode;
return;
}

Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}

// 删除节点 - O(n)
public void delete(int data) {
if (head == null) return;

// 删除头节点
if (head.data == data) {
head = head.next;
return;
}

// 删除其他节点
Node current = head;
while (current.next != null) {
if (current.next.data == data) {
current.next = current.next.next;
return;
}
current = current.next;
}
}

// 查找节点 - O(n)
public boolean search(int data) {
Node current = head;
while (current != null) {
if (current.data == data) {
return true;
}
current = current.next;
}
return false;
}

// 打印链表
public void print() {
Node current = head;
while (current != null) {
System.out.print(current.data + " -> ");
current = current.next;
}
System.out.println("null");
}
}

// 使用示例
LinkedList list = new LinkedList();
list.insertAtHead(3);
list.insertAtHead(2);
list.insertAtHead(1);
list.insertAtTail(4);
list.print(); // 输出: 1 -> 2 -> 3 -> 4 -> null

list.delete(2);
list.print(); // 输出: 1 -> 3 -> 4 -> null

双向链表实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 双向链表节点
class DoublyNode {
int data;
DoublyNode prev;
DoublyNode next;

public DoublyNode(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}

// 双向链表
class DoublyLinkedList {
private DoublyNode head;
private DoublyNode tail;

// 在头部插入
public void insertAtHead(int data) {
DoublyNode newNode = new DoublyNode(data);

if (head == null) {
head = tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}

// 在尾部插入
public void insertAtTail(int data) {
DoublyNode newNode = new DoublyNode(data);

if (tail == null) {
head = tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}

// 正向打印
public void printForward() {
DoublyNode current = head;
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.next;
}
System.out.println("null");
}

// 反向打印
public void printBackward() {
DoublyNode current = tail;
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.prev;
}
System.out.println("null");
}
}

时间复杂度:

  • 访问: O(n)
  • 查找: O(n)
  • 插入: O(1) (已知位置) / O(n) (需要查找)
  • 删除: O(1) (已知位置) / O(n) (需要查找)

数组 vs 链表:

操作 数组 链表
随机访问 O(1) O(n)
插入/删除 O(n) O(1)
内存占用 连续空间 分散空间 + 指针
缓存友好性

栈 (Stack)

定义: 栈是一种后进先出(LIFO - Last In First Out)的数据结构。

核心操作:

  • push(): 入栈 - 将元素压入栈顶
  • pop(): 出栈 - 弹出栈顶元素
  • peek(): 查看栈顶元素(不删除)
  • isEmpty(): 判断栈是否为空

基于数组实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class ArrayStack {
private int[] array;
private int top;
private int capacity;

public ArrayStack(int capacity) {
this.capacity = capacity;
this.array = new int[capacity];
this.top = -1;
}

// 入栈 - O(1)
public void push(int data) {
if (top == capacity - 1) {
throw new StackOverflowError("栈已满");
}
array[++top] = data;
}

// 出栈 - O(1)
public int pop() {
if (isEmpty()) {
throw new IllegalStateException("栈为空");
}
return array[top--];
}

// 查看栈顶元素 - O(1)
public int peek() {
if (isEmpty()) {
throw new IllegalStateException("栈为空");
}
return array[top];
}

// 判断是否为空
public boolean isEmpty() {
return top == -1;
}

// 获取栈大小
public int size() {
return top + 1;
}
}

// 使用示例
ArrayStack stack = new ArrayStack(5);
stack.push(1);
stack.push(2);
stack.push(3);

System.out.println("栈顶元素: " + stack.peek()); // 3
System.out.println("出栈: " + stack.pop()); // 3
System.out.println("出栈: " + stack.pop()); // 2

基于链表实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class LinkedStack {
private Node top;
private int size;

private class Node {
int data;
Node next;

Node(int data) {
this.data = data;
}
}

// 入栈 - O(1)
public void push(int data) {
Node newNode = new Node(data);
newNode.next = top;
top = newNode;
size++;
}

// 出栈 - O(1)
public int pop() {
if (isEmpty()) {
throw new IllegalStateException("栈为空");
}
int data = top.data;
top = top.next;
size--;
return data;
}

// 查看栈顶元素
public int peek() {
if (isEmpty()) {
throw new IllegalStateException("栈为空");
}
return top.data;
}

public boolean isEmpty() {
return top == null;
}

public int size() {
return size;
}
}

Java 标准库中的栈:

1
2
3
4
5
6
7
8
9
10
11
// 方式1: 使用 Stack 类(不推荐,已过时)
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.pop();

// 方式2: 使用 Deque 接口(推荐)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.pop();

应用场景:

  1. 函数调用栈: 程序运行时的函数调用记录
  2. 表达式求值: 后缀表达式计算
  3. 括号匹配: 检查括号是否匹配
  4. 浏览器历史: 后退功能
  5. 撤销操作: Ctrl+Z 功能

经典题目: 括号匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean isValidParentheses(String s) {
Deque<Character> stack = new ArrayDeque<>();

for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;

char top = stack.pop();
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}

return stack.isEmpty();
}

// 测试
System.out.println(isValidParentheses("()[]{}")); // true
System.out.println(isValidParentheses("([)]")); // false
System.out.println(isValidParentheses("{[]}")); // true

队列 (Queue)

定义: 队列是一种先进先出(FIFO - First In First Out)的数据结构。

核心操作:

  • enqueue() / offer(): 入队 - 在队尾添加元素
  • dequeue() / poll(): 出队 - 从队头移除元素
  • peek(): 查看队头元素(不删除)
  • isEmpty(): 判断队列是否为空

基于数组实现(循环队列):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class CircularQueue {
private int[] array;
private int front; // 队头指针
private int rear; // 队尾指针
private int size; // 当前元素个数
private int capacity;

public CircularQueue(int capacity) {
this.capacity = capacity;
this.array = new int[capacity];
this.front = 0;
this.rear = -1;
this.size = 0;
}

// 入队 - O(1)
public void enqueue(int data) {
if (isFull()) {
throw new IllegalStateException("队列已满");
}
rear = (rear + 1) % capacity; // 循环利用空间
array[rear] = data;
size++;
}

// 出队 - O(1)
public int dequeue() {
if (isEmpty()) {
throw new IllegalStateException("队列为空");
}
int data = array[front];
front = (front + 1) % capacity;
size--;
return data;
}

// 查看队头元素
public int peek() {
if (isEmpty()) {
throw new IllegalStateException("队列为空");
}
return array[front];
}

public boolean isEmpty() {
return size == 0;
}

public boolean isFull() {
return size == capacity;
}

public int size() {
return size;
}
}

// 使用示例
CircularQueue queue = new CircularQueue(5);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);

System.out.println("队头元素: " + queue.peek()); // 1
System.out.println("出队: " + queue.dequeue()); // 1
System.out.println("出队: " + queue.dequeue()); // 2

基于链表实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class LinkedQueue {
private Node front;
private Node rear;
private int size;

private class Node {
int data;
Node next;

Node(int data) {
this.data = data;
}
}

// 入队 - O(1)
public void enqueue(int data) {
Node newNode = new Node(data);

if (isEmpty()) {
front = rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
size++;
}

// 出队 - O(1)
public int dequeue() {
if (isEmpty()) {
throw new IllegalStateException("队列为空");
}

int data = front.data;
front = front.next;

if (front == null) {
rear = null;
}

size--;
return data;
}

public int peek() {
if (isEmpty()) {
throw new IllegalStateException("队列为空");
}
return front.data;
}

public boolean isEmpty() {
return front == null;
}

public int size() {
return size;
}
}

Java 标准库中的队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. LinkedList 实现 Queue 接口
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.poll();

// 2. ArrayDeque (推荐,性能更好)
Queue<Integer> queue2 = new ArrayDeque<>();
queue2.offer(1);
queue2.offer(2);
queue2.poll();

// 3. PriorityQueue (优先队列)
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(3);
pq.offer(1);
pq.offer(2);
System.out.println(pq.poll()); // 1 (最小的元素优先出队)

特殊队列:

双端队列 (Deque - Double Ended Queue)

可以在两端进行插入和删除操作。

1
2
3
4
5
6
7
8
9
10
11
Deque<Integer> deque = new ArrayDeque<>();

// 队头操作
deque.addFirst(1);
deque.removeFirst();
deque.peekFirst();

// 队尾操作
deque.addLast(2);
deque.removeLast();
deque.peekLast();

优先队列 (Priority Queue)

元素按照优先级出队,而不是按照插入顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 最小堆(默认)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
System.out.println(minHeap.poll()); // 1

// 最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(2);
System.out.println(maxHeap.poll()); // 3

应用场景:

  1. 任务调度: 按顺序处理任务
  2. 消息队列: Kafka、RabbitMQ
  3. 广度优先搜索: BFS 算法
  4. 打印队列: 打印机任务队列
  5. 线程池: 任务等待队列

非线性数据结构

树 (Tree)

定义: 树是一种层次结构的数据结构,由节点和边组成。每个节点包含数据和指向子节点的指针。

基本概念:

  • 根节点: 树的顶端节点
  • 父节点: 有子节点的节点
  • 子节点: 某个节点的下一层节点
  • 叶子节点: 没有子节点的节点
  • 深度: 从根节点到某节点的边数
  • 高度: 从某节点到叶子节点的最长路径

二叉树 (Binary Tree)

定义: 每个节点最多有两个子节点(左子节点和右子节点)。

二叉树节点定义:

1
2
3
4
5
6
7
8
9
10
11
class TreeNode {
int val;
TreeNode left;
TreeNode right;

public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}

二叉树的分类:

  1. 满二叉树: 所有非叶子节点都有两个子节点,且所有叶子节点在同一层
  2. 完全二叉树: 除最后一层外,其他层都是满的,最后一层从左到右连续
  3. 平衡二叉树: 任意节点的左右子树高度差不超过 1

二叉树的遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class BinaryTree {
TreeNode root;

// 前序遍历: 根 -> 左 -> 右
public void preOrder(TreeNode node) {
if (node == null) return;

System.out.print(node.val + " ");
preOrder(node.left);
preOrder(node.right);
}

// 中序遍历: 左 -> 根 -> 右
public void inOrder(TreeNode node) {
if (node == null) return;

inOrder(node.left);
System.out.print(node.val + " ");
inOrder(node.right);
}

// 后序遍历: 左 -> 右 -> 根
public void postOrder(TreeNode node) {
if (node == null) return;

postOrder(node.left);
postOrder(node.right);
System.out.print(node.val + " ");
}

// 层序遍历(广度优先)
public void levelOrder(TreeNode root) {
if (root == null) return;

Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.print(node.val + " ");

if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
}

// 构建示例树
// 1
// / \
// 2 3
// / \
// 4 5
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

BinaryTree tree = new BinaryTree();
tree.root = root;

System.out.print("前序遍历: ");
tree.preOrder(root); // 1 2 4 5 3

System.out.print("\n中序遍历: ");
tree.inOrder(root); // 4 2 5 1 3

System.out.print("\n后序遍历: ");
tree.postOrder(root); // 4 5 2 3 1

System.out.print("\n层序遍历: ");
tree.levelOrder(root); // 1 2 3 4 5

二叉搜索树 (BST - Binary Search Tree)

定义: 一种特殊的二叉树,满足以下性质:

  • 左子树的所有节点值 < 根节点值
  • 右子树的所有节点值 > 根节点值
  • 左右子树也都是二叉搜索树

BST 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class BST {
TreeNode root;

// 插入节点 - 平均 O(log n),最坏 O(n)
public TreeNode insert(TreeNode node, int val) {
if (node == null) {
return new TreeNode(val);
}

if (val < node.val) {
node.left = insert(node.left, val);
} else if (val > node.val) {
node.right = insert(node.right, val);
}

return node;
}

// 查找节点 - 平均 O(log n),最坏 O(n)
public boolean search(TreeNode node, int val) {
if (node == null) {
return false;
}

if (val == node.val) {
return true;
} else if (val < node.val) {
return search(node.left, val);
} else {
return search(node.right, val);
}
}

// 删除节点
public TreeNode delete(TreeNode node, int val) {
if (node == null) {
return null;
}

if (val < node.val) {
node.left = delete(node.left, val);
} else if (val > node.val) {
node.right = delete(node.right, val);
} else {
// 找到要删除的节点

// 情况1: 叶子节点
if (node.left == null && node.right == null) {
return null;
}

// 情况2: 只有一个子节点
if (node.left == null) {
return node.right;
}
if (node.right == null) {
return node.left;
}

// 情况3: 有两个子节点
// 找到右子树的最小节点(后继节点)
TreeNode successor = findMin(node.right);
node.val = successor.val;
node.right = delete(node.right, successor.val);
}

return node;
}

// 找到最小节点
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}

// 使用示例
BST bst = new BST();
bst.root = bst.insert(bst.root, 50);
bst.insert(bst.root, 30);
bst.insert(bst.root, 70);
bst.insert(bst.root, 20);
bst.insert(bst.root, 40);
bst.insert(bst.root, 60);
bst.insert(bst.root, 80);

// 50
// / \
// 30 70
// / \ / \
// 20 40 60 80

System.out.println(bst.search(bst.root, 40)); // true
System.out.println(bst.search(bst.root, 90)); // false

bst.root = bst.delete(bst.root, 30);

BST 的优缺点:

  • ✅ 查找、插入、删除的平均时间复杂度为 O(log n)
  • ✅ 中序遍历可以得到有序序列
  • ❌ 在最坏情况下(退化为链表),时间复杂度为 O(n)

堆 (Heap)

定义: 堆是一种特殊的完全二叉树,分为最大堆和最小堆。

堆的性质:

  • 最大堆: 父节点的值 ≥ 子节点的值,根节点是最大值
  • 最小堆: 父节点的值 ≤ 子节点的值,根节点是最小值

堆的存储: 通常用数组实现,对于索引 i 的节点:

  • 父节点索引: (i - 1) / 2
  • 左子节点索引: 2 * i + 1
  • 右子节点索引: 2 * i + 2

最小堆实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class MinHeap {
private int[] heap;
private int size;
private int capacity;

public MinHeap(int capacity) {
this.capacity = capacity;
this.heap = new int[capacity];
this.size = 0;
}

// 获取父节点索引
private int parent(int i) {
return (i - 1) / 2;
}

// 获取左子节点索引
private int leftChild(int i) {
return 2 * i + 1;
}

// 获取右子节点索引
private int rightChild(int i) {
return 2 * i + 2;
}

// 交换元素
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}

// 插入元素 - O(log n)
public void insert(int val) {
if (size == capacity) {
throw new IllegalStateException("堆已满");
}

// 在数组末尾插入新元素
heap[size] = val;
int current = size;
size++;

// 上浮操作,维护堆性质
while (current > 0 && heap[current] < heap[parent(current)]) {
swap(current, parent(current));
current = parent(current);
}
}

// 删除最小元素 - O(log n)
public int extractMin() {
if (size == 0) {
throw new IllegalStateException("堆为空");
}

int min = heap[0];

// 将最后一个元素移到根节点
heap[0] = heap[size - 1];
size--;

// 下沉操作,维护堆性质
heapify(0);

return min;
}

// 下沉操作
private void heapify(int i) {
int minIndex = i;
int left = leftChild(i);
int right = rightChild(i);

// 找到最小的节点
if (left < size && heap[left] < heap[minIndex]) {
minIndex = left;
}
if (right < size && heap[right] < heap[minIndex]) {
minIndex = right;
}

// 如果最小节点不是当前节点,交换并继续下沉
if (minIndex != i) {
swap(i, minIndex);
heapify(minIndex);
}
}

// 获取最小元素(不删除) - O(1)
public int getMin() {
if (size == 0) {
throw new IllegalStateException("堆为空");
}
return heap[0];
}

public boolean isEmpty() {
return size == 0;
}

public int size() {
return size;
}
}

// 使用示例
MinHeap heap = new MinHeap(10);
heap.insert(5);
heap.insert(3);
heap.insert(7);
heap.insert(1);

System.out.println("最小元素: " + heap.getMin()); // 1
System.out.println("删除: " + heap.extractMin()); // 1
System.out.println("最小元素: " + heap.getMin()); // 3

堆的应用:

  1. 优先队列: Java 的 PriorityQueue 就是基于堆实现的
  2. 堆排序: 时间复杂度 O(n log n)
  3. Top K 问题: 找出最大/最小的 K 个元素
  4. 任务调度: 按优先级调度任务

经典问题: 查找第 K 大的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int findKthLargest(int[] nums, int k) {
// 使用最小堆,保持堆的大小为 k
PriorityQueue<Integer> minHeap = new PriorityQueue<>();

for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll(); // 移除最小的元素
}
}

return minHeap.peek(); // 堆顶就是第 K 大的元素
}

// 测试
int[] nums = {3, 2, 1, 5, 6, 4};
System.out.println(findKthLargest(nums, 2)); // 5

哈希表 (Hash Table)

定义: 哈希表是根据键(Key)直接访问值(Value)的数据结构。通过哈希函数将键映射到数组的索引位置。

核心概念:

  • 哈希函数: 将键转换为数组索引的函数
  • 哈希冲突: 不同的键映射到同一个索引
  • 负载因子: 元素个数 / 数组容量

解决哈希冲突的方法:

1. 链地址法 (Separate Chaining)

每个数组位置存储一个链表,冲突的元素都存在同一个链表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class HashTable {
private class Entry {
String key;
int value;
Entry next;

Entry(String key, int value) {
this.key = key;
this.value = value;
}
}

private Entry[] table;
private int capacity;
private int size;

public HashTable(int capacity) {
this.capacity = capacity;
this.table = new Entry[capacity];
this.size = 0;
}

// 哈希函数
private int hash(String key) {
return Math.abs(key.hashCode()) % capacity;
}

// 插入/更新 - 平均 O(1)
public void put(String key, int value) {
int index = hash(key);
Entry entry = table[index];

// 查找是否已存在
while (entry != null) {
if (entry.key.equals(key)) {
entry.value = value; // 更新
return;
}
entry = entry.next;
}

// 插入新节点(头插法)
Entry newEntry = new Entry(key, value);
newEntry.next = table[index];
table[index] = newEntry;
size++;
}

// 查找 - 平均 O(1)
public Integer get(String key) {
int index = hash(key);
Entry entry = table[index];

while (entry != null) {
if (entry.key.equals(key)) {
return entry.value;
}
entry = entry.next;
}

return null; // 未找到
}

// 删除 - 平均 O(1)
public void remove(String key) {
int index = hash(key);
Entry entry = table[index];
Entry prev = null;

while (entry != null) {
if (entry.key.equals(key)) {
if (prev == null) {
table[index] = entry.next;
} else {
prev.next = entry.next;
}
size--;
return;
}
prev = entry;
entry = entry.next;
}
}

public int size() {
return size;
}
}

// 使用示例
HashTable ht = new HashTable(10);
ht.put("apple", 1);
ht.put("banana", 2);
ht.put("orange", 3);

System.out.println(ht.get("apple")); // 1
System.out.println(ht.get("banana")); // 2

ht.remove("banana");
System.out.println(ht.get("banana")); // null

2. 开放地址法 (Open Addressing)

当发生冲突时,寻找下一个空闲位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class OpenAddressingHashTable {
private String[] keys;
private Integer[] values;
private int capacity;
private int size;

public OpenAddressingHashTable(int capacity) {
this.capacity = capacity;
this.keys = new String[capacity];
this.values = new Integer[capacity];
this.size = 0;
}

private int hash(String key) {
return Math.abs(key.hashCode()) % capacity;
}

// 线性探测
public void put(String key, int value) {
if (size >= capacity * 0.75) {
throw new IllegalStateException("哈希表负载过高");
}

int index = hash(key);

// 线性探测找到空位或相同key
while (keys[index] != null) {
if (keys[index].equals(key)) {
values[index] = value; // 更新
return;
}
index = (index + 1) % capacity;
}

keys[index] = key;
values[index] = value;
size++;
}

public Integer get(String key) {
int index = hash(key);

while (keys[index] != null) {
if (keys[index].equals(key)) {
return values[index];
}
index = (index + 1) % capacity;
}

return null;
}
}

Java 中的哈希表:

1
2
3
4
5
6
7
8
9
10
11
// HashMap - 基于哈希表实现
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map.get("apple")); // 1

// HashSet - 基于 HashMap 实现
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
System.out.println(set.contains("apple")); // true

HashMap 的工作原理:

  1. 计算 key 的 hashCode
  2. 通过哈希函数计算数组索引: (n - 1) & hash
  3. 如果没有冲突,直接存入
  4. 如果有冲突:
    • JDK 1.7: 使用链表
    • JDK 1.8+: 链表长度 ≤ 8 时使用链表,> 8 时转为红黑树

时间复杂度:

  • 平均情况: O(1)
  • 最坏情况: O(n) (所有元素都冲突)

应用场景:

  1. 缓存系统: LRU Cache
  2. 数据库索引: 快速查找
  3. 去重: HashSet
  4. 统计频率: 字符/单词频率统计

经典问题: 两数之和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();

for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}

return null;
}

// 测试
int[] nums = {2, 7, 11, 15};
int[] result = twoSum(nums, 9);
System.out.println(Arrays.toString(result)); // [0, 1]

时间复杂度和空间复杂度

时间复杂度

时间复杂度用大O表示法描述算法执行时间与输入规模的关系。

常见时间复杂度(从快到慢):

  1. O(1) - 常数时间: 数组随机访问、哈希表查找
  2. O(log n) - 对数时间: 二分查找、平衡二叉树操作
  3. O(n) - 线性时间: 遍历数组、链表查找
  4. O(n log n) - 线性对数时间: 快速排序、归并排序、堆排序
  5. O(n²) - 平方时间: 冒泡排序、选择排序、插入排序
  6. O(n³) - 立方时间: 三层嵌套循环
  7. O(2^n) - 指数时间: 递归计算斐波那契数列
  8. O(n!) - 阶乘时间: 旅行商问题(暴力解法)

时间复杂度对比:

1
2
3
4
5
6
7
n = 100 时:
O(1) = 1
O(log n) ≈ 7
O(n) = 100
O(n log n)≈ 700
O(n²) = 10,000
O(2^n) ≈ 1.27 × 10³⁰

空间复杂度

空间复杂度描述算法占用内存空间与输入规模的关系。

常见空间复杂度:

  1. O(1) - 常数空间: 几个变量
  2. O(n) - 线性空间: 一个数组
  3. O(n²) - 平方空间: 二维数组

数据结构对比总结

数据结构 访问 查找 插入 删除 空间复杂度
数组 O(1) O(n) O(n) O(n) O(n)
链表 O(n) O(n) O(1) O(1) O(n)
O(n) O(n) O(1) O(1) O(n)
队列 O(n) O(n) O(1) O(1) O(n)
哈希表 - O(1) O(1) O(1) O(n)
二叉搜索树 O(log n) O(log n) O(log n) O(log n) O(n)
O(1) O(n) O(log n) O(log n) O(n)

注: 表中的时间复杂度为平均情况。


如何选择合适的数据结构?

  1. 需要快速随机访问 → 数组、ArrayList
  2. 频繁插入/删除 → 链表、LinkedList
  3. 后进先出 → 栈
  4. 先进先出 → 队列
  5. 快速查找/去重 → 哈希表(HashMap/HashSet)
  6. 有序数据 → 二叉搜索树、TreeMap/TreeSet
  7. 优先级处理 → 堆、PriorityQueue
  8. 图相关问题 → 邻接表、邻接矩阵

总结

数据结构是程序设计的基石,理解和掌握常用数据结构对于编写高效程序至关重要。

学习建议:

  1. 理解原理: 不要只记API,要理解底层实现
  2. 动手实践: 自己实现一遍各种数据结构
  3. 刷题巩固: 通过 LeetCode 等平台练习算法题
  4. 分析复杂度: 养成分析时间和空间复杂度的习惯
  5. 阅读源码: 研究 JDK 集合框架的源码实现

推荐资源:

掌握数据结构,从理解概念开始,通过实践加深理解,最终做到灵活运用!