LeetCode进阶算法题解详解

LeetCode进阶算法题解详解

本文深入讲解回溯算法、栈、哈希表和树的经典题目,涵盖全排列、有效括号、计算器、单调栈、最近公共祖先等高频面试题,适合有一定算法基础的同学进阶学习。


目录

  1. 回溯算法
  2. 栈的应用
  3. 哈希表技巧
  4. 树的算法

1. 回溯算法

1.1 全排列 II(Permutations II)

题目描述:给定一个可能包含重复数字的序列,返回所有不重复的全排列。

核心思想

回溯算法 = 深度优先搜索 + 剪枝。通过标记已使用元素和去重逻辑,生成所有不重复的排列。

解题思路

  1. 排序:先对数组排序,方便去重

  2. used数组:标记哪些元素已经被使用

  3. 去重关键

    复制代码
    if (i > 0 && nums[i] == nums[i-1] && !used[i-1])
        continue;
    • 当前元素与前一个相同
    • 前一个元素未被使用(说明在同一层已经处理过)
    • 跳过这个元素,避免重复排列
  4. 回溯三步骤

    • 做选择:used[i] = true; path.push_back(nums[i]);
    • 递归:backtracking(...)
    • 撤销选择:path.pop_back(); used[i] = false;

代码实现

cpp 复制代码
class Solution {
private:
    vector<vector<int>> result; 
    vector<int> path; 
    
    void backtracking(vector<int>& nums, vector<bool>& used) {
        // 终止条件:路径长度等于数组长度
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        
        // 遍历所有可能的选择
        for (int i = 0; i < nums.size(); i++) {
            // 剪枝1:元素已被使用
            if (used[i]) continue;
            
            // 剪枝2:去重逻辑(核心)
            // 同一层中,相同元素只使用一次
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])
                continue;
            
            // 做选择
            used[i] = true;
            path.push_back(nums[i]);
            
            // 递归
            backtracking(nums, used);
            
            // 撤销选择(回溯)
            path.pop_back();
            used[i] = false;
        }
    }
    
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        
        // 排序是去重的前提
        sort(nums.begin(), nums.end());
        
        backtracking(nums, used);
        return result;
    }
};

去重原理图解

复制代码
输入: [1, 1, 2]
排序后: [1, 1, 2]

第一层:
  选1(索引0) -> used[0]=true
    第二层:
      选1(索引1) -> used[1]=true
        第三层:选2 -> [1,1,2] ✓
      选2(索引2) -> [1,2,1] ✓
      
  选1(索引1) -> 剪枝!
    因为 nums[1]==nums[0] 且 !used[0]
    说明同一层已经用过值为1的元素
    
  选2(索引2) -> used[2]=true
    第二层:
      选1(索引0) -> [2,1,1] ✓

复杂度分析

  • 时间复杂度:O(n × n!),共n!个排列,每个排列需要O(n)时间构造
  • 空间复杂度:O(n),递归栈深度为n

关键点总结

  1. 排序是去重的基础
  2. used[i-1] == false 表示同层去重
  3. 回溯模板:选择 → 递归 → 撤销

2. 栈的应用

2.1 有效的括号(Valid Parentheses)

题目描述:判断字符串中的括号是否有效配对。

解题思路

  • 使用栈存储左括号
  • 遇到右括号时,检查栈顶是否匹配
  • 最后栈必须为空

代码实现

cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
        
        for (char ch : s) {
            // 左括号入栈
            if (ch == '(' || ch == '[' || ch == '{') {
                st.push(ch);
            } 
            // 右括号匹配
            else if (ch == ')' || ch == ']' || ch == '}') {
                // 栈空说明没有对应的左括号
                if (st.empty()) {
                    return false;
                }
                
                char c = st.top();
                // 检查是否匹配
                if ((ch == ')' && c != '(') || 
                    (ch == ']' && c != '[') ||
                    (ch == '}' && c != '{')) {
                    return false;
                }
                st.pop();
            } 
            // 非括号字符,返回false
            else {
                return false;
            }
        }
        
        // 栈必须为空
        return st.empty();
    }
};

