• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
第八天
博客园    首页    新随笔    联系   管理    订阅  订阅

算法

双指针

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]。

  

posted on 2026-08-09 17:32  小白小泡泡  阅读(8)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3