算法
双指针
public int[] twoSumSorted(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) { // ⚠️ 注意:不是 <=
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{left, right};
} else if (sum < target) {
left++; // 和太小,左指针右移
} else {
right--; // 和太大,右指针左移
}
}
return new int[0]; // 无解
}
public int minWindow(String s, String t) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) need.merge(c, 1, Integer::sum);
int left = 0, valid = 0; // valid: 窗口中满足need条件的字符种类数
int start = 0, minLen = Integer.MAX_VALUE;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 1. 右边界字符入窗
if (need.containsKey(c)) {
window.merge(c, 1, Integer::sum);
if (window.get(c).equals(need.get(c))) valid++;
}
// 2. 当窗口合法时,尝试收缩左边界
while (valid == need.size()) {
// 更新最优解
if (right - left + 1 < minLen) {
start = left;
minLen = right - left + 1;
}
char d = s.charAt(left);
left++;
// 3. 左边界字符出窗
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) valid--;
window.merge(d, -1, Integer::sum);
}
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
模板四步法(务必形成肌肉记忆)
初始化:定义 need(目标)、window(当前窗口)、valid(合法计数器)
右扩:right++,更新 window 和 valid
判断收缩:当 valid == need.size()(或其他合法条件)时进入内层循环
左缩:left++,更新 window 和 valid,同时更新答案
// 示例:寻找链表中点(偶数取左中点)
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) { // ⚠️ 顺序不能反!
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
滑动窗口
public class SlidingWindowTemplate {
public int solve(String s /* or int[] nums */) {
// 【步骤1】初始化数据结构与指针
Map<Character, Integer> window = new HashMap<>(); // 当前窗口状态
// ... 其他辅助变量(如 need, valid, count 等)
int left = 0;
int result = 0; // 或 Integer.MAX_VALUE / "" 等
// 【步骤2】右指针主动扩张
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 更新窗口状态(c 进入窗口)
window.merge(c, 1, Integer::sum);
// ... 更新其他相关状态
// 【步骤3】判断是否需要收缩(关键!)
while (/* 窗口不合法 or 需要尝试更优解 */) {
// 【步骤4】左指针被动收缩
char d = s.charAt(left);
left++;
// 更新窗口状态(d 离开窗口)
window.merge(d, -1, Integer::sum);
// ... 更新其他相关状态
// ⚠️ 答案更新必须在此处(收缩过程中)
// result = Math.min/max(result, ...);
}
// 若题目要求"最长合法窗口",答案也可在外层更新
// result = Math.max(result, right - left + 1);
}
return result;
}
}
模板核心原则
右指针永远主动前进:for 循环驱动,保证遍历完整数据。
左指针仅在被需要时后退:由 while 条件触发,维持窗口合法性或探索更优解。
状态更新对称:入窗和出窗操作必须成对出现,且逻辑镜像。
答案更新位置决定语义:
求最小值(如最短子串)→ 在 while 内部更新(每次收缩都可能更优)
求最大值(如最长无重复)→ 可在 while 外部更新(扩张后窗口更大)
表格
模式 适用场景 状态变量 示例题目
计数器模式 字符频次、元素个数 Map/Array + valid/count #76 最小覆盖子串、#438 字母异位词
集合模式 唯一性、存在性 Set + size #3 无重复最长子串
数值模式 和、积、乘积 sum/product + threshold #209 长度最小子数组、#560 和为K子数组(前缀和变体)ASCII/小写字母场景务必用数组替代HashMap。int[128] 比 HashMap<Character, Integer> 快3-5倍,且避免自动装箱开销。面试时主动提及此优化体现工程素养。
public int lengthOfLongestSubstring(String s) {
int[] window = new int[128]; // ASCII数组优化
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
window[c]++;
// 收缩条件:当前字符重复
while (window[c] > 1) {
window[s.charAt(left++)]
--;
}
// 最大值在收缩后更新(此时窗口一定合法)
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
public String minWindow(String s, String t) {
int[] need = new int[128], window = new int[128];
int required = 0; // t中不同字符种类数
for (char c : t.toCharArray()) {
if (need[c] == 0) required++;
need[c]++;
}
int left = 0, valid = 0;
int start = 0, minLen = Integer.MAX_VALUE;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (need[c] > 0) {
window[c]++;
if (window[c] == need[c]) valid++;
}
// 收缩条件:已覆盖所有字符
while (valid == required) {
// 最小值必须在收缩内部更新
if (right - left + 1 < minLen) {
start = left;
minLen = right - left + 1;
}
char d = s.charAt(left++);
if (need[d] > 0) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
回溯算法
class BacktrackTemplate {
private List<List<Integer>> result = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>(); // ⚠️ 必须用LinkedList支持O(1)移除末尾
public List<List<Integer>> solve(int[] nums) {
backtrack(nums, 0);
return result;
}
private void backtrack(int[] nums, int startIndex) {
// 【要素1】终止条件:收集合法路径
if (/* 满足题目要求 */) {
result.add(new ArrayList<>(path)); // ⚠️ 必须new副本!
return; // 注意:是否return取决于是否需要继续搜索
}
// 【要素2】选择列表:从startIndex开始避免重复组合
for (int i = startIndex; i < nums.length; i++) {
// 【要素3】剪枝:提前排除无效分支(关键优化点)
if (/* 不满足约束 */) continue;
// 【要素4】做选择
path.add(nums[i]);
// 【要素5】递归进入下一层
backtrack(nums, i + 1); // 组合用i+1;排列用0+used数组
// 【要素6】撤销选择(回溯核心)
path.removeLast();
}
}
}
🔑 模板核心原则
path 必须用 LinkedList:removeLast() 为 O(1),ArrayList 为 O(N)。
结果收集必须 new ArrayList<>(path):否则所有结果指向同一引用。
startIndex 语义决定问题类型:
i+1 → 组合/子集(元素不可重复使用,顺序无关)
0 + boolean[] used → 排列(元素不可重复使用,顺序有关)
i → 完全背包型组合(元素可重复使用)
剪枝位置在 for 循环内、做选择前:越早剪枝,收益越大。
| 剪枝类型 | 适用场景 | 实现方式 | 效果 |
| :--- | :--- | :--- | :--- |
| 排序+相邻去重 | 含重复元素的组合/子集 | `if (i > start && nums[i] == nums[i-1]) continue;` | 避免重复解 |
| 剩余元素不足 | 组合总和、k个元素组合 | `if (path.size() + (n-i) < k) break;` | 提前终止无效分支 |
| 当前值超限 | 组合总和、目标和 | `if (sum + nums[i] > target) break;` (需排序) | 利用单调性剪枝 |
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
private boolean[] used;
public List<List<Integer>> permute(int[] nums) {
used = new boolean[nums.length];
backtrack(nums);
return res;
}
private void backtrack(int[] nums) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue; // 已使用则跳过
used[i] = true;
path.add(nums[i]);
backtrack(nums);
path.removeLast();
used[i] = false; // 撤销标记
}
}
}
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
private boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums); // ⚠️ 去重必须先排序
used = new boolean[nums.length];
backtrack(nums);
return res;
}
private void backtrack(int[] nums) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
// 去重核心:同层相同元素只选第一个
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
used[i] = true;
path.add(nums[i]);
backtrack(nums);
path.removeLast();
used[i] = false;
}
}
}
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 为数值剪枝做准备
backtrack(candidates, target, 0, 0);
return res;
}
private void backtrack(int[] cands, int target, int sum, int start) {
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < cands.length; i++) {
// 数值剪枝:排序后后续更大,直接break
if (sum + cands[i] > target) break;
path.add(cands[i]);
backtrack(cands, target, sum + cands[i], i); // ⚠️ 传i而非i+1(可重复)
path.removeLast();
}
}
}
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
private void backtrack(int[] nums, int start) {
// 子集:每个节点都是合法解(无需终止条件判断)
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums, i + 1);
path.removeLast();
}
}
}
| 陷阱 | 后果 | 解决方案 |
| :--- | :--- | :--- |
| `path` 用 `ArrayList` | `removeLast` O(N)导致TLE | 始终用 `LinkedList` |
| 结果未拷贝 | 所有结果相同 | `new ArrayList<>(path)` |
| 去重条件写错 | 漏解或多解 | 牢记:`i>start && nums[i]==nums[i-1]`(组合)或 `i>0 && nums[i]==nums[i-1] && !used[i-1]`(排列) |
| 数值剪枝用 `continue` | 未充分利用排序 | 排序后用 `break` |
| 忘记撤销 `used` 标记 | 后续分支全部被阻塞 | 回溯时必须 `used[i]=false` |
先分类再套模板:“这是组合问题,元素不可重复使用,用 startIndex=i+1 模板。”
主动说明剪枝:“我先排序,然后在循环中用 sum+cands[i]>target 剪枝,将指数级复杂度大幅降低。”
解释去重原理:“!used[i-1] 确保同层相同元素只选第一个,避免 [1,1,2] 产生两个 [1,2]。
浙公网安备 33010602011771号