优化版本(更简洁)

cpp 复制代码
bool isValid(string s) {
    stack<char> st;
    unordered_map<char, char> pairs = {
        {')', '('}, {']', '['}, {'}', '{'}
    };
    
    for (char ch : s) {
        if (pairs.count(ch)) {
            // 右括号
            if (st.empty() || st.top() != pairs[ch]) {
                return false;
            }
            st.pop();
        } else {
            // 左括号
            st.push(ch);
        }
    }
    return st.empty();
}

时间复杂度 :O(n)
空间复杂度:O(n)


2.2 基本计算器 II(Basic Calculator II)

题目描述:实现一个基本的计算器,支持加减乘除。

解题思路

  1. 使用栈保存中间结果
  2. 维护一个preSign记录前一个运算符
  3. 遇到新的运算符或到达末尾时,根据preSign处理当前数字:
    • +:直接入栈
    • -:负数入栈
    • *:与栈顶相乘
    • /:与栈顶相除
  4. 最后求栈中所有数字之和

代码实现

cpp 复制代码
class Solution {
public:
    int calculate(string s) {
        vector<int> stk;
        char preSign = '+';  // 初始化为'+'
        int num = 0;
        int n = s.length();
        
        for (int i = 0; i < n; ++i) {
            // 构建多位数字
            if (isdigit(s[i])) {
                num = num * 10 + (s[i] - '0');
            }
            
            // 遇到运算符或到达末尾,处理前面的数字
            if ((!isdigit(s[i]) && s[i] != ' ') || i == n - 1) {
                switch (preSign) {
                    case '+':
                        stk.push_back(num);
                        break;
                    case '-':
                        stk.push_back(-num);
                        break;
                    case '*':
                        stk.back() *= num;
                        break;
                    case '/':
                        stk.back() /= num;
                        break;
                }
                preSign = s[i];  // 更新运算符
                num = 0;         // 重置数字
            }
        }
        
        // 累加栈中所有元素
        return accumulate(stk.begin(), stk.end(), 0);
    }
};

执行过程示例

复制代码
输入: "3+2*2"

i=0: '3' -> num=3
i=1: '+' -> preSign='+', stk=[3], preSign='+', num=0
i=2: '2' -> num=2
i=3: '*' -> preSign='+', stk=[3,2], preSign='*', num=0
i=4: '2' -> num=2, 末尾处理 -> preSign='*', stk=[3,4]

结果: 3+4=7

时间复杂度 :O(n)
空间复杂度:O(n)

关键点

  • 乘除法立即计算(修改栈顶)
  • 加减法延迟计算(入栈)
  • 处理多位数字和空格

2.3 每日温度(Daily Temperatures)

题目描述:给定温度列表,返回每天需要等待多少天才会有更高温度。

解题思路

  • 使用单调栈(维护递减序列)
  • 栈中存储索引
  • 当前温度大于栈顶温度时,说明找到了更高温度
  • 计算天数差并出栈

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        stack<int> st;  // 单调栈,存储索引
        vector<int> result(temperatures.size(), 0);
        
        for (int i = 0; i < temperatures.size(); i++) {
            // 当前温度大于栈顶索引对应的温度
            while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
                int idx = st.top();
                result[idx] = i - idx;  // 计算天数差
                st.pop();
            }
            st.push(i);  // 当前索引入栈
        }
        
        return result;
    }
};

执行过程示例

复制代码
输入: [73, 74, 75, 71, 69, 72, 76, 73]

i=0: 73 -> st=[0]
i=1: 74>73 -> result[0]=1, st=[1]
i=2: 75>74 -> result[1]=1, st=[2]
i=3: 71<75 -> st=[2,3]
i=4: 69<71 -> st=[2,3,4]
i=5: 72>69>71 -> result[4]=1, result[3]=2, st=[2,5]
i=6: 76>72>75 -> result[5]=1, result[2]=4, st=[6]
i=7: 73<76 -> st=[6,7]

