💼 数据结构与算法面试核心考点精要
高频题型 · 代码模板 · 复杂度分析 · 避坑指南
🔑 一、数组与字符串
1.1 双指针技术
模板:左右指针(相向移动)
java
int left = 0, right = arr.length - 1;
while (left < right) {
if (condition) {
// 处理逻辑
left++;
right--;
} else if (arr[left] < target) {
left++;
} else {
right--;
}
}
适用场景 :两数之和、盛水最多容器、回文判断
时间复杂度 :O(n)
空间复杂度:O(1)
模板:快慢指针(同向移动)
java
int slow = 0;
for (int fast = 0; fast < arr.length; fast++) {
if (condition(arr[fast])) {
arr[slow++] = arr[fast];
}
}
return slow; // 新数组长度
适用场景 :移除元素、删除排序数组重复项
关键点 :slow 指向待写入位置,fast 遍历原数组
1.2 滑动窗口
模板:固定窗口大小
java
int left = 0, sum = 0, maxSum = Integer.MIN_VALUE;
for (int right = 0; right < arr.length; right++) {
sum += arr[right];
if (right - left + 1 == k) { // 窗口大小达到k
maxSum = Math.max(maxSum, sum);
sum -= arr[left++]; // 滑动窗口
}
}
模板:可变窗口大小(含哈希表)
java
Map<Character, Integer> window = new HashMap<>();
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
window.put(c, window.getOrDefault(c, 0) + 1);
while (window.get(c) > 1) { // 收缩条件
char d = s.charAt(left++);
window.put(d, window.get(d) - 1);
}
maxLen = Math.max(maxLen, right - left + 1);
}
适用场景 :无重复字符最长子串、最小覆盖子串
时间复杂度:O(n) --- 每个元素最多进出窗口一次
🔑 二、链表
2.1 基础操作模板
反转链表(迭代)
java
public ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next; // 保存下一节点
curr.next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
return prev;
}
时间复杂度 :O(n)
空间复杂度:O(1)
反转链表(递归)
java
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseList(head.next);
head.next.next = head; // 反转当前节点
head.next = null; // 避免环
return newHead;
}
2.2 快慢指针高级应用
检测环(Floyd判圈算法)
java
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
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;
}
寻找环入口
java
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 相遇
slow = head; // 慢指针回到起点
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow; // 环入口
}
}
return null;
}
数学原理 :设环外长度a,环内相遇点距入口b,环长c。相遇时:
2(a+b) = a + b + nc → a = (n-1)c + (c-b)
即从起点和相遇点同时走,必在入口相遇
🔑 三、栈与队列
3.1 单调栈(Monotonic Stack)
模板:寻找右侧第一个更大元素
java
public int[] nextGreaterElement(int[] nums) {
int[] result = new int[nums.length];
Arrays.fill(result, -1);
Deque<Integer> stack = new ArrayDeque<>(); // 存储索引
for (int i = 0; i < nums.length; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
int idx = stack.pop();
result[idx] = nums[i];
}
stack.push(i);
}
return result;
}
适用场景 :柱状图最大矩形、每日温度
时间复杂度:O(n) --- 每个元素入栈出栈各一次
3.2 双端队列实现滑动窗口最大值
java
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k <= 0) return new int[0];
int[] result = new int[nums.length - k + 1];
Deque<Integer> deque = new ArrayDeque<>(); // 存储索引,队首为最大值索引
for (int i = 0; i < nums.length; i++) {
// 移除队首过期元素
if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 维护单调递减队列:移除队尾小于当前元素的索引
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 窗口形成后记录结果
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
时间复杂度 :O(n)
空间复杂度:O(k)
🔑 四、二叉树
4.1 遍历模板
前序遍历(迭代)
java
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null) stack.push(node.right); // 右先入栈
if (node.left != null) stack.push(node.left); // 左后入栈
}
return result;
}
中序遍历(迭代)
java
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
while (curr != null) {
stack.push(curr);
curr = curr.left; // 一路向左
}
curr = stack.pop();
result.add(curr.val);
curr = curr.right; // 转向右子树
}
return result;
}
4.2 二叉搜索树(BST)验证
java
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return validate(node.left, min, node.val) &&
validate(node.right, node.val, max);
}
关键点 :不能仅比较父子节点,需传递上下界约束
时间复杂度 :O(n)
空间复杂度:O(h),h为树高
🔑 五、哈希表
5.1 LRU 缓存实现
java
class LRUCache {
private final int capacity;
private final Map<Integer, Node> cache;
private final Node head, tail; // 双向链表哑节点
public LRUCache(int capacity) {
this.capacity = capacity;
cache = new HashMap<>();
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!cache.containsKey(key)) return -1;
Node node = cache.get(key);
remove(node);
add(node);
return node.value;
}
public void put(int key, int value) {
if (cache.containsKey(key)) {
remove(cache.get(key));
} else if (cache.size() >= capacity) {
cache.remove(head.next.key);
remove(head.next);
}
add(new Node(key, value));
}
private void add(Node node) {
Node prev = tail.prev;
prev.next = node;
node.prev = prev;
node.next = tail;
tail.prev = node;
cache.put(node.key, node);
}
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
cache.remove(node.key);
}
static class Node {
int key, value;
Node prev, next;
Node(int k, int v) { key = k; value = v; }
}
}
时间复杂度 :get/put 均为 O(1)
空间复杂度:O(capacity)
🔑 六、堆与优先队列
6.1 Top K 问题模板
求前K大元素(小顶堆)
java
public int[] topKFrequent(int[] nums, int k) {
// 1. 统计频率
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) freq.put(num, freq.getOrDefault(num, 0) + 1);
// 2. 小顶堆维护Top K
PriorityQueue<Map.Entry<Integer, Integer>> heap =
new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());
for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
heap.offer(entry);
if (heap.size() > k) heap.poll(); // 移除最小元素
}
// 3. 提取结果
int[] result = new int[k];
int i = 0;
while (!heap.isEmpty()) result[i++] = heap.poll().getKey();
return result;
}
时间复杂度 :O(n log k)
空间复杂度:O(n)
🔑 七、动态规划
7.1 背包问题模板
0-1背包(二维DP)
java
public int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= capacity; j++) {
dp[i][j] = dp[i - 1][j]; // 不选第i个物品
if (j >= weights[i - 1]) {
dp[i][j] = Math.max(
dp[i][j],
dp[i - 1][j - weights[i - 1]] + values[i - 1] // 选第i个物品
);
}
}
}
return dp[n][capacity];
}
0-1背包(空间优化:一维DP)
java
public int knapsackOptimized(int[] weights, int[] values, int capacity) {
int[] dp = new int[capacity + 1];
for (int i = 0; i < weights.length; i++) {
// 逆序遍历防止重复选择
for (int j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[capacity];
}
关键点:一维DP必须逆序遍历,避免物品被重复选择
7.2 股票买卖问题状态机
通用模板(含冷冻期)
java
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) return 0;
// dp[i][0]: 持有股票
// dp[i][1]: 不持有(冷冻期)
// dp[i][2]: 不持有(非冷冻期)
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
}
return Math.max(dp[n - 1][1], dp[n - 1][2]);
}
状态转移:
- 持有 = max(前一天持有, 前一天非冷冻期买入)
- 冷冻期 = 前一天持有并卖出
- 非冷冻期 = max(前一天冷冻期, 前一天非冷冻期)
🔑 八、图算法
8.1 BFS最短路径(无权图)
java
public int shortestPath(int[][] graph, int start, int target) {
if (start == target) return 0;
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
steps++;
for (int i = 0; i < size; i++) {
int node = queue.poll();
for (int neighbor : graph[node]) {
if (neighbor == target) return steps;
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
return -1; // 不可达
}
8.2 DFS回溯模板
java
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] nums, int start, List<Integer> path,
List<List<Integer>> result) {
result.add(new ArrayList<>(path)); // 每个节点都是解
for (int i = start; i < nums.length; i++) {
path.add(nums[i]); // 做选择
backtrack(nums, i + 1, path, result); // 递归
path.remove(path.size() - 1); // 撤销选择
}
}
关键点:
path传递引用需深拷贝加入结果集start参数避免重复选择(组合问题)- 排列问题需用
visited数组标记
📊 复杂度速查表
| 数据结构/算法 | 时间复杂度(平均) | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 数组访问 | O(1) | O(1) | 随机访问 |
| 链表遍历 | O(n) | O(1) | 顺序访问、频繁插入删除 |
| 哈希表 | O(1) | O(n) | 快速查找、去重 |
| 二叉搜索树 | O(log n) | O(n) | 有序数据、范围查询 |
| 堆 | O(log n)插入/删除 O(1)查最值 | O(n) | Top K、优先队列 |
| 快排 | O(n log n) | O(log n) | 通用排序 |
| 归并排序 | O(n log n) | O(n) | 稳定排序、链表排序 |
| BFS | O(V+E) | O(V) | 最短路径(无权图) |
| DFS | O(V+E) | O(V) | 连通性、回溯 |
| 动态规划 | O(n²)~O(n) | O(n²)~O(n) | 最优子结构问题 |
⚠️ 高频边界陷阱清单
| 问题类型 | 边界条件 | 检查点 |
|---|---|---|
| 数组 | 空数组、单元素 | `if (arr == null |
| 二分查找 | 溢出、死循环 | mid = left + (right - left) / 2 while (left <= right) vs while (left < right) |
| 链表 | 空链表、单节点 | `if (head == null |
| 递归 | 栈溢出 | 深度>1000考虑迭代,尾递归优化 |
| 整数运算 | 溢出 | (a + b) / 2 → a + (b - a) / 2 乘法前转 long |
| 字符串 | 空串、单字符 | `if (s == null |
| 树遍历 | 空树 | if (root == null) return ... |
| DP初始化 | 无效状态标记 | 用 Integer.MAX_VALUE / 2 避免加法溢出 |
🚀 面试编码 Checklist
✅ 开始编码前
- 明确输入输出类型及范围
- 询问边界条件处理方式(空值/负数/溢出)
- 口头描述1-2种解法并分析复杂度
- 确认最优解法后再编码
✅ 编码过程中
- 变量命名清晰(避免a/b/c)
- 关键步骤添加简短注释
- 先实现主干逻辑,再处理边界
- 每写5行代码自测一个小用例
✅ 编码完成后
- 用简单用例走查代码(含边界)
- 明确说明时间/空间复杂度
- 主动提出可能的优化方向
- 询问面试官是否需要改进
注 :本文所有代码模板均经过 LeetCode 核心题验证,可直接用于面试实战。建议针对每类题型手写3遍以上形成肌肉记忆。
更新日期:2026年2月14日