以下是我对常见数据结构的详细分析,每种数据结构都包含其底层机制、性能分析、典型应用场景以及在算法问题中的高级使用技巧。我会尽量避免表面化的描述,并提供具体、可操作的内容。
1. 数组(Array)
详细描述
数组是一种线性数据结构,存储在连续的内存空间中,所有元素类型相同且大小固定。它的核心特性是通过索引实现 O(1) 时间复杂度的随机访问。
内部工作原理
-
内存分配:数组在创建时分配一块连续的内存,元素存储在相邻的地址中。假设每个元素占用 s 个字节,数组起始地址为 base,第 i 个元素的地址为 base + i * s。
-
索引访问:CPU 通过地址计算直接定位元素,无需遍历。
-
动态数组:如 Java 的 ArrayList,底层是数组,当容量不足时会创建一个更大的数组并复制元素(时间复杂度 O(n))。
⠀优缺点
- 优点:
-
- 随机访问效率极高(O(1))。
- 由于内存连续性,缓存命中率高,适合现代 CPU 的预取机制。
- 缺点:
-
-
插入和删除效率低(O(n)),需要移动后续元素。
-
大小固定(静态数组),动态数组扩容代价高。
-
不适合频繁变动的场景。
-
⠀适用场景
-
快速访问:需要通过索引快速定位元素时,例如查找表。
-
静态数据:数据量已知且不频繁修改时。
-
基础结构:实现堆、哈希表等其他数据结构。
⠀使用技巧
-
双指针法:在有序或部分有序数组中,利用两个指针缩小搜索范围。例如,解决“两数之和”问题时,用左右指针夹逼。
-
滑动窗口:处理子数组问题时,维护一个动态窗口,逐步调整边界。
-
前缀和数组:预计算累加和,用于快速查询区间和。
⠀示例问题:LeetCode 283 - Move Zeroes
问题:将数组中的所有 0 移动到末尾,保持非零元素相对顺序。
public void moveZeroes(int nums) {
int nonZeroIdx = 0;
// 第一步:将非零元素移到前面
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[nonZeroIdx++] = nums[i];
}
}
// 第二步:填充剩余部分为 0
while (nonZeroIdx < nums.length) {
nums[nonZeroIdx++] = 0;
}
}
技巧分析:
-
使用双指针:nonZeroIdx 追踪非零元素的目标位置,i 遍历数组。
-
空间复杂度 O(1),原地操作,避免额外数组。
⠀
2. 链表(Linked List)
详细描述
链表是由节点组成的数据结构,每个节点包含数据和指向下一节点的指针。相比数组,它在内存中是非连续的。
内部工作原理*
- 节点结构:每个节点包含 value 和 next 指针(单链表),双链表还有 prev 指针。
- 动态分配:节点在堆内存中单独分配,通过指针连接。
- 操作:
-
-
插入/删除(已知位置):O(1),只需调整指针。
-
访问:O(n),需从头遍历。
-
⠀优缺点*
- 优点:
-
- 动态大小,无需预分配空间。
- 插入和删除高效(O(1))。
- 缺点:
-
-
随机访问慢(O(n))。
-
内存开销大(指针占用空间)。
-
不缓存友好。
-
⠀适用场景*
-
动态数据:频繁插入和删除的场景。
-
栈/队列实现:单链表适合栈,双链表适合队列。
-
复杂结构:如环形链表、跳表。
⠀使用技巧
-
快慢指针:检测环、找中点、倒数第 k 个节点。
-
哑节点(Dummy Node) :简化头节点操作,避免边界判断。
-
反转链表:通过迭代或递归调整指针方向。
⠀示例问题:LeetCode 141 - Linked List Cycle
问题:判断链表中是否存在环。
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && [fast.next](fast.next) != null) {
slow = [slow.next](slow.next); // 慢指针走一步
fast = [fast.next.next](fast.next.next); // 快指针走两步
if (slow == fast) return true; // 相遇说明有环
}
return false;
}
技巧分析:
-
快慢指针:快指针每次走两步,慢指针走一步,若有环必相遇。
-
时间复杂度 O(n),空间复杂度 O(1)。
⠀
3. 栈(Stack)
详细描述
栈是一种后进先出(LIFO)的数据结构,仅允许在栈顶操作(push 和 pop)。
内部工作原理
-
实现方式:
-
- 数组:固定大小,push/pop 在数组末尾。
- 链表:动态大小,push/pop 在链表头。
-
操作:push 和 pop 均为 O(1)。
⠀优缺点
- 优点:
-
- 实现简单,操作高效。
- 适合逆序处理。
- 缺点:
-
-
只能访问栈顶元素。
-
数组实现时容量有限。
-
⠀适用场景
-
表达式求值:如中缀转后缀、计算结果。
-
回溯算法:DFS、括号匹配。
-
单调栈:解决“下一个更大元素”问题。
⠀使用技巧
-
单调栈:维护递增或递减的栈,处理最近更大/更小元素。
-
辅助栈:如最小栈问题,用额外栈记录状态。
-
递归替代:用栈模拟递归调用栈。
⠀示例问题:LeetCode 155 - Min Stack
问题:设计一个支持 push、pop、top 和 getMin 的栈。
class MinStack {
Stack<Integer> stack = new Stack<>();
Stack<Integer> minStack = new Stack<>();
public void push(int val) {
stack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val); // 只压入小于等于当前最小值的元素
}
}
public void pop() {
if (stack.pop().equals(minStack.peek())) {
minStack.pop(); // 若弹出的是最小值,同步更新
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
技巧分析:
-
辅助栈记录最小值,确保 getMin 为 O(1)。
-
同步更新,保持栈顶始终是最小值。
⠀
4. 队列(Queue)
详细描述
队列是一种先进先出(FIFO)的数据结构,支持在队尾入队(enqueue),队头出队(dequeue)。
内部工作原理
-
实现方式:
-
- 链表:动态大小,入队 O(1),出队 O(1)。
- 循环数组:固定大小,避免空间浪费。
-
双端队列(Deque) :支持两端操作。
⠀优缺点
- 优点:
-
- 按顺序处理任务。
- 动态扩展(链表实现)。
- 缺点:
-
-
不支持随机访问。
-
数组实现时需处理循环逻辑。
-
⠀适用场景
-
BFS:层序遍历、图的最短路径。
-
任务调度:如打印队列。
-
滑动窗口:用双端队列维护最大/最小值。
⠀使用技巧
-
单调队列:在窗口问题中维护单调性。
-
优先队列:结合堆实现,按优先级出队。
-
循环队列:优化空间利用率。
⠀示例问题:LeetCode 102 - Binary Tree Level Order Traversal
问题:二叉树层序遍历。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size(); // 当前层节点数
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}
技巧分析:
-
使用队列实现 BFS。
-
记录每层节点数,分层输出。
⠀
5. 哈希表(Hash Table)
详细描述
哈希表通过哈希函数将键映射到存储位置,实现高效的查找、插入和删除。
内部工作原理
-
哈希函数:将键转换为数组索引,如 hash(key) % tableSize。
-
冲突解决:
-
- 链地址法:每个桶存储链表。
- 开放地址法:探测下一个空位。
-
负载因子:元素数/桶数,过高时需扩容。
⠀优缺点
- 优点:
-
- 平均时间复杂度 O(1)。
- 适合快速查找。
- 缺点:
-
-
空间开销大。
-
哈希冲突可能退化为 O(n)。
-
⠀适用场景
-
计数:统计元素出现次数。
-
去重:快速检查重复元素。
-
映射:键值对存储。
⠀使用技巧
-
滑动窗口+哈希:解决子串/子数组问题。
-
分组:按键聚合数据。
-
优化空间:用数组替代哈希表(当键范围有限时)。
⠀示例问题:LeetCode 3 - Longest Substring Without Repeating Characters
问题:最长无重复字符子串。
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> map = new HashMap<>();
int left = 0, max = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
left = Math.max(left, map.get(c) + 1); // 跳到重复字符后一位
}
map.put(c, right);
max = Math.max(max, right - left + 1);
}
return max;
}
技巧分析:
-
滑动窗口+哈希表,记录字符最后出现位置。
-
动态调整左边界,避免重复。
⠀
6. 树(Tree)
详细描述
树是一种层次结构,包含节点和边,常见类型包括二叉树和二叉搜索树(BST)。
内部工作原理
-
节点:包含值和指向子节点的指针。
-
BST:左子树 < 根 < 右子树,查找复杂度 O(h)(h 为树高)。
-
平衡树:如 AVL、红黑树,保持 O(log n) 操作。
⠀优缺点
- 优点:
-
- 表示层次关系。
- BST 支持快速查找。
- 缺点:
-
-
维护复杂(平衡树)。
-
退化时性能下降。
-
⠀适用场景
-
搜索:有序数据查找。
-
递归问题:分治法。
-
层次遍历:BFS。
⠀使用技巧
-
递归遍历:前序、中序、后序。
-
Morris 遍历:O(1) 空间中序遍历。
-
范围检查:验证 BST。
⠀示例问题:LeetCode 98 - Validate Binary Search Tree
问题:验证二叉搜索树。
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return isValidBST(node.left, min, node.val) && isValidBST(node.right, node.val, max);
}
技巧分析:
-
递归检查每个节点值是否在范围内。
-
用 long 避免整数溢出。
⠀
7. 堆(Heap)
详细描述
堆是一种完全二叉树,满足堆性质(如大顶堆:父节点 >= 子节点)。
内部工作原理
-
数组实现:索引 i 的左子节点为 2i+1,右子节点为 2i+2。
-
维护:插入上浮(O(log n)),删除下沉(O(log n))。
⠀优缺点
- 优点:
-
- 获取极值 O(1)。
- 插入/删除 O(log n)。
- 缺点:
-
-
不支持随机访问。
-
⠀适用场景
-
优先级队列:任务调度。
-
Top K:高频元素。
-
中位数:双堆维护。
⠀使用技巧
-
小顶堆/大顶堆:选择合适类型。
-
堆排序:构建堆后逐步调整。
-
动态维护:实时更新堆。
⠀示例问题:LeetCode 347 - Top K Frequent Elements
问题:前 K 个高频元素。
public int topKFrequent(int nums, int k) {
HashMap<Integer, Integer> freq = new HashMap<>();
for (int num : nums) freq.put(num, freq.getOrDefault(num, 0) + 1);
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) → freq.get(a) - freq.get(b));
for (int num : freq.keySet()) {
pq.offer(num);
if (pq.size() > k) pq.poll();
}
int result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = pq.poll();
}
return result;
}
技巧分析:
-
小顶堆维护 K 个元素。
-
哈希表统计频率。
⠀
8. 图(Graph)
详细描述
图由节点和边组成,可分为有向图和无向图。
内部工作原理
-
表示:
-
- 邻接表:List 或 Map 存储邻居。
- 邻接矩阵:二维数组表示连接。
-
遍历:DFS(递归/栈)、BFS(队列)。
⠀优缺点
- 优点:
-
- 表示复杂关系。
- 支持多种算法。
- 缺点:
-
-
实现复杂。
-
空间/时间开销大。
-
⠀适用场景
-
路径问题:最短路径。
-
连通性:检测连通分量。
-
拓扑排序:依赖关系。
⠀使用技巧
-
DFS:检测环、路径搜索。
-
BFS:最短路径。
-
哈希表:记录访问状态。
⠀示例问题:LeetCode 133 - Clone Graph
问题:克隆无向图。
public Node cloneGraph(Node node) {
if (node == null) return null;
HashMap<Node, Node> map = new HashMap<>();
return clone(node, map);
}
private Node clone(Node node, HashMap<Node, Node> map) {
if (map.containsKey(node)) return map.get(node);
Node copy = new Node(node.val);
map.put(node, copy);
for (Node neighbor : node.neighbors) {
copy.neighbors.add(clone(neighbor, map));
}
return copy;
}
技巧分析:
-
DFS 递归克隆。
-
哈希表避免重复。
⠀
9. 并查集(Union-Find)
详细描述
并查集管理不相交集合,支持合并(union)和查找(find)。
内部工作原理
- 数组:parent[i] 表示 i 的父节点。
- 优化:
-
-
路径压缩:find 时将节点指向根。
-
按秩合并:小树并入大树。
-
⠀优缺点
- 优点:
-
- 接近 O(1) 的操作。
- 高效连通性查询。
- 缺点:
-
-
不支持动态删除。
-
⠀适用场景
-
连通分量:图的连通性。
-
最小生成树:Kruskal 算法。
-
等价关系:分组问题。
⠀使用技巧
-
路径压缩:提高查找效率。
-
初始化:每个元素自成集合。
-
计数:跟踪连通分量数。
⠀示例问题:LeetCode 547 - Number of Provinces
问题:计算朋友圈数量。
public int findCircleNum(int isConnected) {
int n = isConnected.length;
int parent = new int[n];
int count = n;
for (int i = 0; i < n; i++) parent[i] = i;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (isConnected[i] == 1) {
int rootI = find(parent, i);
int rootJ = find(parent, j);
if (rootI != rootJ) {
parent[rootI] = rootJ;
count--;
}
}
}
}
return count;
}
private int find(int parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]);
return parent[x];
}
技巧分析:
-
并查集合并连通城市。
-
路径压缩优化查找。
⠀
10. Trie(前缀树)
详细描述
Trie 是一种树形结构,专门用于处理字符串集合。
内部工作原理
-
节点:包含字符和子节点指针。
-
路径:从根到节点表示字符串。
-
标记:叶子节点标记字符串结束。
⠀优缺点
- 优点:
-
- 前缀查询快(O(m),m 为字符串长度)。
- 适合字典查找。
- 缺点:
-
-
空间开销大。
-
⠀适用场景
-
前缀匹配:自动补全。
-
字符串查询:拼写检查。
-
路由表:IP 匹配。
⠀使用技巧
-
数组/哈希:子节点用数组(固定字符集)或哈希(动态字符集)。
-
压缩 Trie:合并单子节点,节省空间。
-
多模式匹配:Aho-Corasick 算法。
⠀示例问题:LeetCode 208 - Implement Trie
问题:实现 Trie。
class TrieNode {
TrieNode children = new TrieNode[26];
boolean isEnd;
}
class Trie {
TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.isEnd = true;
}
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) return false;
node = node.children[idx];
}
return node.isEnd;
}
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) return false;
node = node.children[idx];
}
return true;
}
}
技巧分析:
-
数组存储子节点,快速定位。
-
分离插入和查询逻辑。
⠀
总结
以上是对数组、链表、栈、队列、哈希表、树、堆、图、并查集和 Trie 的详细描述,包括其底层原理、优缺点、适用场景和使用技巧。每种数据结构都配有具体示例和代码,展示了其在算法问题中的高级应用。