结果: [1, 1, 4, 2, 1, 1, 0, 0]

时间复杂度 :O(n),每个元素最多入栈出栈一次
空间复杂度:O(n)

单调栈应用场景

  • 下一个更大/更小元素
  • 柱状图最大矩形
  • 接雨水问题

3. 哈希表技巧

3.1 砖墙(Brick Wall)

题目描述:在砖墙中画一条垂直线,使穿过的砖块数量最少。

解题思路

  1. 最少穿过的砖块 = 总行数 - 最多经过的缝隙
  2. 使用哈希表统计每个位置的缝隙数
  3. 找出缝隙最多的位置

关键点

  • 不统计最右边的缝隙(边界)
  • 使用前缀和定位缝隙位置

代码实现

cpp 复制代码
class Solution {
public:
    int leastBricks(vector<vector<int>>& wall) {
        unordered_map<int, int> cnt;  // 位置 -> 缝隙数
        
        // 遍历每一行
        for (auto& widths : wall) {
            int n = widths.size();
            int sum = 0;  // 当前位置(前缀和)
            
            // 统计每个缝隙位置(不包括最后)
            for (int i = 0; i < n - 1; i++) {
                sum += widths[i];
                cnt[sum]++;
            }
        }
        
        // 找出缝隙最多的位置
        int maxCnt = 0;
        for (auto& [pos, c] : cnt) {
            maxCnt = max(maxCnt, c);
        }
        
        // 总行数 - 最多缝隙数 = 最少穿过砖块数
        return wall.size() - maxCnt;
    }
};

图解示例

复制代码
输入:
[[1,2,2,1],
 [3,1,2],
 [1,3,2],
 [2,4],
 [3,1,2],
 [1,3,1,1]]

缝隙位置统计:
位置1: 2行有缝隙 (行0, 行2)
位置3: 3行有缝隙 (行1, 行4, 行5)
位置4: 4行有缝隙 (行0, 行2, 行3, 行5) <- 最多
位置5: 2行有缝隙 (行1, 行4)

结果: 6 - 4 = 2

时间复杂度 :O(n × m),n为行数,m为平均砖块数
空间复杂度:O(k),k为不同缝隙位置数

优化思路

  • 使用哈希表统计频率
  • 前缀和定位缝隙
  • 转化为求最大值问题

4. 树的算法

4.1 二叉树的最近公共祖先(Lowest Common Ancestor)

题目描述:找到二叉树中两个节点的最近公共祖先。

核心思想

后序遍历(左右根),从下往上返回信息。

解题思路

  1. 终止条件

    • 遇到空节点:返回null
    • 遇到p或q:返回当前节点
  2. 递归过程

    • 在左子树中查找
    • 在右子树中查找
  3. 返回逻辑

    • 左右子树都找到:当前节点是LCA
    • 只有一边找到:返回那一边的结果
    • 都没找到:返回null

代码实现

cpp 复制代码
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 终止条件:空节点或找到目标节点
        if (!root || root == p || root == q) {
            return root;
        }
        
        // 在左右子树中递归查找
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        
        // 情况1: 左右子树都找到了,说明p和q分别在两侧
        if (left && right) {
            return root;
        }
        
        // 情况2: 只有一边找到,返回非空的那一边
        // 可能是LCA在某一侧,也可能是p/q之一是另一个的祖先
        return left ? left : right;
    }
};

图解示例

复制代码
        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4

查找 p=5, q=1 的LCA:

后序遍历过程:
1. 访问左子树(5): 找到p,返回5
2. 访问右子树(1): 找到q,返回1
3. 根节点(3): left=5, right=1, 都非空 -> 返回3

查找 p=5, q=4 的LCA:

