数据结构与算法实战指南
数据结构与算法是大厂面试的硬通货,也是 CSDN 流量最大的基础板块。本文不追求覆盖所有知识点,而是聚焦面试与实战最高频的部分:复杂度分析、数组链表、栈队列、哈希表、树与二叉树、排序、查找、动态规划、常见算法思想,每个都带图解式讲解和代码。
一、复杂度分析:算法的度量衡
1.1 时间复杂度
衡量算法执行时间随输入规模增长的趋势,用大 O 表示(忽略常数和低阶项):
| 复杂度 | 含义 | 例子 |
|---|---|---|
| O(1) | 常数时间 | 数组按下标取元素 |
| O(log n) | 对数时间 | 二分查找 |
| O(n) | 线性时间 | 遍历数组 |
| O(n log n) | 线性对数 | 快速排序、归并排序 |
| O(n²) | 平方时间 | 冒泡排序、双层循环 |
| O(2ⁿ) | 指数时间 | 斐波那契(朴素递归) |
判断技巧:
- 单层循环 → O(n);
- 双层循环 → O(n²);
- 循环减半 → O(log n);
- 递归看递推式。
1.2 空间复杂度
算法运行需要多少额外内存:
- O(1):几个变量;
- O(n):一个长度为 n 的辅助数组。
面试要求:说解法时主动说"时间复杂度 O(n),空间复杂度 O(1)"。
1.3 实战:分析一道题
题目:判断一个字符串里的字符是否都不同。
python
# 解法一:双重循环 O(n²),空间 O(1)
def is_unique(s):
for i in range(len(s)):
for j in range(i + 1, len(s)):
if s[i] == s[j]:
return False
return True
# 解法二:哈希表 O(n),空间 O(n)
def is_unique(s):
seen = set()
for c in s:
if c in seen:
return False
seen.add(c)
return True
面试要能说出两种解法的复杂度差异。
二、数组与链表
2.1 数组(Array)
特点:
- 内存连续,按下标访问 O(1);
- 插入/删除要移动元素 O(n);
- 扩容要整体搬移。
实战场景:随机访问多、元素个数确定。
2.2 链表(LinkedList)
特点:
- 节点 + 指针,内存不连续;
- 插入/删除(已知位置)O(1);
- 按下标访问 O(n)。
java
public class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
常见操作:
java
// 反转链表(面试高频)
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
// 判断是否有环(快慢指针)
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
2.3 数组 vs 链表
| 维度 | 数组 | 链表 |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 插入删除(头部) | O(n) | O(1) |
| 内存 | 连续 | 分散 |
| 缓存友好 | 好 | 差 |
Java 里 :ArrayList 是数组,LinkedList 是链表。
三、栈与队列
3.1 栈(Stack):后进先出
场景:括号匹配、函数调用、撤销操作、表达式求值。
java
// 括号匹配(经典题)
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
Map<Character, Character> map = Map.of(')', '(', ']', '[', '}', '{');
for (char c : s.toCharArray()) {
if (map.containsKey(c)) {
if (stack.isEmpty() || stack.pop() != map.get(c)) return false;
} else {
stack.push(c);
}
}
return stack.isEmpty();
}
3.2 队列(Queue):先进先出
场景:任务排队、BFS 广度优先搜索、消息队列。
java
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
int head = queue.poll(); // 出队
queue.peek(); // 看队头
3.3 单调栈(进阶)
场景:下一个更大元素、柱状图最大矩形。
java
// 下一个更大元素(O(n) 解法)
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Deque<Integer> stack = new ArrayDeque<>(); // 存下标
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
result[stack.pop()] = nums[i];
}
stack.push(i);
}
return result;
}
四、哈希表(HashMap)
4.1 原理
通过哈希函数把 key 映射到数组下标,查找 O(1)。冲突用链地址法(链表)或开放寻址。
Java HashMap 要点(面试必问):
- 数组 + 链表 + 红黑树(链表超 8 个转红黑树);
- 扩容:默认 16,负载因子 0.75,扩为 2 倍;
- 为什么容量是 2 的幂:哈希 & (length-1) 快速取模。
4.2 实战:两数之和(LeetCode 1)
java
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 new int[]{-1, -1};
}
复杂度:O(n) 时间,O(n) 空间------典型的"空间换时间"。
4.3 实战:数组去重与计数
python
from collections import Counter
# 统计词频
words = ["a", "b", "a", "c", "b", "a"]
counter = Counter(words) # {'a': 3, 'b': 2, 'c': 1}
counter.most_common(2) # [('a', 3), ('b', 2)]
五、树与二叉树
5.1 二叉树遍历(必考)
java
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
}
// 前序:根 → 左 → 右
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 + " ");
}
// 层序(BFS,用队列)
public void levelOrder(TreeNode root) {
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);
}
}
记忆口诀:前中后序看"根"的位置(前=根在前,中=根在中,后=根在后)。
5.2 二叉搜索树(BST)
性质:左子树 < 根 < 右子树。查找 O(log n)。
java
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) return root;
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
5.3 平衡树
BST 退化成链表就变 O(n)------需要自平衡:
- AVL:严格平衡,旋转多;
- 红黑树:近似平衡,Java TreeMap/TreeSet、HashMap 冲突链表用的就是它。
对比:MySQL 索引用 B+Tree(多叉,减少磁盘 IO);内存里用红黑树。
六、排序算法(面试必考)
6.1 快排(最常用)
python
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + mid + quick_sort(right)
复杂度 :平均 O(n log n),最坏 O(n²)(有序数组 + 选首元素)。不稳定。
工程优化:随机选 pivot、三数取中、小数组用插入排序。
6.2 归并排序(稳定)
python
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(l, r):
result = []
i = j = 0
while i < len(l) and j < len(r):
if l[i] <= r[j]:
result.append(l[i]); i += 1
else:
result.append(r[j]); j += 1
return result + l[i:] + r[j:]
复杂度:O(n log n),稳定,空间 O(n)。
6.3 排序对比
| 算法 | 平均 | 最坏 | 稳定 | 空间 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(n²) | 是 | O(1) |
| 插入 | O(n²) | O(n²) | 是 | O(1) |
| 选择 | O(n²) | O(n²) | 否 | O(1) |
| 快排 | O(n log n) | O(n²) | 否 | O(log n) |
| 归并 | O(n log n) | O(n log n) | 是 | O(n) |
| 堆排 | O(n log n) | O(n log n) | 否 | O(1) |
工程结论:语言内置排序(Java Arrays.sort / Python sorted)都经过优化,直接调;面试考原理和手写快排/归并。
七、查找算法
7.1 二分查找(要求有序)
java
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防溢出
if (nums[mid] == target) return mid;
if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
细节(面试踩坑点):
left <= right还是<:看区间定义;mid = left + (right - left) / 2:防(left+right)溢出;- 变体:找左边界、右边界、第一个 ≥ target 的位置。
7.2 实战:旋转数组找最小值
java
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) left = mid + 1;
else right = mid;
}
return nums[left];
}
八、动态规划(DP)
8.1 什么时候用 DP
- 求最值(最大、最小、最少次数);
- 有重叠子问题(递归会重复算);
- 有最优子结构(子问题最优能推出整体最优)。
三要素:状态定义、转移方程、初始条件。
8.2 经典入门:斐波那契
python
# 朴素递归:O(2^n),指数爆炸
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
# DP(自底向上):O(n)
def fib_dp(n):
if n <= 1: return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
空间优化(滚动数组):只需要两个变量,O(1) 空间。
8.3 经典:爬楼梯(LeetCode 70)
问题:n 阶楼梯,每次爬 1 或 2 阶,有多少种爬法?
状态 :dp[i] = 爬到第 i 阶的方法数。
转移 :dp[i] = dp[i-1] + dp[i-2](最后一步爬 1 阶或 2 阶)。
初始 :dp[1]=1, dp[2]=2。
python
def climb_stairs(n):
if n <= 2: return n
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
8.4 经典:最大子数组和(LeetCode 53)
问题:数组里连续子数组的最大和。
状态 :dp[i] = 以 numsi 结尾的最大子数组和。
转移 :dp[i] = max(nums[i], dp[i-1] + nums[i])(要么从自己开始,要么接前面)。
python
def max_subarray(nums):
max_sum = cur = nums[0]
for x in nums[1:]:
cur = max(x, cur + x) # 状态转移
max_sum = max(max_sum, cur)
return max_sum
8.5 经典:0-1 背包
问题:容量 W 的背包,n 个物品(重量 wi,价值 vi),最多装多少价值?
状态 :dp[j] = 容量 j 能装的最大价值。
转移 :dp[j] = max(dp[j], dp[j-w[i]] + v[i])(逆序遍历防重复用)。
python
def knapsack(W, weights, values):
dp = [0] * (W + 1)
for i in range(len(weights)):
for j in range(W, weights[i] - 1, -1): # 逆序!
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[W]
为什么逆序:正序会让同一个物品被多次放入(变成完全背包)。
九、算法思想总结
9.1 五大常用思想
| 思想 | 核心 | 典型 |
|---|---|---|
| 贪心 | 每步选当前最优 | 零钱兑换(某些)、区间调度 |
| 分治 | 拆小 → 解决 → 合并 | 归并排序、快排 |
| 回溯 | 试探 + 回退 | 全排列、N 皇后 |
| DP | 状态 + 转移 | 背包、最长公共子序列 |
| 双指针 | 一快一慢/左右夹逼 | 两数之和、链表判环 |
9.2 实战:双指针(有序数组两数之和)
python
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target: return [left, right]
if s < target: left += 1
else: right -= 1
return [-1, -1]
O(n) 时间,O(1) 空间------比哈希更省空间。
十、刷题路线建议
按顺序刷(LeetCode Hot 100):
- 数组/字符串(两数之和、盛水容器、最长子串);
- 链表(反转、环、合并);
- 树(遍历、最大深度、公共祖先);
- 二分/双指针;
- 栈队列(有效括号、滑动窗口);
- DP(爬楼梯、打家劫舍、背包);
- 图(BFS/DFS、岛屿数量)。
方法论:
- 每道题先想暴力解,再说优化;
- 会写代码 ≠ 会讲------面试要边说边写;
- 高频题刷 3 遍(今天、三天后、一周后);
- 归纳套路:看到"最值+可拆子问题"想 DP,看到"有序+查找"想二分。
本章小结
数据结构与算法的核心不是背题,是建立复杂度意识和套路库:数组链表选型、哈希空间换时间、树的分治思想、DP 的状态转移、双指针优化。把本文的十几道经典题吃透,再按路线刷 Hot 100,面试的算法关就稳了。
五篇 CSDN 热门主题文档全部完成:Vue3、PyTorch、Elasticsearch、设计模式、数据结构与算法------覆盖前端、AI、搜索、代码设计、算法五大方向。
(全文完)