后序遍历过程:
1. 访问节点5的左子树(6): 返回null
2. 访问节点5的右子树(2):
   - 左子树(7): 返回null
   - 右子树(4): 找到q,返回4
   - 节点2: left=null, right=4 -> 返回4
3. 节点5: left=null, right=4 -> 返回4
4. 根节点3: left=4, right=null -> 返回4

等等,上面有误,让我重新分析...

正确过程:
1. lowestCommonAncestor(5, 5, 4):
   - root==p,直接返回5
2. lowestCommonAncestor(3, 5, 4):
   - left = lowestCommonAncestor(5, 5, 4) = 5
   - right = lowestCommonAncestor(1, 5, 4) = null
   - 返回 left = 5

算法正确性证明

情况1:p和q在root的两侧

  • 左子树返回p(或p的祖先)
  • 右子树返回q(或q的祖先)
  • root是LCA ✓

情况2:p和q都在root的左侧

  • 左子树返回LCA
  • 右子树返回null
  • 返回左子树的结果 ✓

情况3:p是q的祖先(或反之)

  • 先遇到的节点直接返回
  • 这个节点就是LCA ✓

时间复杂度 :O(n),最坏情况遍历所有节点
空间复杂度:O(h),h为树高,递归栈空间

关键点总结

  1. 后序遍历,从下往上返回信息
  2. 遇到目标节点立即返回
  3. 根据左右子树返回值判断LCA位置

总结

本文涵盖了四大类进阶算法:

回溯算法

  • 核心:选择 → 递归 → 撤销
  • 去重:排序 + used数组
  • 应用:全排列、组合、子集

栈的应用

  • 括号匹配:栈的经典应用
  • 计算器:运算符优先级处理
  • 单调栈:寻找下一个更大/更小元素

哈希表技巧

  • 频率统计:快速查找和计数
  • 前缀和:累加定位
  • 空间换时间:O(1)查找

树的算法

  • 后序遍历:从下往上返回信息
  • 递归三要素:终止条件、递归逻辑、返回值
  • LCA:利用递归返回值判断

学习建议

  1. 理解本质:不要死记硬背,理解算法原理
  2. 画图分析:复杂问题用图示推演过程
  3. 变式练习:掌握一题多解,举一反三
  4. 复杂度分析:养成分析时空复杂度的习惯
  5. 代码规范:注意边界条件和代码可读性

相关题目推荐

回溯

  • 全排列 I/II
  • 组合总和 I/II/III
  • 子集 I/II
  • N皇后

  • 最小栈
  • 柱状图最大矩形
  • 接雨水
  • 逆波兰表达式

哈希表

  • 两数之和
  • 字母异位词分组
  • 最长连续序列

  • 二叉树的序列化与反序列化
  • 路径总和 I/II/III
  • 验证二叉搜索树

持续练习,定期复习,祝大家刷题顺利!🚀

相关推荐
让我们一起加油好吗3 小时前
【基础算法】DFS中的剪枝与优化
算法·深度优先·剪枝
Q741_1473 小时前
C++ 模拟题 力扣495. 提莫攻击 题解 每日一题
c++·算法·leetcode·模拟
我命由我123454 小时前
Excel - Excel 列出一列中所有不重复数据
经验分享·学习·职场和发展·word·powerpoint·excel·职场发展
Felven4 小时前
A. Be Positive
算法
小O的算法实验室4 小时前
2026年COR SCI2区,自适应K-means和强化学习RL算法+有效疫苗分配问题,深度解析+性能实测,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
青岛少儿编程-王老师5 小时前
CCF编程能力等级认证GESP—C++7级—20250927
数据结构·c++·算法
夏鹏今天学习了吗5 小时前
【LeetCode热题100(39/100)】对称二叉树
算法·leetcode·职场和发展
天选之女wow6 小时前
【代码随想录算法训练营——Day34】动态规划——416.分割等和子集
算法·leetcode·动态规划
Boop_wu7 小时前
[数据结构] 哈希表
算法·哈希算法·散列表