编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • 数据结构与算法专栏
  • 基础认知

  • 线性结构

  • 树与哈希

  • 工业级实现

  • 算法思想

  • 实战与综合

  • 算法题考核

    • README
    • 数组算法题合集
      • A001 两数之和(Two Sum)
        • 01. 题目描述
        • 02. 题目分析:把直觉变成算法
        • 03. 解法一:暴力枚举(O(N²))
        • 04. 解法二:哈希表(O(N),最优)
        • 05. 三解法对比与选型
        • 06. 深入思考:为什么哈希表是 O(1)?
        • 07. 总结
      • A002 三数之和(3Sum)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 解法一:排序+双指针
        • 04. 解法对比
        • 05. 总结
      • A003 最接近的三数之和(3Sum Closest)
        • 01. 题目描述
        • 02. 题目分析:从"等号"到"绝对值"
        • 03. 代码
        • 04. 总结
      • A004 四数之和(4Sum)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
      • A005 盛最多水的容器(Container With Most Water)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 动态演示
        • 04. 代码
        • 05. 盛水容器 vs 接雨水
        • 06. 总结
      • A006 接雨水(Trapping Rain Water)
        • 01. 题目描述
        • 02. 题目分析:把直觉变成公式
        • 03. 解法一:动态规划(预处理最大值)
        • 04. 解法二:单调栈(按行填坑)
        • 05. 解法三:双指针(空间 O(1) 的最优解)
        • 06. 三解法对比与选型
        • 07. 总结
      • A007 移动零(Move Zeroes)
        • 01. 题目描述
        • 02. 题目分析:三种写法的演变
        • 03. 解法对比
        • 04. 代码
        • 05. 与其他双指针题的对比
        • 06. 总结
      • A008 删除有序数组中的重复项(Remove Duplicates)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 双指针题型总结
      • A009 删除有序数组中的重复项 II
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
      • A010 合并两个有序数组(Merge Sorted Array)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
      • A011 颜色分类(荷兰国旗问题)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
      • A012 最大子数组和(Maximum Subarray)
        • 01. 题目描述
        • 02. 题目分析:最优子结构的诞生
        • 03. 解法一:动态规划(Kadane 算法,O(N)/O(1))
        • 04. 解法二:分治(O(NlogN)/O(logN))
        • 05. 两解法对比
        • 06. 总结
      • A013 乘积最大子数组(Maximum Product Subarray)
        • 01. 题目描述
        • 02. 题目分析:为什么不能直接套 Kadane?
        • 03. 解法一:双变量 DP(O(N)/O(1))
        • 04. 解法二:双向扫描(选读)
        • 05. 两解法对比
        • 06. A012 vs A013 对比
        • 07. 总结
      • A014 和为 K 的子数组(Subarray Sum Equals K)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
      • A015 长度最小的子数组(Minimum Size Subarray Sum)
        • 01. 题目描述
        • 02. 题目分析:两种思考路径
        • 03. 解法一:滑动窗口(O(N)/O(1))
        • 04. 解法二:前缀和 + 二分(O(NlogN),选读)
        • 05. 滑动窗口 vs 其他题型
        • 06. 总结
      • A016 无重复字符的最长子串
        • 01. 题目
        • 02. 题目分析
        • 03. 解法演进
        • 04. 代码
        • 05. 手推演示
      • A017 找到字符串中所有字母异位词
        • 01. 题目
        • 02. 题目分析
        • 03. 解法一:朴素滑动窗口(O(N·26))
        • 04. 解法二:diff 计数优化(O(N))
        • 05. 总结
      • A018 最小覆盖子串(Minimum Window Substring)
        • 01. 题目描述
        • 02. 题目分析:从朴素到最优
        • 03. 代码
        • 04. 滑动窗口题型总览
        • 05. 总结
      • A019 字符串相乘(Multiply Strings)
        • 01. 题目
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
      • A020 字符串转换整数 (atoi)
        • 01. 题目描述
        • 02. 题目分析
        • 03. 代码
        • 04. 总结
    • 链表算法题合集
    • 栈队列算法题合集
    • 哈希算法题合集
    • 二叉树算法题合集
    • 堆算法题合集
    • 图算法题合集
    • 二分查找算法题合集
    • 双指针算法题合集
    • 排序算法题合集
    • 分治算法题合集
    • 贪心算法题合集
    • 回溯算法题合集
    • 动规算法题合集
    • 位运算算法题合集
    • 数学算法题合集
  • 算法
  • 算法题考核
杨充
2021-05-10
目录

数组算法题合集

# A001 两数之和(Two Sum)

LeetCode 1 · ⭐ · 哈希映射

LeetCode 第一题,面试出场率最高的题目。看似简单,却能考察哈希表的理解深度——从 O(N²) 到 O(N) 的思维跃迁。

# 01. 题目描述

给定整数数组 nums 和整数目标值 target,在该数组中找出和为目标值的两个整数,返回它们的数组下标。每种输入只会对应一个答案,且同一元素不能使用两次。

示例:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:nums[0] + nums[1] = 2 + 7 = 9
1
2
3

# 02. 题目分析:把直觉变成算法

2.1 核心公式

对每个元素 x,需要找另一个元素 y:

$$y = target - x$$

问题转化为:快速判断 target - x 是否已经在之前遍历过的元素中出现过。

2.2 朴素做法

双层循环,对每个元素扫描后面的所有元素。

  • 时间:O(N²)——每个元素都要扫描剩余部分
  • 空间:O(1)

N ≤ 10⁴ 时 O(N²) ≈ 10⁸ 次操作,可能超时(OJ 要求通常 < 10⁸)。

2.3 优化方向:空间换时间

观察:每次都需要"回头找"之前遍历过的一个值。这恰好是哈希表的天然应用场景——O(1) 查找。

2.4 解法演进

flowchart LR
    A["朴素 O(N²)"] -->|空间换时间| B["哈希表 O(N)"]
    B --> C["一次遍历:边查边存"]
    C --> D["最优解:O(N)/O(N)"]
1
2
3
4

# 03. 解法一:暴力枚举(O(N²))

public int[] twoSum(int[] nums, int target) {
    for (int i = 0; i < nums.length; i++)
        for (int j = i + 1; j < nums.length; j++)
            if (nums[i] + nums[j] == target)
                return new int[]{i, j};
    return new int[0];
}
1
2
3
4
5
6
7

复杂度:时间 O(N²),空间 O(1)。数据量稍大就无法通过。

# 04. 解法二:哈希表(O(N),最优)

4.1 思路

遍历数组时,将每个元素的值和下标存入哈希表。对当前元素 x,O(1) 查找 target - x 是否已在表中。

flowchart TD
    A["i=0, x=2"] --> B["查 9-2=7 在表中?否"]
    B --> C["存 {2:0}"]
    C --> D["i=1, x=7"]
    D --> E["查 9-7=2 在表中?是!"]
    E --> F["返回 [0,1]"]
1
2
3
4
5
6

4.2 代码

Java:

public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int y = target - nums[i];
        if (map.containsKey(y))
            return new int[]{map.get(y), i};
        map.put(nums[i], i);
    }
    return new int[0];
}
1
2
3
4
5
6
7
8
9
10

Python:

def twoSum(self, nums: List[int], target: int) -> List[int]:
    seen = {}
    for i, x in enumerate(nums):
        y = target - x
        if y in seen:
            return [seen[y], i]
        seen[x] = i
1
2
3
4
5
6
7

C++:

vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int, int> mp;
    for (int i = 0; i < nums.size(); i++) {
        int y = target - nums[i];
        if (mp.count(y)) return {mp[y], i};
        mp[nums[i]] = i;
    }
    return {};
}
1
2
3
4
5
6
7
8
9

4.3 复杂度

维度 值
时间 O(N),一次遍历 + O(1) 哈希查找
空间 O(N),哈希表存最多 N 个元素

# 05. 三解法对比与选型

维度 暴力枚举 两遍哈希 一遍哈希(最优)
时间复杂度 O(N²) O(N) O(N)
空间复杂度 O(1) O(N) O(N)
代码行数 ~8 行 ~12 行 ~10 行
适用场景 N < 1000 通用 面试标准答案

选型建议

面试场景:
  先提暴力 O(N²)(展示基本思路)
  然后优化到哈希 O(N)(展示优化能力)
  加分项 → 讨论哈希冲突对复杂度的影响(展示底层理解)
1
2
3
4

# 06. 深入思考:为什么哈希表是 O(1)?

哈希冲突的影响

Java HashMap 在冲突较多时退化为红黑树(链表长度 ≥ 8),查找退化到 O(logN)。

极端情况:如果故意构造所有 key 映射到同一个桶,查找退化为 O(logN),总复杂度 O(NlogN)。

但在随机数据下,均匀分布的哈希函数保证 O(1) 摊还时间。

与两数之和系列的联系

两数之和 O(N) ─── 哈希表
      ↓ 升级
三数之和 O(N²) ─── 排序+双指针
      ↓ 推广
四数之和 O(N³) ─── 排序+双指针+剪枝
      ↓ 泛化
N数之和         ─── 排序+(N-2)循环+双指针
1
2
3
4
5
6
7

# 07. 总结

核心启示 说明
空间换时间 哈希表 O(1) 查找替代 O(N) 扫描
边遍历边查询 不需要预处理,一次遍历同时完成"查"和"存"
哈希方法论 凡是"查找是否存在"的问题,优先想哈希表

相关题目:A002 三数之和、H002 搜索插入位置、I005 两数之和 II

# A002 三数之和(3Sum)

LeetCode 15 · ⭐⭐ · 排序+双指针

从两数之和的哈希思路升级到排序+双指针夹逼——这是处理"多数之和"问题的方法论转折点。去重是这道题的难点。

# 01. 题目描述

判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j, i != k, j != k 且和为 0。返回所有不重复的三元组。

示例:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:不同的三元组是 [-1,-1,2] 和 [-1,0,1]
1
2
3

# 02. 题目分析

2.1 延续两数之和的思路?

两数之和用哈希 O(N),三数之和如果也哈希:

固定 a → 对 b+c = -a 用两数之和的哈希法
1

这样 O(N²) 时间 + O(N) 空间,但去重极其麻烦——哈希表中的组合可能有重复。

2.2 排序+双指针:为何更优?

排序后数组有序,双指针可以从两端夹逼。核心优势:

维度 哈希法 排序+双指针
时间复杂度 O(N²) O(N²)
空间复杂度 O(N) O(logN)(排序栈)
去重难度 复杂 天然有序,跳重即可
扩展性 难以泛化 轻松泛化到 N 数之和

2.3 为什么用排序+双指针而非哈希?

关键原因:有序数组去重只需要跳过相邻相同值——《O(1) 操作》。哈希法需要在结果集里判重,复杂度显著上升。

flowchart TD
    A["排序 O(NlogN)"] --> B["固定 i = 0..n-3"]
    B --> C["双指针 l=i+1, r=n-1 夹逼"]
    C --> D{"sum < 0 ?"}
    D -->|YES| E["l++"]
    D -->|NO| F{"sum > 0 ?"}
    F -->|YES| G["r--"]
    F -->|NO| H["命中!记录+去重"]
    H --> I["l++, r--"]
    I --> C
1
2
3
4
5
6
7
8
9
10

# 03. 解法一:排序+双指针

3.1 流程演示

排序后:[-4, -1, -1, 0, 1, 2]

i=0(-4): l=1(-1), r=5(2)  sum=-3<0 → l++
i=0(-4): l=2(-1), r=5(2)  sum=-3<0 → l++
i=0(-4): l=3(0),  r=5(2)  sum=-2<0 → l++
i=0(-4): l=4(1),  r=5(2)  sum=-1<0 → l++ → l==r 结束

i=1(-1): 与i=0相等 → 跳过(去重)

i=2(-1): l=3(0), r=5(2)   sum=1>0 → r--
i=2(-1): l=3(0), r=4(1)   sum=0 → 命中 [-1,0,1]
         l跳过重复,r跳过重复 → l=5,r=3 → 结束

结果:[[-1,-1,2], [-1,0,1]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3.2 去重三处(核心难点)

位置 条件 含义
固定值 i i>0 && nums[i]==nums[i-1] 同一固定值只处理一次
左指针 l 命中后跳过 == 相邻 同值只记录一次
右指针 r 同上 同上

3.3 剪枝优化

nums[i] > 0 时直接 break——已排序,后面的数都 >0,不可能三数和为 0。

3.4 代码

Java:

public List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    int n = nums.length;
    for (int i = 0; i < n - 2; i++) {
        if (nums[i] > 0) break;                        // 剪枝
        if (i > 0 && nums[i] == nums[i - 1]) continue;  // i去重
        int l = i + 1, r = n - 1;
        while (l < r) {
            int sum = nums[i] + nums[l] + nums[r];
            if (sum == 0) {
                res.add(Arrays.asList(nums[i], nums[l], nums[r]));
                while (l < r && nums[l] == nums[l + 1]) l++; // l去重
                while (l < r && nums[r] == nums[r - 1]) r--; // r去重
                l++; r--;
            } else if (sum < 0) l++;
            else r--;
        }
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Python:

def threeSum(self, nums: List[int]) -> List[List[int]]:
    nums.sort()
    res, n = [], len(nums)
    for i in range(n - 2):
        if nums[i] > 0: break
        if i > 0 and nums[i] == nums[i - 1]: continue
        l, r = i + 1, n - 1
        while l < r:
            s = nums[i] + nums[l] + nums[r]
            if s == 0:
                res.append([nums[i], nums[l], nums[r]])
                while l < r and nums[l] == nums[l + 1]: l += 1
                while l < r and nums[r] == nums[r - 1]: r -= 1
                l += 1; r -= 1
            elif s < 0: l += 1
            else: r -= 1
    return res
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

C++:

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    int n = nums.size();
    for (int i = 0; i < n - 2; i++) {
        if (nums[i] > 0) break;
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int l = i + 1, r = n - 1;
        while (l < r) {
            int s = nums[i] + nums[l] + nums[r];
            if (s == 0) {
                res.push_back({nums[i], nums[l], nums[r]});
                while (l < r && nums[l] == nums[l + 1]) l++;
                while (l < r && nums[r] == nums[r - 1]) r--;
                l++; r--;
            } else if (s < 0) l++;
            else r--;
        }
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

复杂度:时间 O(N²),空间 O(logN)(排序栈)。

# 04. 解法对比

解法 时间 空间 去重难度 推荐度
暴力三重循环 O(N³) O(1) 简单 ❌
哈希 O(N²) O(N) 困难 ❌
排序+双指针 O(N²) O(logN) 简单 ✅

# 05. 总结

核心启示 说明
排序+双指针 将 O(N³) 降到 O(N²),天然去重
三处去重 i/l/r 各一处,缺一不可
剪枝 nums[i] > 0 直接 break
方法论转折 哈希 → 双指针,开辟了"N 数之和"系列

相关题目:A003 最接近的三数之和、A004 四数之和、A001 两数之和

# A003 最接近的三数之和(3Sum Closest)

LeetCode 16 · ⭐⭐ · 排序+双指针

三数之和的孪生题。"等于 0"变成"最接近 target",框架完全一致。此题展示:算法框架不变,只改判断条件即可应对变形题。

# 01. 题目描述

给定整数数组 nums 和目标值 target,找出三个整数使得它们的和与 target 最接近。返回和。假定每组输入恰好一个解。

示例:

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:-1 + 2 + 1 = 2,与 target=1 差值为 1,最接近
1
2
3

# 02. 题目分析:从"等号"到"绝对值"

2.1 A003 vs A002 对比

flowchart LR
    subgraph A002["三数之和 sum==0"]
        A1["sum<0 → l++"]
        A2["sum>0 → r--"]
        A3["sum==0 → 记录+去重"]
    end
    subgraph A003["最接近的三数之和"]
        B1["sum<target → l++"]
        B2["sum>target → r--"]
        B3["每次更新 closest"]
    end
1
2
3
4
5
6
7
8
9
10
11
A002 三数之和 A003 最接近
判定条件 sum == 0 abs(sum-target) 最小
记录时机 仅 ==0 时 每次都更新
提前返回 可(命中 0) 仅 ==target 可返回
去重 三处去重 i 去重即可

2.2 为什么去重需求降低?

A003 只求一个值(最接近的和),不是求所有组合。即使有重复 i 值,结果也不会少——只影响效率。所以只需对 i 去重。

2.3 双指针方向

$$sum < target \rightarrow l\text{++} \quad\text{(需要更大的和)}$$ $$sum > target \rightarrow r\text{--} \quad\text{(需要更小的和)}$$

# 03. 代码

Java:

public int threeSumClosest(int[] nums, int target) {
    Arrays.sort(nums);
    int closest = nums[0] + nums[1] + nums[2];
    int n = nums.length;
    for (int i = 0; i < n - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;   // i去重
        int l = i + 1, r = n - 1;
        while (l < r) {
            int sum = nums[i] + nums[l] + nums[r];
            // 更新最近值
            if (Math.abs(sum - target) < Math.abs(closest - target))
                closest = sum;
            if (sum < target) l++;
            else if (sum > target) r--;
            else return sum;                              // 精确命中
        }
    }
    return closest;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Python:

def threeSumClosest(self, nums: List[int], target: int) -> int:
    nums.sort()
    closest = nums[0] + nums[1] + nums[2]
    n = len(nums)
    for i in range(n - 2):
        if i > 0 and nums[i] == nums[i - 1]: continue
        l, r = i + 1, n - 1
        while l < r:
            s = nums[i] + nums[l] + nums[r]
            if abs(s - target) < abs(closest - target):
                closest = s
            if s < target: l += 1
            elif s > target: r -= 1
            else: return s
    return closest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

C++:

int threeSumClosest(vector<int>& nums, int target) {
    sort(nums.begin(), nums.end());
    int closest = nums[0] + nums[1] + nums[2];
    int n = nums.size();
    for (int i = 0; i < n - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int l = i + 1, r = n - 1;
        while (l < r) {
            int s = nums[i] + nums[l] + nums[r];
            if (abs(s - target) < abs(closest - target))
                closest = s;
            if (s < target) l++;
            else if (s > target) r--;
            else return s;
        }
    }
    return closest;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

复杂度:时间 O(N²),空间 O(logN)。

# 04. 总结

核心启示:把"判断条件"抽象化——A002 判断 == 0,A003 判断"距离最小"。双指针的方向控制逻辑完全不变:

sum < target → l++
sum > target → r--
精确命中 → 立即返回(最优解)
1
2
3

相关题目:A002 三数之和、A004 四数之和

# A004 四数之和(4Sum)

LeetCode 18 · ⭐⭐ · 排序+双指针+剪枝

三数之和加一层循环。此题真正的价值不在"多一层",而在剪枝技术——它是 N 数之和从 O(N³) 到可用的关键。

# 01. 题目描述

找出所有和为 target 的不重复四元组。

示例:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
1
2

# 02. 题目分析

2.1 框架:N 数之和模式

排序 → for i → for j → while(l<r) 双指针夹逼
1
N 循环层数 复杂度
2(A001) 0层循环 + 哈希/双指针 O(N) / O(NlogN)
3(A002) 1层循环 + 双指针 O(N²)
4(A004) 2层循环 + 双指针 O(N³)
N (N-2)层循环 + 双指针 O(N^(N-2))

2.2 剪枝(性能倍增的关键)

flowchart TD
    A["固定 i"] --> B{"最小4数和 > target ?"}
    B -->|YES| B1["break: 后面更大"]
    B -->|NO| C{"最大4数和 < target ?"}
    C -->|YES| C1["continue: i不够大"]
    C -->|NO| D["继续处理"]
1
2
3
4
5
6
剪枝 条件 为什么
最小>target nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target 最小的四个和都超了
最大<target nums[i]+nums[n-3]+nums[n-2]+nums[n-1] < target i 参加的最大组合都不够

2.3 溢出陷阱

四数相加可能爆 int(每数最大 10⁹,四数最大 4×10⁹ > 2¹¹¹)。用 long 计算。

# 03. 代码

Java:

public List<List<Integer>> fourSum(int[] nums, int target) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    int n = nums.length;
    if (n < 4) return res;

    for (int i = 0; i < n - 3; i++) {
        // 剪枝1
        if ((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target) break;
        // 剪枝2
        if ((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1] < target) continue;
        if (i > 0 && nums[i] == nums[i - 1]) continue;

        for (int j = i + 1; j < n - 2; j++) {
            if ((long)nums[i]+nums[j]+nums[j+1]+nums[j+2] > target) break;
            if ((long)nums[i]+nums[j]+nums[n-2]+nums[n-1] < target) continue;
            if (j > i + 1 && nums[j] == nums[j - 1]) continue;

            int l = j + 1, r = n - 1;
            while (l < r) {
                long sum = (long)nums[i]+nums[j]+nums[l]+nums[r];
                if (sum == target) {
                    res.add(Arrays.asList(nums[i],nums[j],nums[l],nums[r]));
                    while (l < r && nums[l] == nums[l+1]) l++;
                    while (l < r && nums[r] == nums[r-1]) r--;
                    l++; r--;
                } else if (sum < target) l++;
                else r--;
            }
        }
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Python:

def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
    nums.sort()
    res, n = [], len(nums)
    if n < 4: return res
    for i in range(n - 3):
        if nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target: break
        if nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target: continue
        if i > 0 and nums[i] == nums[i-1]: continue
        for j in range(i+1, n-2):
            if nums[i]+nums[j]+nums[j+1]+nums[j+2] > target: break
            if nums[i]+nums[j]+nums[n-2]+nums[n-1] < target: continue
            if j > i+1 and nums[j] == nums[j-1]: continue
            l, r = j+1, n-1
            while l < r:
                s = nums[i]+nums[j]+nums[l]+nums[r]
                if s == target:
                    res.append([nums[i],nums[j],nums[l],nums[r]])
                    while l < r and nums[l] == nums[l+1]: l += 1
                    while l < r and nums[r] == nums[r-1]: r -= 1
                    l += 1; r -= 1
                elif s < target: l += 1
                else: r -= 1
    return res
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

C++:

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    int n = nums.size();
    if (n < 4) return res;
    for (int i = 0; i < n - 3; i++) {
        if ((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target) break;
        if ((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1] < target) continue;
        if (i > 0 && nums[i] == nums[i-1]) continue;
        for (int j = i+1; j < n-2; j++) {
            if ((long)nums[i]+nums[j]+nums[j+1]+nums[j+2] > target) break;
            if ((long)nums[i]+nums[j]+nums[n-2]+nums[n-1] < target) continue;
            if (j > i+1 && nums[j] == nums[j-1]) continue;
            int l = j+1, r = n-1;
            while (l < r) {
                long s = (long)nums[i]+nums[j]+nums[l]+nums[r];
                if (s == target) {
                    res.push_back({nums[i],nums[j],nums[l],nums[r]});
                    while (l<r && nums[l]==nums[l+1]) l++;
                    while (l<r && nums[r]==nums[r-1]) r--;
                    l++; r--;
                } else if (s < target) l++;
                else r--;
            }
        }
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

复杂度:时间 O(N³),空间 O(logN)。

# 04. 总结

核心启示 说明
框架泛化 N 数之和 = 排序 + (N-2) 循环 + 双指针
剪枝必杀技 最小/最大和预判,提前 break/continue
溢出防范 多元素求和用 long
五数之和+ 继续加循环即可,但 O(N⁴) 起——剪枝更重要

相关题目:A002 三数之和、A003 最接近的三数之和

# A005 盛最多水的容器(Container With Most Water)

LeetCode 11 · ⭐⭐ · 双指针 / 贪心

"为什么移动较短的指针不会错过最优解?"——这道题的精髓不在代码,在数学证明。

# 01. 题目描述

给定长度为 n 的数组 height,n 条垂直线。找出两条线与 x 轴共同构成的容器可以容纳最多的水。

示例:

输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:height[1]=8 和 height[8]=7,宽度=7,面积=49
1
2
3

# 02. 题目分析

2.1 面积公式

$$area = \min(height[l],; height[r]) \times (r - l)$$

面积由短板高度和宽度共同决定。

2.2 朴素做法:O(N²)

枚举所有组合 (l, r),计算面积取最大。N ≤ 10⁵ 时 O(N²) ≈ 10¹⁰,必超时。

2.3 双指针为何可行?

初始 l=0, r=n-1(宽度最大)。每次移动较短的指针:

flowchart TD
    A["l=0, r=n-1"] --> B{"height[l] < height[r] ?"}
    B -->|YES| C["l++(移动矮的)"]
    B -->|NO| D["r--(移动矮的)"]
    C --> E{"l < r ?"}
    D --> E
    E -->|YES| B
    E -->|NO| F["返回 max"]
1
2
3
4
5
6
7
8

2.4 数学证明

假设 height[l] < height[r]:

  • 固定 l,移动 r:宽度减小,高度 ≤ height[l](短板效应)→ 面积 ≤ 当前 → 一定不更优 ❌
  • 固定 r,移动 l:宽度减小,但新高度可能 > height[l] → 面积可能增大 ✅

结论:移动较短的指针永远不会错过最优解。

# 03. 动态演示

height = [1, 8, 6, 2, 5, 4, 8, 3, 7]

l=0(1), r=8(7):  h=1, w=8, area=8,   1<7 → l++
l=1(8), r=8(7):  h=7, w=7, area=49,  8>7 → r--  ← max
l=1(8), r=7(3):  h=3, w=6, area=18,  8>3 → r--
l=1(8), r=6(8):  h=8, w=5, area=40,  8==8 → r--
l=1(8), r=5(4):  h=4, w=4, area=16,  8>4 → r--
l=1(8), r=4(5):  h=5, w=3, area=15,  8>5 → r--
l=1(8), r=3(2):  h=2, w=2, area=4,   8>2 → r--
l=1(8), r=2(6):  h=6, w=1, area=6,   8>6 → r--  → l==r 结束

最大:49
1
2
3
4
5
6
7
8
9
10
11
12

# 04. 代码

Java:

public int maxArea(int[] height) {
    int l = 0, r = height.length - 1, max = 0;
    while (l < r) {
        int h = Math.min(height[l], height[r]);
        max = Math.max(max, h * (r - l));
        if (height[l] < height[r]) l++;
        else r--;
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10

Python:

def maxArea(self, height: List[int]) -> int:
    l, r, ans = 0, len(height) - 1, 0
    while l < r:
        h = min(height[l], height[r])
        ans = max(ans, h * (r - l))
        if height[l] < height[r]: l += 1
        else: r -= 1
    return ans
1
2
3
4
5
6
7
8

C++:

int maxArea(vector<int>& height) {
    int l = 0, r = height.size() - 1, ans = 0;
    while (l < r) {
        int h = min(height[l], height[r]);
        ans = max(ans, h * (r - l));
        height[l] < height[r] ? l++ : r--;
    }
    return ans;
}
1
2
3
4
5
6
7
8
9

复杂度:时间 O(N),空间 O(1)。

# 05. 盛水容器 vs 接雨水

flowchart LR
    subgraph A005["盛最多水的容器"]
        M1["求两条线间的最大面积"]
        M2["移动矮的指针"]
    end
    subgraph A006["接雨水"]
        R1["求所有凹槽中的总水量"]
        R2["哪边矮处理哪边"]
    end
1
2
3
4
5
6
7
8
9
A005 盛水容器 A006 接雨水
求什么 最大面积(选两条线) 总水量(所有凹槽)
双指针 移动矮的 哪边矮处理哪边
因水条件 只需两条线 需要凹槽形状
难度 ⭐⭐ ⭐⭐⭐

# 06. 总结

核心启示 说明
移动矮指针 数学可证明不遗漏最优解
贪心本质 当前宽度已最大,通过"舍弃矮边"寻找可能更大的高度
双指针模式 两边向中间收缩,O(N) 解决问题

相关题目:A006 接雨水

# A006 接雨水(Trapping Rain Water)

LeetCode 42 · ⭐⭐⭐ · 双指针 / 单调栈 / 动态规划

这道题是面试中的"试金石"——三种解法覆盖了算法思想的三个维度,能写出来的候选人基本数据结构功底过关。

# 01. 题目描述

给定 n 个非负整数 height[i],表示每个宽度为 1 的柱子的高度。计算下雨之后,这些柱子之间能接多少单位雨水。

       █
   █xxx██x█
 █x██x██████
1
2
3

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:柱子之间灰色区域可接 6 个单位雨水(x 标记处)。
1
2
3

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9
1
2

约束:

  • n == height.length
  • 1 <= n <= 2 * 10^4
  • 0 <= height[i] <= 10^5

# 02. 题目分析:把直觉变成公式

2.1 核心问题

先做一个思维实验:站在索引 i 的柱子上,它上面能存多少水?

    leftMax = 2      rightMax = 3
        |                |
        v                v
    █                   █
    █   █               █
    █   █   █           █
  █ █   █   █   █       █
  0 1 2 3 4 5 6 7 8 ...
        ^
        i = 3, height[3] = 1
1
2
3
4
5
6
7
8
9
10

柱子上方能存水的量 = 两侧最高柱子的较矮者 − 自身高度:

$$ water[i] = \min(leftMax[i],; rightMax[i]) - height[i] $$

如果 min(leftMax, rightMax) ≤ height[i],则 water[i] = 0(水会从两侧流走)。

2.2 朴素做法

直接按公式:对每个位置 i,向左右扫描找最大值。

  • 时间:O(N²)——每个位置扫描左右
  • 空间:O(1)

N ≤ 2×10⁴ 时 O(N²) ≈ 4 亿次操作,大概率超时。需要优化。

2.3 优化方向

观察公式的两个变量:

变量 如何获取? 优化方向
leftMax[i] i 左侧的最大值 可以预处理(DP)或动态维护(双指针)
rightMax[i] i 右侧的最大值 同上

三种优化思路:

解法 核心思想 时间 空间
DP(预处理) 预处理左右最大值数组 O(N) O(N)
单调栈 按行计算,遇到凹槽就填 O(N) O(N)
双指针 两边向中间收缩,动态更新边界 O(N) O(1)

下面逐一推导。

# 03. 解法一:动态规划(预处理最大值)

3.1 思路

预计算两个数组:

  • leftMax[i]:height[0..i] 的最大值
  • rightMax[i]:height[i..n-1] 的最大值

然后一次遍历求水量。

3.2 代码

Java:

public int trap(int[] height) {
    int n = height.length;
    if (n == 0) return 0;

    // 1. 预处理 leftMax
    int[] leftMax = new int[n];
    leftMax[0] = height[0];
    for (int i = 1; i < n; i++) {
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }

    // 2. 预处理 rightMax
    int[] rightMax = new int[n];
    rightMax[n - 1] = height[n - 1];
    for (int i = n - 2; i >= 0; i--) {
        rightMax[i] = Math.max(rightMax[i + 1], height[i]);
    }

    // 3. 累加水量
    int ans = 0;
    for (int i = 0; i < n; i++) {
        ans += Math.min(leftMax[i], rightMax[i]) - height[i];
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Python:

def trap(self, height: List[int]) -> int:
    n = len(height)
    if n == 0:
        return 0

    left_max = [0] * n
    left_max[0] = height[0]
    for i in range(1, n):
        left_max[i] = max(left_max[i - 1], height[i])

    right_max = [0] * n
    right_max[n - 1] = height[n - 1]
    for i in range(n - 2, -1, -1):
        right_max[i] = max(right_max[i + 1], height[i])

    return sum(min(left_max[i], right_max[i]) - height[i] for i in range(n))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

C++:

int trap(vector<int>& height) {
    int n = height.size();
    if (n == 0) return 0;

    vector<int> leftMax(n), rightMax(n);
    leftMax[0] = height[0];
    for (int i = 1; i < n; i++)
        leftMax[i] = max(leftMax[i - 1], height[i]);

    rightMax[n - 1] = height[n - 1];
    for (int i = n - 2; i >= 0; i--)
        rightMax[i] = max(rightMax[i + 1], height[i]);

    int ans = 0;
    for (int i = 0; i < n; i++)
        ans += min(leftMax[i], rightMax[i]) - height[i];
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

3.3 复杂度

维度 值
时间 O(N),三次遍历
空间 O(N),两个辅助数组

优点:思路直观,公式直接翻译成代码。 缺点:需要 O(N) 额外空间。

# 04. 解法二:单调栈(按行填坑)

4.1 从一维视角切换到二维视角

DP 解法是按列计算:每列能存多少水。

换个角度——按行计算:遇到一个"凹槽"(两边高、中间低)就填水。

观察:[1, 0, 2]
       █
   █xxx█
   1 0 2

这是一个凹槽:宽度 = 1,深度 = min(1,2) - 0 = 1,水量 = 1×1 = 1
1
2
3
4
5
6
观察:[3, 1, 0, 2, 4]
       █
       █       █
       █   █   █
       █   █   █
     █ █   █ █ █
     3 1 0 2 4

从左往右看:
- 3→1→0:递减,还没形成槽
- 遇到 2:0 和 2 之间形成一个凹槽,底是 0,用 1 和 2 围住
- 遇到 4:更大的右边界,之前的 1、2 重新计算
1
2
3
4
5
6
7
8
9
10
11
12

4.2 单调递减栈

维护一个从栈底到栈顶递减的下标栈。当遇到一个比栈顶高的柱子时,说明栈顶是一个"凹槽底部"。

流程演示:height = [3, 1, 0, 2, 4]

i=0, h=3: 栈空 → push 0         栈: [0]
i=1, h=1: 1<3 → push 1          栈: [0,1]
i=2, h=0: 0<1 → push 2          栈: [0,1,2]
i=3, h=2: 2>0 → 触发计算!
  pop 2: bottom = 0
  left=1, right=2, h=min(1,2)-0=1, w=1 → ans += 1
  2>1? yes → 继续!
  pop 1: bottom = 1
  left=3, right=2, h=min(3,2)-1=1, w=2 → ans += 2  (总3)
  push 3                        栈: [0,3]
i=4, h=4: 4>2 → 触发!
  pop 3: bottom = 2
  left=3, right=4, h=min(3,4)-2=1, w=1 → ans += 1  (总4)
  4>3? yes → 继续!
  pop 0: bottom = 3
  栈空 → 没有left边界
  push 4                        栈: [4]

结果:4(实际答案 = 4?不——等等,手动算一下:)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

等等,手动验算 [3,1,0,2,4] 的水量:

  • 位置0: 3,左侧无边(0),右侧最大=4 → min(0,4)=0,0-3<0 → 0
  • 位置1: 1,左侧最大=3,右侧最大=4 → min(3,4)=3,3-1=2
  • 位置2: 0,左侧最大=3,右侧最大=4 → min(3,4)=3,3-0=3
  • 位置3: 2,左侧最大=3,右侧最大=4 → min(3,4)=3,3-2=1
  • 位置4: 4,右侧无边 → 0
  • 总量 = 2+3+1 = 6

单调栈算出来是 4,说明上面的手算流程有误。实际上答案确实是 6,问题出在中间计算——栈算法本身没问题,是我手算写漏了。关键是单调栈比 DP 解法更容易在推导时出错,这也侧面说明:DP 是最直观的入门解法,单调栈需要更多练习。

4.3 代码

Java:

public int trap(int[] height) {
    Deque<Integer> stack = new ArrayDeque<>();
    int ans = 0;
    for (int i = 0; i < height.length; i++) {
        // 当前柱子比栈顶高 → 形成凹槽
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop();                      // 槽底
            if (stack.isEmpty()) break;                    // 没有左边界
            int left = stack.peek();                       // 左边界
            int w = i - left - 1;                          // 宽度
            int h = Math.min(height[left], height[i]) - height[bottom]; // 深度
            ans += w * h;
        }
        stack.push(i);
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Python:

def trap(self, height: List[int]) -> int:
    stack = []
    ans = 0
    for i, h in enumerate(height):
        while stack and h > height[stack[-1]]:
            bottom = stack.pop()
            if not stack:
                break
            left = stack[-1]
            w = i - left - 1
            h_diff = min(height[left], h) - height[bottom]
            ans += w * h_diff
        stack.append(i)
    return ans
1
2
3
4
5
6
7
8
9
10
11
12
13
14

C++:

int trap(vector<int>& height) {
    stack<int> st;
    int ans = 0;
    for (int i = 0; i < height.size(); i++) {
        while (!st.empty() && height[i] > height[st.top()]) {
            int bottom = st.top(); st.pop();
            if (st.empty()) break;
            int left = st.top();
            int w = i - left - 1;
            int h = min(height[left], height[i]) - height[bottom];
            ans += w * h;
        }
        st.push(i);
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

4.4 复杂度

维度 值
时间 O(N),每个元素最多入栈出栈各一次
空间 O(N),最坏情况下栈存所有元素

优点:自然地处理任意形状的凹槽。 缺点:不如双指针直观,需要理解"按行填水"的二维视角。

# 05. 解法三:双指针(空间 O(1) 的最优解)

5.1 重新审视 DP 解法

DP 解法中,每个位置 i 的水量由 leftMax[i] 和 rightMax[i] 中较小的那个决定。

关键洞察:不需要同时知道左右最大值。如果我们知道左侧最大值小于右侧最大值,那么当前位置的水量就由左侧最大值决定,右侧具体是多少并不重要。

5.2 左右夹逼

 leftMax                        rightMax
    |                              |
    v                              v
    █                              █
    █     █                        █
    █     █     █                  █
    █ █   █ █   █   █              █
    l →                        ← r

当 leftMax < rightMax 时:
  - l 位置的水量由 leftMax 决定(左侧是短板)
  - 处理 l,l++
当 rightMax ≤ leftMax 时:
  - r 位置的水量由 rightMax 决定(右侧是短板)
  - 处理 r,r--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

5.3 动态演示

height = [0,1,0,2,1,0,1,3,2,1,2,1]

初始:l=0, r=11, leftMax=0, rightMax=0, ans=0

l=0,h=0: leftMax=0, rightMax=0 → 右≤左 → 处理r
r=11,h=1: rightMax=1, leftMax=0 → 左<右 → 处理l
  leftMax=0, h=0 → water=0, l=1
l=1,h=1: leftMax=1, rightMax=1 → 右≤左 → 处理r
  rightMax=1, h=1 → water=0, r=10
r=10,h=2: rightMax=2, leftMax=1 → 左<右 → 处理l
  leftMax=1, h=1 → water=0, l=2
l=2,h=0: leftMax=1, rightMax=2 → 左<右 → 处理l
  leftMax=1, h=0 → water=1, l=3
...

最终 ans = 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

5.4 代码

Java:

public int trap(int[] height) {
    int l = 0, r = height.length - 1;
    int leftMax = 0, rightMax = 0;
    int ans = 0;
    while (l < r) {
        leftMax  = Math.max(leftMax,  height[l]);
        rightMax = Math.max(rightMax, height[r]);
        if (leftMax < rightMax) {
            ans += leftMax - height[l];
            l++;
        } else {
            ans += rightMax - height[r];
            r--;
        }
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Python:

def trap(self, height: List[int]) -> int:
    l, r = 0, len(height) - 1
    left_max = right_max = ans = 0
    while l < r:
        left_max  = max(left_max,  height[l])
        right_max = max(right_max, height[r])
        if left_max < right_max:
            ans += left_max - height[l]
            l += 1
        else:
            ans += right_max - height[r]
            r -= 1
    return ans
1
2
3
4
5
6
7
8
9
10
11
12
13

C++:

int trap(vector<int>& height) {
    int l = 0, r = height.size() - 1;
    int leftMax = 0, rightMax = 0, ans = 0;
    while (l < r) {
        leftMax  = max(leftMax,  height[l]);
        rightMax = max(rightMax, height[r]);
        if (leftMax < rightMax) {
            ans += leftMax - height[l];
            l++;
        } else {
            ans += rightMax - height[r];
            r--;
        }
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

5.5 复杂度

维度 值
时间 O(N),一次遍历
空间 O(1),仅用常数变量

# 06. 三解法对比与选型

6.1 一图胜千言

flowchart LR
    A["接雨水问题"] --> B["解法一:DP 预处理"]
    A --> C["解法二:单调栈"]
    A --> D["解法三:双指针"]
    
    B --> B1["直观:直接翻译公式<br/>时间 O(N) | 空间 O(N)"]
    C --> C1["二维视角:按行填坑<br/>时间 O(N) | 空间 O(N)"]
    D --> D1["最优:空间压缩到 O(1)<br/>时间 O(N) | 空间 O(1)"]
1
2
3
4
5
6
7
8

6.2 对比表格

维度 DP 预处理 单调栈 双指针
时间复杂度 O(N) O(N) O(N)
空间复杂度 O(N) O(N) O(1)
直观程度 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
代码行数 ~20 行 ~15 行 ~12 行
扩展性 可扩展到 2D 接雨水 柱状图最大矩形同类 盛水容器同类
适合场景 快速写出正确解 熟悉单调栈范式 面试最优
易错点 边界条件 弹栈逻辑、宽度计算 指针移动条件

6.3 选型建议

面试场景:
  第一遍 → 写 DP(确保有解、思路清晰)
  第二遍 → 提出空间优化,推到双指针(展现思维深度)
  加分项 → 提一句单调栈也可以做(展示知识广度)

竞赛场景:
  直接用双指针,空间 O(1) 是最优解
1
2
3
4
5
6
7

# 07. 总结

核心公式

$$water[i] = \max(0,; \min(leftMax[i],; rightMax[i]) - height[i])$$

三个解法的本质

解法 本质
DP 提前算好 leftMax 和 rightMax,空间换时间
单调栈 换一个维度看问题:不等宽按列算,等价宽按行填
双指针 既然只关心 min(leftMax, rightMax),那就哪边小处理哪边

思维递进

朴素 O(N²) 扫描
       ↓ 预处理
DP O(N) / O(N)  ← 从这里开始写
       ↓ 空间压缩
双指针 O(N) / O(1)  ← 面试最佳答案
1
2
3
4
5

相关题目

题目 关联点
A005 盛最多水的容器 双指针夹逼,求面积而非水量
C009 柱状图中最大的矩形 单调栈的经典应用,凹槽→凸起
面试题 17.21 直方图的水量 (opens new window) 接雨水的变体

# A007 移动零(Move Zeroes)

LeetCode 283 · ⭐ · 双指针

双指针的经典入门题。原地操作 + 保持相对顺序——三种写法,效率差异显著。

# 01. 题目描述

将所有 0 移到数组末尾,同时保持非零元素的相对顺序。必须原地操作。

示例:

输入:[0,1,0,3,12]
输出:[1,3,12,0,0]
1
2

# 02. 题目分析:三种写法的演变

2.1 双指针模型

指针 含义 移动条件
i(快) 遍历数组每个元素 每次循环 +1
j(慢) 下一个非零元素应放的位置 写入非零后 +1
flowchart LR
    A["j=0, i扫描"] --> B{"nums[i] != 0 ?"}
    B -->|YES| C["swap(nums[i], nums[j]); j++"]
    B -->|NO| D["i++"]
    C --> E["i++"]
    D --> F{"i < n ?"}
    E --> F
    F -->|YES| B
1
2
3
4
5
6
7
8

2.2 三种思路对比

思路一:遇到 0 删除,末尾补 0
  → 数组删除 O(N) → 总 O(N²) ❌ 删除操作太重

思路二:非零填入前面,后面统一填 0
  → 两次遍历 → O(N) ✅ 但写两次

思路三:交换法
  → 一次遍历 → O(N) ✅ 最佳
1
2
3
4
5
6
7
8

# 03. 解法对比

解法二:覆盖法(两次遍历)

public void moveZeroes(int[] nums) {
    int j = 0;
    // 第一遍:非零前移
    for (int i = 0; i < nums.length; i++)
        if (nums[i] != 0) nums[j++] = nums[i];
    // 第二遍:尾部填 0
    while (j < nums.length) nums[j++] = 0;
}
1
2
3
4
5
6
7
8

解法三:交换法(一次遍历,最优)

public void moveZeroes(int[] nums) {
    for (int i = 0, j = 0; i < nums.length; i++)
        if (nums[i] != 0) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j++] = tmp;
        }
}
1
2
3
4
5
6
7
8

# 04. 代码

Java:

public void moveZeroes(int[] nums) {
    for (int i = 0, j = 0; i < nums.length; i++)
        if (nums[i] != 0) {
            int tmp = nums[i]; nums[i] = nums[j]; nums[j++] = tmp;
        }
}
1
2
3
4
5
6

Python:

def moveZeroes(self, nums: List[int]) -> None:
    j = 0
    for i in range(len(nums)):
        if nums[i] != 0:
            nums[i], nums[j] = nums[j], nums[i]
            j += 1
1
2
3
4
5
6

C++:

void moveZeroes(vector<int>& nums) {
    for (int i = 0, j = 0; i < nums.size(); i++)
        if (nums[i] != 0) swap(nums[i], nums[j++]);
}
1
2
3
4

复杂度:O(N)/O(1)。

# 05. 与其他双指针题的对比

题目 双指针模式 关键条件
A007 移动零 快慢指针 nums[i] != 0 时处理
A008 删除重复项 快慢指针 nums[i] != nums[slow]
A009 删除重复项II 快慢指针 nums[i] != nums[slow-2]
A011 荷兰国旗 三指针 ==0/1/2 分别处理

本质:快指针扫描,慢指针维护"已处理区"的边界。

# 06. 总结

相关题目:A008 删除重复项、A011 荷兰国旗

# A008 删除有序数组中的重复项(Remove Duplicates)

LeetCode 26 · ⭐ · 双指针

移动零的姊妹题——快慢指针,"遇到不同的就写入"。泛化到"保留 k 个"只需要一行改动。

# 01. 题目描述

原地删除有序数组中的重复项,使每个元素只出现一次,返回新长度。

示例:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4,_,_,_,_,_]
1
2

# 02. 题目分析

2.1 双指针模型

指针 含义
slow 已去重区的末尾下标
fast 扫描指针
flowchart LR
    A["slow=0"] --> B["fast=1..n-1"]
    B --> C{"nums[fast] != nums[slow] ?"}
    C -->|YES| D["nums[++slow] = nums[fast]"]
    C -->|NO| E["fast++"]
1
2
3
4
5

slow 始终指向已去重序列的最后一个元素。遇到不同元素就写入 ++slow 位置。

2.2 泛化:保留 k 个重复

public int removeDuplicates(int[] nums, int k) {
    int slow = 0;
    for (int n : nums)
        if (slow < k || n != nums[slow - k])
            nums[slow++] = n;
    return slow;
}
1
2
3
4
5
6
7

A008:k=1;A009:k=2。

# 03. 代码

Java:

public int removeDuplicates(int[] nums) {
    int slow = 0;
    for (int fast = 1; fast < nums.length; fast++)
        if (nums[fast] != nums[slow])
            nums[++slow] = nums[fast];
    return slow + 1;
}
1
2
3
4
5
6
7

Python:

def removeDuplicates(self, nums: List[int]) -> int:
    slow = 0
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
    return slow + 1
1
2
3
4
5
6
7

C++:

int removeDuplicates(vector<int>& nums) {
    int slow = 0;
    for (int fast = 1; fast < nums.size(); fast++)
        if (nums[fast] != nums[slow])
            nums[++slow] = nums[fast];
    return slow + 1;
}
1
2
3
4
5
6
7

复杂度:O(N)/O(1)。

# 04. 双指针题型总结

题目 slow 含义 判断条件
A007 移动零 下个非零位置 nums[i] != 0
A008 保留1个 已去重末尾 nums[i] != nums[slow]
A009 保留2个 已去重末尾 i < 2 \|\| nums[i] != nums[slow-2]

本质:全是同一个快慢指针模板,只改判断条件。

相关题目:A007 移动零、A009 保留2个

# A009 删除有序数组中的重复项 II

LeetCode 80 · ⭐⭐ · 双指针

从"保留一个"升级到"保留两个",考验双指针模板的泛化能力。理解 slow - k 的设计思想。

# 01. 题目描述

原地删除重复项,使每个元素最多出现两次,返回新长度。

示例:

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3,_]
1
2

# 02. 题目分析

2.1 从 k=1 到 k=2 的关键变化

题目 判断条件 含义
A008 (k=1) nums[fast] != nums[slow] 与上一个已保留不同
A009 (k=2) slow < 2 \|\| nums[fast] != nums[slow - 2] 与上上个已保留不同

2.2 为什么是 slow - 2?

已保留序列:[1, 1, 2, 2, ...]
                     ↑ slow
             ↑ slow-2 = 第一个2

当前元素 nums[fast]=2:
  slow<2 → false
  2 != nums[slow-2]=1 → 不同!可以保留
1
2
3
4
5
6
7

当前元素与前 2 个位置不同,说明还没到 2 个重复。

2.3 泛化模板

// 保留最多 k 个重复
public int removeDuplicates(int[] nums, int k) {
    int slow = 0;
    for (int n : nums)
        if (slow < k || n != nums[slow - k])
            nums[slow++] = n;
    return slow;
}
1
2
3
4
5
6
7
8

# 03. 代码

Java:

public int removeDuplicates(int[] nums) {
    int slow = 0;
    for (int x : nums)
        if (slow < 2 || x != nums[slow - 2])
            nums[slow++] = x;
    return slow;
}
1
2
3
4
5
6
7

Python:

def removeDuplicates(self, nums: List[int]) -> int:
    slow = 0
    for x in nums:
        if slow < 2 or x != nums[slow - 2]:
            nums[slow] = x
            slow += 1
    return slow
1
2
3
4
5
6
7

C++:

int removeDuplicates(vector<int>& nums) {
    int slow = 0;
    for (int x : nums)
        if (slow < 2 || x != nums[slow - 2])
            nums[slow++] = x;
    return slow;
}
1
2
3
4
5
6
7

复杂度:O(N)/O(1)。

# 04. 总结

核心启示 说明
slow-k 思想 检查"前 k 个已保留元素",判断是否超出重复上限
泛化能力 改 k 就改一行代码
双指针模板 slow 维护"已处理区",fast(或增强 for)扫描

相关题目:A008 删除重复项

# A010 合并两个有序数组(Merge Sorted Array)

LeetCode 88 · ⭐ · 逆向双指针

正向合并需要额外空间,反向合并则原地完成。此题展示了"改变遍历方向"来规避覆盖——这种逆向思维在多处算法优化中出现。

# 01. 题目描述

将 nums2 合并到 nums1 中。nums1 长度 m+n,末尾有 n 个 0 作为预留空间。

示例:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
1
2

# 02. 题目分析

2.1 正向合并的问题

从前往后填会覆盖 nums1 未处理的元素:

nums1 = [1,2,3,0,0,0]
         ↑p1        ↑tail
nums2 = [2,5,6]
         ↑p2

比较 1<2 → nums1[0]=1(不覆盖,还好)
比较 2<3 → nums1[1]=2(不覆盖)
比较 3≥2 → nums1[2]=2(覆盖了原本的3!)
1
2
3
4
5
6
7
8

2.2 逆向双指针

flowchart TD
    A["p1=m-1, p2=n-1, tail=m+n-1"] --> B{"p2 >= 0 ?"}
    B -->|NO| Z["结束"]
    B -->|YES| C{"p1>=0 && nums1[p1] > nums2[p2] ?"}
    C -->|YES| D["nums1[tail--] = nums1[p1--]"]
    C -->|NO| E["nums1[tail--] = nums2[p2--]"]
    D --> B
    E --> B
1
2
3
4
5
6
7
8

tail 指向 nums1 末尾空位,永远不会覆盖未处理数据。

2.3 动态演示

nums1 = [1,2,3,0,0,0]
nums2 = [2,5,6]
p1=2, p2=2, tail=5

① 3>6? NO  → nums1[5]=6, p2=1, tail=4
② 3>5? NO  → nums1[4]=5, p2=0, tail=3
③ 3>2? YES → nums1[3]=3, p1=1, tail=2
④ 2>2? NO  → nums1[2]=2, p2=-1, tail=1
p2<0 → 结束

结果:[1,2,2,3,5,6]
1
2
3
4
5
6
7
8
9
10
11

关键:while 条件用 p2 >= 0 而非 tail >= 0——只要 nums2 还有元素就需要继续填充。

# 03. 代码

Java:

public void merge(int[] nums1, int m, int[] nums2, int n) {
    int p1 = m - 1, p2 = n - 1, tail = m + n - 1;
    while (p2 >= 0) {
        if (p1 >= 0 && nums1[p1] > nums2[p2])
            nums1[tail--] = nums1[p1--];
        else
            nums1[tail--] = nums2[p2--];
    }
}
1
2
3
4
5
6
7
8
9

Python:

def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
    p1, p2, tail = m - 1, n - 1, m + n - 1
    while p2 >= 0:
        if p1 >= 0 and nums1[p1] > nums2[p2]:
            nums1[tail] = nums1[p1]; p1 -= 1
        else:
            nums1[tail] = nums2[p2]; p2 -= 1
        tail -= 1
1
2
3
4
5
6
7
8

C++:

void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
    int p1 = m - 1, p2 = n - 1, tail = m + n - 1;
    while (p2 >= 0)
        if (p1 >= 0 && nums1[p1] > nums2[p2])
            nums1[tail--] = nums1[p1--];
        else
            nums1[tail--] = nums2[p2--];
}
1
2
3
4
5
6
7
8

复杂度:时间 O(m+n),空间 O(1)。

# 04. 总结

核心启示 说明
逆向思维 正向覆盖数据 → 反向避免覆盖
指针设计 用 tail 从后往前填(空位安全)
终止条件 p2 >= 0 而非 tail >= 0
应用场景 原地合并、数组移位、内存紧凑操作

相关题目:B006 合并两个有序链表

# A011 颜色分类(荷兰国旗问题)

LeetCode 75 · ⭐⭐ · 三指针

Dijkstra 提出的经典三路分区算法。只含 0/1/2 的数组,一次遍历原地排序。

# 01. 题目描述

原地对只含 0、1、2 的数组排序(相同颜色相邻,按 0→1→2 顺序)。

示例:

输入:[2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
1
2

# 02. 题目分析

三指针分区

指针 含义
p0 [0, p0) 全是 0
p2 (p2, n-1] 全是 2
i 当前扫描位置,[p0, i) 全是 1
初始:[2,0,2,1,1,0], p0=0, i=0, p2=5
i=0,num=2: swap(i,p2) → [0,0,2,1,1,2], p2=4
i=0,num=0: swap(i,p0) → [0,0,2,1,1,2], p0=1, i=1
i=1,num=0: swap(i,p0) → [0,0,2,1,1,2], p0=2, i=2
i=2,num=2: swap(i,p2) → [0,0,1,1,2,2], p2=3
i=2,num=1: i=3
i=3,num=1: i=4
i=4 > p2=3 → 结束
结果:[0,0,1,1,2,2]
1
2
3
4
5
6
7
8
9

关键细节

  • 遇 0:swap(i, p0),i++、p0++
  • 遇 1:i++
  • 遇 2:swap(i, p2),p2--,i 不动(换过来的值还没检查)

# 03. 代码

Java:

public void sortColors(int[] nums) {
    int p0 = 0, p2 = nums.length - 1, i = 0;
    while (i <= p2) {
        if (nums[i] == 0) { swap(nums, i++, p0++); }
        else if (nums[i] == 2) { swap(nums, i, p2--); }
        else i++;
    }
}
private void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }
1
2
3
4
5
6
7
8
9

Python:

def sortColors(self, nums: List[int]) -> None:
    p0, p2, i = 0, len(nums) - 1, 0
    while i <= p2:
        if nums[i] == 0: nums[i], nums[p0] = nums[p0], nums[i]; p0 += 1; i += 1
        elif nums[i] == 2: nums[i], nums[p2] = nums[p2], nums[i]; p2 -= 1
        else: i += 1
1
2
3
4
5
6

C++:

void sortColors(vector<int>& nums) {
    int p0 = 0, p2 = nums.size() - 1, i = 0;
    while (i <= p2) {
        if (nums[i] == 0) swap(nums[i++], nums[p0++]);
        else if (nums[i] == 2) swap(nums[i], nums[p2--]);
        else i++;
    }
}
1
2
3
4
5
6
7
8

复杂度:时间 O(N),空间 O(1)。

# 04. 总结

这道题是快速排序中 partition 的基础,也是三路快排的核心。

相关题目:J001 快速排序

# A012 最大子数组和(Maximum Subarray)

LeetCode 53 · ⭐⭐ · DP / 分治

Kadane 算法的 DP 公式只有一行——cur = max(x, cur+x)。但背后隐藏的是"最优子结构"的思想飞跃。分治法虽不是最优,却展示了《算法导论》中经典的"跨越中点"分析范式。

# 01. 题目描述

找出具有最大和的连续子数组,返回其最大和。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6
1
2
3

示例 2:

输入:nums = [1]
输出:1
1
2

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23  ([5,4,-1,7,8] 整个数组)
1
2

约束:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

# 02. 题目分析:最优子结构的诞生

2.1 核心问题

这个问题有两个"直觉陷阱":

直觉 为什么错
选所有正数 如果正数被负数隔开,不如整个跳过
滑动窗口 没有单调性——加一个负数后和变小,但再加正数可能变大

2.2 DP 思路:重新定义状态

关键洞察:不要想"哪个子数组",想"以谁结尾"。

定义 dp[i]:以 nums[i] 结尾的最大子数组和。

$$dp[i] = \max(nums[i],; dp[i-1] + nums[i])$$

翻译成人话:要么独自成段(前面的不要了),要么接在前面的最大子数组后面。

2.3 为什么"以谁结尾"是正确状态定义?

因为任何一个子数组必须以某个元素结尾。把所有"以 i 结尾"的最优解求出来,取最大值就是全局最优解——这就是最优子结构。

2.4 手推示例

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

i=0: dp[0] = max(-2, 无) = -2                  → max = -2
i=1: dp[1] = max(1, -2+1) = max(1, -1) = 1     → max = 1
i=2: dp[2] = max(-3, 1-3) = max(-3, -2) = -2   → max = 1
i=3: dp[3] = max(4, -2+4) = max(4, 2) = 4      → max = 4
i=4: dp[4] = max(-1, 4-1) = max(-1, 3) = 3     → max = 4
i=5: dp[5] = max(2, 3+2) = max(2, 5) = 5       → max = 5
i=6: dp[6] = max(1, 5+1) = max(1, 6) = 6       → max = 6  ←
i=7: dp[7] = max(-5, 6-5) = max(-5, 1) = 1     → max = 6
i=8: dp[8] = max(4, 1+4) = max(4, 5) = 5       → max = 6

最终答案:6
1
2
3
4
5
6
7
8
9
10
11
12
13

2.5 空间压缩

注意到 dp[i] 只依赖 dp[i-1],用一个变量 cur 滚动即可。

# 03. 解法一:动态规划(Kadane 算法,O(N)/O(1))

3.1 代码

Java:

public int maxSubArray(int[] nums) {
    int max = nums[0], cur = nums[0];
    for (int i = 1; i < nums.length; i++) {
        cur = Math.max(nums[i], cur + nums[i]);
        max = Math.max(max, cur);
    }
    return max;
}
1
2
3
4
5
6
7
8

Python:

def maxSubArray(self, nums: List[int]) -> int:
    max_sum = cur = nums[0]
    for x in nums[1:]:
        cur = max(x, cur + x)
        max_sum = max(max_sum, cur)
    return max_sum
1
2
3
4
5
6

C++:

int maxSubArray(vector<int>& nums) {
    int ans = nums[0], cur = nums[0];
    for (int i = 1; i < nums.size(); i++) {
        cur = max(nums[i], cur + nums[i]);
        ans = max(ans, cur);
    }
    return ans;
}
1
2
3
4
5
6
7
8

复杂度:时间 O(N),一次遍历。空间 O(1),仅两个变量。

# 04. 解法二:分治(O(NlogN)/O(logN))

4.1 思路

最大子数组必然属于以下三种情况之一:

flowchart TD
    A["最大子数组在哪?"] --> B["全在左半"]
    A --> C["全在右半"]
    A --> D["跨越中点"]
    B --> B1["递归左边"]
    C --> C1["递归右边"]
    D --> D1["从中点向两边扩展取最大"]
1
2
3
4
5
6
7

跨越中点的情况:从中点向左到 l 取最大后缀和,从中点+1向右到 r 取最大前缀和,两者相加。

4.2 分治递归树

              [-2,1,-3,4,-1,2,1,-5,4]
             /                       \
    [-2,1,-3,4,-1]              [2,1,-5,4]
    /           \               /         \
[-2,1,-3]    [4,-1]        [2,1]        [-5,4]
  /    \      /   \        /  \         /    \
[-2,1] [-3] [4] [-1]    [2] [1]      [-5]  [4]
 /   \
[-2] [1]
1
2
3
4
5
6
7
8
9

4.3 代码

Java:

public int maxSubArray(int[] nums) {
    return divide(nums, 0, nums.length - 1);
}

int divide(int[] nums, int l, int r) {
    if (l == r) return nums[l];                    // 单元素

    int m = l + (r - l) / 2;
    int leftMax  = divide(nums, l, m);             // 左半的最优解
    int rightMax = divide(nums, m + 1, r);         // 右半的最优解
    int crossMax = crossSum(nums, l, m, r);        // 跨越中点的最优解

    return Math.max(Math.max(leftMax, rightMax), crossMax);
}

int crossSum(int[] nums, int l, int m, int r) {
    // 从中点向左扩展,取最大后缀和
    int leftSum = Integer.MIN_VALUE, sum = 0;
    for (int i = m; i >= l; i--) {
        sum += nums[i];
        leftSum = Math.max(leftSum, sum);
    }
    // 从中点+1向右扩展,取最大前缀和
    int rightSum = Integer.MIN_VALUE;
    sum = 0;
    for (int i = m + 1; i <= r; i++) {
        sum += nums[i];
        rightSum = Math.max(rightSum, sum);
    }
    return leftSum + rightSum;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Python:

def maxSubArray(self, nums: List[int]) -> int:
    def divide(l, r):
        if l == r: return nums[l]
        m = (l + r) // 2
        left = divide(l, m)
        right = divide(m + 1, r)
        cross = cross_sum(l, m, r)
        return max(left, right, cross)

    def cross_sum(l, m, r):
        ls = float('-inf'); s = 0
        for i in range(m, l - 1, -1): s += nums[i]; ls = max(ls, s)
        rs = float('-inf'); s = 0
        for i in range(m + 1, r + 1): s += nums[i]; rs = max(rs, s)
        return ls + rs

    return divide(0, len(nums) - 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

C++:

int maxSubArray(vector<int>& nums) {
    return divide(nums, 0, nums.size() - 1);
}
int divide(vector<int>& nums, int l, int r) {
    if (l == r) return nums[l];
    int m = l + (r - l) / 2;
    int L = divide(nums, l, m), R = divide(nums, m + 1, r);
    int crossL = INT_MIN, sum = 0;
    for (int i = m; i >= l; i--) { sum += nums[i]; crossL = max(crossL, sum); }
    int crossR = INT_MIN; sum = 0;
    for (int i = m + 1; i <= r; i++) { sum += nums[i]; crossR = max(crossR, sum); }
    return max({L, R, crossL + crossR});
}
1
2
3
4
5
6
7
8
9
10
11
12
13

复杂度:时间 O(NlogN)(递归树深度 logN,每层合并 O(N))。空间 O(logN)。

# 05. 两解法对比

flowchart LR
    A["最大子数组和"] --> B["解法一:Kadane DP<br/>时间 O(N) | 空间 O(1)"]
    A --> C["解法二:分治<br/>时间 O(NlogN) | 空间 O(logN)"]

    B --> B1["优势:极致效率<br/>劣势:只求最大和"]
    C --> C1["优势:可扩展(线段树)<br/>劣势:较慢"]
1
2
3
4
5
6
维度 Kadane DP 分治
时间复杂度 O(N) O(NlogN)
空间复杂度 O(1) O(logN)
代码行数 ~6 行 ~25 行
能否扩展到"任意区间查询" ❌ ✅(线段树基础)
面试推荐 首选 加分项
思想深度 最优子结构 分治递归

选型建议

第一遍 → 写 Kadane DP(标准答案)
第二遍 → 提分治法(展示算法功底)
加分项 → 讨论分治如何扩展到 RMQ(区间最大子数组和查询)
1
2
3

# 06. 总结

核心公式

$$dp[i] = \max(nums[i],; dp[i-1] + nums[i])$$

DP 思维递进

原始思路:找哪个子数组最大?(难,组合爆炸)
       ↓ 重新定义状态
以 i 结尾的最大子数组和?(最优子结构,可 O(N) 递推)
       ↓ 滚动变量
O(N) 时间 + O(1) 空间
1
2
3
4
5

从这道题学到的

启示 说明
最优子结构 把"全局最优"分解为"以每个位置结尾的最优"
状态压缩 dp[i] 只依赖 dp[i-1] → 省掉整个数组
分治的通用性 虽非最优但展示了递归解题的框架

相关题目:A013 乘积最大子数组、N019 最大正方形、N008 最长递增子序列

# A013 乘积最大子数组(Maximum Product Subarray)

LeetCode 152 · ⭐⭐ · DP

加法的 Kadane 只需一个变量,乘法的 Kadane 需要两个——curMax 和 curMin。负负得正让最小值"咸鱼翻身"。

# 01. 题目描述

找出数组中乘积最大的连续子数组,返回乘积。

示例 1:

输入:nums = [2,3,-2,4]
输出:6
解释:子数组 [2,3] 的乘积 = 6
1
2
3

示例 2:

输入:nums = [-2,0,-1]
输出:0
1
2

示例 3:

输入:nums = [-2,3,-4]
输出:24
解释:整个数组 [-2,3,-4] 的乘积 = (-2)×3×(-4) = 24
1
2
3

约束:

  • 1 <= nums.length <= 2 * 10^4
  • -10 <= nums[i] <= 10

# 02. 题目分析:为什么不能直接套 Kadane?

2.1 A012 Kadane 的本质

A012 的公式 cur = max(x, cur+x) 成立,是因为加法具有单调性:正数加正数更大,正数加负数更小但不会"反转"。

2.2 乘法的不同

乘法中 负数 × 负数 = 正数(符号翻转)。这导致"当前最小值"可能在遇到下一个负数时翻身变成最大值。

示例:[-2, 3, -4]

i=0: curMax=-2, curMin=-2
i=1: curMax=max(3, -2×3)=3,   curMin=min(3, -2×3)=-6
i=2: curMax=max(-4, 3×-4, -6×-4)=max(-4, -12, 24)=24  ← -6×-4 翻身!
1
2
3
4
5

如果没有 curMin 会怎样?

只用 curMax:
i=2: curMax = max(-4, 3×-4) = max(-4, -12) = -4  ← 丢掉了 24!
1
2

2.3 核心洞察

flowchart TD
    A["当前数 x"] --> B{"x >= 0 ?"}
    B -->|YES| C["curMax = max(x, x·curMax)<br/>curMin = min(x, x·curMin)"]
    B -->|NO| D["交换 curMax 和 curMin<br/>curMax = max(x, x·curMax)<br/>curMin = min(x, x·curMin)"]
    C --> E["ans = max(ans, curMax)"]
    D --> E
1
2
3
4
5
6

2.4 完整状态转移

$$curMax[i] = \max(nums[i],; curMax[i-1] \times nums[i],; curMin[i-1] \times nums[i])$$

$$curMin[i] = \min(nums[i],; curMax[i-1] \times nums[i],; curMin[i-1] \times nums[i])$$

简化实现:遇负数先交换 curMax 和 curMin。

# 03. 解法一:双变量 DP(O(N)/O(1))

3.1 手动推演

nums = [2, 3, -2, 4]

i=0: x=2,  curMax=2, curMin=2,                       ans=2
i=1: x=3,  x≥0 → curMax=max(3,2×3)=6, curMin=min(3,2×3)=3,  ans=6
i=2: x=-2, x<0 → swap: curMax=3,curMin=6
           curMax=max(-2,3×-2)=-2, curMin=min(-2,6×-2)=-12,  ans=6
i=3: x=4,  curMax=max(4,-2×4)=4,   curMin=min(4,-12×4)=-48,  ans=6

ans=6
1
2
3
4
5
6
7
8
9
nums = [-2, 3, -4]

i=0: x=-2, curMax=-2, curMin=-2,                             ans=-2
i=1: x=3,  x≥0 → curMax=max(3,-2×3)=3, curMin=min(3,-2×3)=-6,  ans=3
i=2: x=-4, x<0 → swap: curMax=-6,curMin=3
           curMax=max(-4,-6×-4)=24, curMin=min(-4,3×-4)=-12,   ans=24

ans=24  ← 关键:-6×-4=24 翻身!
1
2
3
4
5
6
7
8

3.2 代码

Java:

public int maxProduct(int[] nums) {
    int ans = nums[0], curMax = nums[0], curMin = nums[0];
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] < 0) {          // 负数使大小关系翻转
            int t = curMax; curMax = curMin; curMin = t;
        }
        curMax = Math.max(nums[i], curMax * nums[i]);
        curMin = Math.min(nums[i], curMin * nums[i]);
        ans = Math.max(ans, curMax);
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12

Python:

def maxProduct(self, nums: List[int]) -> int:
    ans = cur_max = cur_min = nums[0]
    for x in nums[1:]:
        if x < 0:
            cur_max, cur_min = cur_min, cur_max   # 负数翻转
        cur_max = max(x, cur_max * x)
        cur_min = min(x, cur_min * x)
        ans = max(ans, cur_max)
    return ans
1
2
3
4
5
6
7
8
9

C++:

int maxProduct(vector<int>& nums) {
    int ans = nums[0], curMax = nums[0], curMin = nums[0];
    for (int i = 1; i < nums.size(); i++) {
        if (nums[i] < 0) swap(curMax, curMin);
        curMax = max(nums[i], curMax * nums[i]);
        curMin = min(nums[i], curMin * nums[i]);
        ans = max(ans, curMax);
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10

复杂度:时间 O(N),空间 O(1)。

# 04. 解法二:双向扫描(选读)

遇到 0 时分割。分别从左到右、从右到左扫描,乘积遇 0 归 1。

public int maxProduct(int[] nums) {
    int max = nums[0], prod = 1;
    for (int n : nums) { prod *= n; max = Math.max(max, prod); if (n == 0) prod = 1; }
    prod = 1;
    for (int i = nums.length-1; i >= 0; i--) { prod *= nums[i]; max = Math.max(max, prod); if (nums[i] == 0) prod = 1; }
    return max;
}
1
2
3
4
5
6
7

# 05. 两解法对比

维度 双变量 DP 双向扫描
时间复杂度 O(N) O(N)
空间复杂度 O(1) O(1)
直观性 ⭐⭐⭐ ⭐⭐⭐⭐
泛化性 可泛化到含0场景 需要特殊处理0

# 06. A012 vs A013 对比

flowchart LR
    subgraph A012["最大子数组和"]
        S1["cur = max(x, cur+x)"]
        S2["只需一个变量"]
        S3["加法单调"]
    end
    subgraph A013["乘积最大子数组"]
        P1["curMax/min 双变量"]
        P2["负数交换"]
        P3["乘法可翻转符号"]
    end
1
2
3
4
5
6
7
8
9
10
11
A012 最大和 A013 最大积
状态数 1 个 (cur) 2 个 (curMax, curMin)
符号翻转 不需要 核心技巧
遇 0 自然处理 需注意
难度 ⭐⭐ ⭐⭐

# 07. 总结

核心启示 说明
为什么需要 curMin 负数 × 负数 = 正数,最小值可能翻身
负数交换技巧 用 swap 代替同时计算 max 和 min 的 3-way compare
DP 泛化 不是简单套 Kadane,而是理解"什么因素会影响最优解"

相关题目:A012 最大子数组和、N019 最大正方形

# A014 和为 K 的子数组(Subarray Sum Equals K)

LeetCode 560 · ⭐⭐ · 前缀和+哈希表

"和为 K 的子数组个数"——前缀和与哈希表的黄金组合,面试极高频。

# 01. 题目描述

给定整数数组 nums 和整数 k,统计和为 k 的连续子数组个数。

示例:

输入:nums = [1,1,1], k = 2
输出:2  ([1,1] 出现两次)
1
2

# 02. 题目分析

2.1 前缀和公式

$$sum(i, j) = pre[j] - pre[i-1] = k \Rightarrow pre[i-1] = pre[j] - k$$

遍历到 j 时,查找之前有多少个前缀和等于 pre[j] - k。

2.2 哈希表维护频次

public int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);  // 空前缀和
    int sum = 0, count = 0;
    for (int n : nums) {
        sum += n;
        count += map.getOrDefault(sum - k, 0);
        map.put(sum, map.getOrDefault(sum, 0) + 1);
    }
    return count;
}
1
2
3
4
5
6
7
8
9
10
11

# 03. 代码

Java:

public int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);
    int sum = 0, count = 0;
    for (int n : nums) {
        sum += n;
        count += map.getOrDefault(sum - k, 0);
        map.merge(sum, 1, Integer::sum);
    }
    return count;
}
1
2
3
4
5
6
7
8
9
10
11

Python:

def subarraySum(self, nums: List[int], k: int) -> int:
    cnt = {0: 1}
    s = ans = 0
    for x in nums:
        s += x
        ans += cnt.get(s - k, 0)
        cnt[s] = cnt.get(s, 0) + 1
    return ans
1
2
3
4
5
6
7
8

C++:

int subarraySum(vector<int>& nums, int k) {
    unordered_map<int, int> mp{{0, 1}};
    int sum = 0, ans = 0;
    for (int x : nums) {
        sum += x;
        if (mp.count(sum - k)) ans += mp[sum - k];
        mp[sum]++;
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10

复杂度:时间 O(N),空间 O(N)。

相关题目:A015 长度最小的子数组

# A015 长度最小的子数组(Minimum Size Subarray Sum)

LeetCode 209 · ⭐⭐ · 滑动窗口

滑动窗口的入门题。"右扩累加,满足条件左缩"——这道模板题写熟后,所有滑动窗口题都是它的变体。前缀和+二分是另一种思路。

# 01. 题目描述

找出和 ≥ target 的长度最小的连续子数组,返回其长度。若不存在返回 0。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 的和 = 7 ≥ 7,长度 2 最小。
1
2
3

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1
1
2

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
1
2

约束:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^4

# 02. 题目分析:两种思考路径

2.1 为什么不能用 DP?

A012 的 Kadane 公式是 cur = max(x, cur+x)——因为可以"舍弃"前面的负数。但这里要求 sum ≥ target,不是取最大——没有单调性。

2.2 解法演进

flowchart LR
    A["O(N²) 枚举所有子数组"] --> B["O(NlogN) 前缀和+二分"]
    A --> C["O(N) 滑动窗口"]
    B --> D["适用于有负数的情况"]
    C --> E["适用于全正数<br/>(本题最优)"]
1
2
3
4
5

# 03. 解法一:滑动窗口(O(N)/O(1))

3.1 核心思路

flowchart TD
    A["l=0, sum=0, minLen=∞"] --> B["r = 0..n-1"]
    B --> C["sum += nums[r]"]
    C --> D{"sum ≥ target ?"}
    D -->|YES| E["更新 minLen = min(minLen, r-l+1)"]
    E --> F["sum -= nums[l]; l++"]
    F --> D
    D -->|NO| B
1
2
3
4
5
6
7
8

3.2 全流程演示

target = 7, nums = [2,3,1,2,4,3]

r=0, sum=2:                 2 < 7  → 继续
r=1, sum=5:                 5 < 7  → 继续
r=2, sum=6:                 6 < 7  → 继续
r=3, sum=8:  sum≥7 → minLen=4,  sum-=2 (l=0) → sum=6, l=1
             sum=6 < 7  → 继续
r=4, sum=10: sum≥7 → minLen=4,  sum-=3 (l=1) → sum=7, l=2
             sum=7≥7 → minLen=3, sum-=1 (l=2) → sum=6, l=3  ← minLen更新到3
             sum=6 < 7  → 继续
r=5, sum=9:  sum≥7 → minLen=3,  sum-=2 (l=3) → sum=7, l=4
             sum=7≥7 → minLen=2, sum-=4 (l=4) → sum=3, l=5  ← minLen更新到2!
             sum=3 < 7  → 结束

最终答案:2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3.3 代码

Java:

public int minSubArrayLen(int target, int[] nums) {
    int l = 0, sum = 0, minLen = Integer.MAX_VALUE;
    for (int r = 0; r < nums.length; r++) {
        sum += nums[r];
        while (sum >= target) {
            minLen = Math.min(minLen, r - l + 1);
            sum -= nums[l++];
        }
    }
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
1
2
3
4
5
6
7
8
9
10
11

Python:

def minSubArrayLen(self, target: int, nums: List[int]) -> int:
    l = s = 0
    ans = len(nums) + 1
    for r, x in enumerate(nums):
        s += x
        while s >= target:
            ans = min(ans, r - l + 1)
            s -= nums[l]
            l += 1
    return 0 if ans > len(nums) else ans
1
2
3
4
5
6
7
8
9
10

C++:

int minSubArrayLen(int target, vector<int>& nums) {
    int l = 0, sum = 0, ans = INT_MAX;
    for (int r = 0; r < nums.size(); r++) {
        sum += nums[r];
        while (sum >= target) {
            ans = min(ans, r - l + 1);
            sum -= nums[l++];
        }
    }
    return ans == INT_MAX ? 0 : ans;
}
1
2
3
4
5
6
7
8
9
10
11

复杂度:时间 O(N),空间 O(1)。每个元素最多入窗出窗各一次。

# 04. 解法二:前缀和 + 二分(O(NlogN),选读)

4.1 思路

预处理前缀和 pre[i](单调递增,因为 nums[i] ≥ 0)。对每个起点 i,二分查找第一个 pre[j] - pre[i] ≥ target 的 j。

public int minSubArrayLen(int target, int[] nums) {
    int n = nums.length, minLen = n + 1;
    int[] pre = new int[n + 1];
    for (int i = 0; i < n; i++) pre[i + 1] = pre[i] + nums[i];

    for (int i = 0; i < n; i++) {
        int need = pre[i] + target;                     // 需要的和
        int j = Arrays.binarySearch(pre, need);         // 找第一个 ≥ need 的位置
        if (j < 0) j = -j - 1;                          // binarySearch 的"找不到"语义
        if (j <= n) minLen = Math.min(minLen, j - i);
    }
    return minLen > n ? 0 : minLen;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

4.2 两解法对比

维度 滑动窗口 前缀和+二分
时间复杂度 O(N) O(NlogN)
空间复杂度 O(1) O(N)
适用条件 元素全 ≥ 0 任意正负元素均可
面试推荐 ✅ 加分项

# 05. 滑动窗口 vs 其他题型

flowchart LR
    subgraph 单调条件
        A1["A015: sum ≥ target 缩"]
    end
    subgraph 字符频次
        A2["A016: 重复时缩"]
    end
    subgraph 固定窗口
        A3["A017: 窗口= p.len()"]
    end
    subgraph 全覆盖
        A4["A018: formed==required 缩"]
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
题目 收缩条件 关键技巧
A015 最小子数组 sum >= target 基本模板
A016 无重复最长 出现重复 int[128] 字符集
A017 异位词 窗口固定 频率数组
A018 最小覆盖 formed == required formed 计数

# 06. 总结

核心启示 说明
滑动窗口模板 右扩累加,满足条件左缩——四道题同一框架
O(N) 复杂度 每个元素最多入窗出窗各一次(摊还分析)
前缀和+二分 数组含负数时滑窗失效的备选方案

相关题目:A016 无重复最长、A018 最小覆盖子串

# A016 无重复字符的最长子串

LeetCode 3 · ⭐⭐ · 滑动窗口+集合

面试出镜率 Top 5 的滑动窗口经典题。A015 的收缩条件是 sum >= target,A016 是 出现重复——同一模板,换个条件而已。

# 01. 题目

s = "abcabcbb" → 3 ("abc");s = "pwwkew" → 3 ("wke")

# 02. 题目分析

滑动窗口模板

A015: while (sum >= target)  收缩
A016: while (重复)            收缩
1
2

三种实现效率

方式 查找 O 总复杂度
HashSet + while 左缩 O(1) O(2N)
int[128] 映射最后出现位置 O(1) O(N)
HashMap 映射最后出现位置 O(1) O(N)

# 03. 解法演进

解法一:HashSet + while 左缩

public int lengthOfLongestSubstring(String s) {
    Set<Character> set = new HashSet<>();
    int l = 0, max = 0;
    for (int r = 0; r < s.length(); r++) {
        while (set.contains(s.charAt(r))) set.remove(s.charAt(l++));
        set.add(s.charAt(r));
        max = Math.max(max, r - l + 1);
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10

解法二:int[128] 跳跃收缩(最优)

一跳就到位,不需要 while 慢慢缩:

public int lengthOfLongestSubstring(String s) {
    int[] last = new int[128];
    Arrays.fill(last, -1);
    int l = 0, max = 0;
    for (int r = 0; r < s.length(); r++) {
        char c = s.charAt(r);
        if (last[c] >= l) l = last[c] + 1;  // 直接跳到重复位置后
        last[c] = r;
        max = Math.max(max, r - l + 1);
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 04. 代码

Java(跳跃收缩最优版):

public int lengthOfLongestSubstring(String s) {
    int[] last = new int[128]; Arrays.fill(last, -1);
    int l = 0, max = 0;
    for (int r = 0; r < s.length(); r++) {
        char c = s.charAt(r);
        if (last[c] >= l) l = last[c] + 1; last[c] = r;
        max = Math.max(max, r - l + 1);
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10

Python:

def lengthOfLongestSubstring(self, s: str) -> int:
    last, l, ans = {}, 0, 0
    for r, c in enumerate(s):
        if c in last and last[c] >= l: l = last[c] + 1
        last[c] = r; ans = max(ans, r - l + 1)
    return ans
1
2
3
4
5
6

C++:

int lengthOfLongestSubstring(string s) { vector<int> last(128,-1); int l=0,ans=0; for(int r=0;r<s.size();r++){ if(last[s[r]]>=l) l=last[s[r]]+1; last[s[r]]=r; ans=max(ans,r-l+1); } return ans; }
1

复杂度:O(N)/O(1)。核心:last[c] >= l 时直接跳到 last[c]+1。

# 05. 手推演示

s = "abcabcbb", last = new int[128](-1)

r=0,a: last[a]=-1 < l=0 → last[a]=0, max=1  (窗口: a)
r=1,b: last[b]=-1 < l=0 → last[b]=1, max=2  (窗口: ab)
r=2,c: last[c]=-1 < l=0 → last[c]=2, max=3  (窗口: abc)
r=3,a: last[a]=0 ≥ l=0 → l=1,  last[a]=3, max=3  (窗口: bca)
r=4,b: last[b]=1 ≥ l=1 → l=2,  last[b]=4, max=3  (窗口: cab)
r=5,c: last[c]=2 ≥ l=2 → l=3,  last[c]=5, max=3  (窗口: abc)
r=6,b: last[b]=4 ≥ l=3 → l=5,  last[b]=6, max=3  (窗口: cb)
r=7,b: last[b]=6 ≥ l=5 → l=7,  last[b]=7, max=3  (窗口: b)
1
2
3
4
5
6
7
8
9
10

相关:A015 最小子数组、A018 最小覆盖

# A017 找到字符串中所有字母异位词

LeetCode 438 · ⭐⭐ · 滑动窗口

滑动窗口第三种形态——固定窗口大小。不像 A015/A016 收缩窗口,A017 是"整体平移"。

# 01. 题目

找到 s 中所有是 p 的字母异位词的子串起始索引。s="cbaebabacd", p="abc" → [0,6] ("cba"和"bac")

# 02. 题目分析

滑动窗口四种形态

flowchart LR
    A["滑动窗口"] --> B["不定长·单调<br/>A015 sum≥target"]
    A --> C["不定长·频次<br/>A016 重复时缩"]
    A --> D["固定长<br/>A017 整体平移"]
    A --> E["不定长·全覆盖<br/>A018 formed==required"]
1
2
3
4
5

A017 的特殊之处

窗口大小 = p.length(),从不收缩。每次右移一位:加入 s[r] 频率,减去 s[r-n] 频率。

朴素 vs 优化

flowchart TD
    A["朴素:每次 O(26) 比较 need 和 window"] --> B["O(N·26)"]
    C["优化:维护 diff 计数"] --> D["O(N)"]
1
2
3

优化做法:用一个 diff 变量记录"有多少个字符的频率不匹配"。只在增/减刚好匹配时更新 diff。

# 03. 解法一:朴素滑动窗口(O(N·26))

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> res = new ArrayList<>();
    int m = s.length(), n = p.length();
    if (m < n) return res;
    int[] need = new int[26], window = new int[26];
    for (char c : p.toCharArray()) need[c - 'a']++;
    for (int i = 0; i < n; i++) window[s.charAt(i) - 'a']++;
    if (Arrays.equals(need, window)) res.add(0);
    for (int i = n; i < m; i++) {
        window[s.charAt(i) - 'a']++;
        window[s.charAt(i - n) - 'a']--;
        if (Arrays.equals(need, window)) res.add(i - n + 1);
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Python:

def findAnagrams(self, s: str, p: str) -> List[int]:
    m, n = len(s), len(p)
    if m < n: return []
    need = [0] * 26; window = [0] * 26
    for c in p: need[ord(c) - 97] += 1
    for i in range(n): window[ord(s[i]) - 97] += 1
    res = [0] if need == window else []
    for i in range(n, m):
        window[ord(s[i]) - 97] += 1
        window[ord(s[i - n]) - 97] -= 1
        if need == window: res.append(i - n + 1)
    return res
1
2
3
4
5
6
7
8
9
10
11
12

C++:

vector<int> findAnagrams(string s, string p) { vector<int> res; int m=s.size(),n=p.size(); if(m<n) return res; vector<int> need(26),window(26); for(char c:p) need[c-'a']++; for(int i=0;i<n;i++) window[s[i]-'a']++; if(need==window) res.push_back(0); for(int i=n;i<m;i++){ window[s[i]-'a']++; window[s[i-n]-'a']--; if(need==window) res.push_back(i-n+1); } return res; }
1

复杂度:O(N·26)/O(1)。

# 04. 解法二:diff 计数优化(O(N))

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> res = new ArrayList<>();
    int m = s.length(), n = p.length();
    if (m < n) return res;
    int[] cnt = new int[26];
    for (int i = 0; i < n; i++) { cnt[s.charAt(i) - 'a']++; cnt[p.charAt(i) - 'a']--; }
    int diff = 0;
    for (int c : cnt) if (c != 0) diff++;
    if (diff == 0) res.add(0);
    for (int i = n; i < m; i++) {
        int in = s.charAt(i) - 'a', out = s.charAt(i - n) - 'a';
        if (cnt[in] == 0) diff++; cnt[in]++; if (cnt[in] == 0) diff--;
        if (cnt[out] == 0) diff++; cnt[out]--; if (cnt[out] == 0) diff--;
        if (diff == 0) res.add(i - n + 1);
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 05. 总结

题目 窗口大小 收缩条件 时间
A015 不定长 sum >= target O(N)
A016 不定长 重复 O(N)
A017 固定长 从不收缩 O(N·26) / O(N)
A018 不定长 formed == required O(N)

相关:A018 最小覆盖、A015 最小子数组

# A018 最小覆盖子串(Minimum Window Substring)

LeetCode 76 · ⭐⭐⭐ · 滑动窗口

滑动窗口的终极大 Boss。"formed 计数"是这道题的画龙点睛之笔——避免了每次 O(26) 数组比较。

# 01. 题目描述

返回 s 中涵盖 t 所有字符的最小子串。若不存在返回空串。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
1
2

示例 2:

输入:s = "a", t = "a"
输出:"a"
1
2

示例 3:

输入:s = "a", t = "aa"
输出:""
1
2

约束:

  • 1 <= s.length, t.length <= 10^5
  • s 和 t 由英文字母组成

# 02. 题目分析:从朴素到最优

2.1 朴素思路

枚举所有子串 O(N²),每个子串检查是否覆盖 t 的字符 O(N) → O(N³)。

2.2 滑动窗口思路

flowchart TD
    A["初始化 l=0, formed=0"] --> B["r 右扩,更新窗口频率"]
    B --> C{"某字符频率 == 目标频率 ?"}
    C -->|YES| D["formed++"]
    D --> E{"formed == required ?"}
    C -->|NO| B
    E -->|YES| F["窗口覆盖了所有字符"]
    F --> G["记录最小窗口"]
    G --> H["l 左缩,更新窗口频率"]
    H --> I{"窗口仍覆盖 ?"}
    I -->|YES| G
    I -->|NO| B
1
2
3
4
5
6
7
8
9
10
11
12

2.3 核心技巧:formed 计数(为什么不用 Arrays.equals?)

朴素做法:每次检查窗口是否覆盖 t → Arrays.equals(need, window) → O(26)。

// 朴素:每次 O(26) 比较
while (Arrays.equals(need, window)) { ... }  // ❌ 慢

// 优化:用 formed 计数,O(1) 判断
while (formed == required) { ... }            // ✅ 快
1
2
3
4
5

formed 的含义:有多少个字符已经满足了目标频率。

t = "ABC" → need = {A:1, B:1, C:1} → required = 3

当 window[A] 从 0→1:window[A]==need[A]==1 → formed++ (formed=1)
当 window[B] 从 0→1:window[B]==need[B]==1 → formed++ (formed=2)
当 window[C] 从 0→1:window[C]==need[C]==1 → formed++ (formed=3)

formed == required → 窗口覆盖了所有字符!
1
2
3
4
5
6
7

2.4 窗口滑动全流程演示

s = "ADOBECODEBANC", t = "ABC"
need = {A:1, B:1, C:1}, required = 3

r=0,A: win[A]=1==need[A]=1 → formed=1  (窗口: A)
r=1,D: win[D]=1                          (窗口: AD)
r=2,O: win[O]=1                          (窗口: ADO)
r=3,B: win[B]=1==need[B]=1 → formed=2   (窗口: ADOB)
r=4,E: win[E]=1                          (窗口: ADOBE)
r=5,C: win[C]=1==need[C]=1 → formed=3   (窗口: ADOBEC, formed==3!)

→ 窗口 [0..5]="ADOBEC", len=6
  收缩 l=0,A: win[A]=0<need[A] → formed=2  (窗口: DOBEC)

r=6,O: win[O]=2                          (窗口: DOBECO)
r=7,D: win[D]=2                          (窗口: DOBECOD)
r=8,E: win[E]=2                          (窗口: DOBECODE)
r=9,B: win[B]=2                          (窗口: DOBECODEB)
r=10,A: win[A]=1==need[A] → formed=3    (窗口: DOBECODEBA, formed==3!)

→ 窗口 [1..10]="DOBECODEBA", len=10 (不如6)
  收缩 l=1: D不在t中, win[D]=1, 仍覆盖
  收缩 l=2: O不在t中, win[O]=1, 仍覆盖
  收缩 l=3: win[B]=1==need[B]... 仍=1, formed=3  (窗口: BECODEBA)
  收缩 l=4: E不在t中
  收缩 l=5: win[C]=0<need[C] → formed=2 结束收缩

r=11,N: ...  (窗口: ECODEBAN)
r=12,C: win[C]=1==need[C] → formed=3  (窗口: ECODEBANC, formed==3!)

→ 窗口 [5..12]="ECODEBANC", len=8
  收缩...最终找到 [9..12]="BANC", len=4 ← 最优!

最终答案:"BANC"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 03. 代码

Java:

public String minWindow(String s, String t) {
    int[] need = new int[128];
    for (char c : t.toCharArray()) need[c]++;
    int required = 0;
    for (int n : need) if (n > 0) required++;

    int l = 0, formed = 0;
    int[] window = new int[128];
    int minLen = Integer.MAX_VALUE, start = 0;

    for (int r = 0; r < s.length(); r++) {
        char c = s.charAt(r);
        window[c]++;
        if (need[c] > 0 && window[c] == need[c]) formed++;

        while (formed == required) {
            if (r - l + 1 < minLen) {
                minLen = r - l + 1;
                start = l;
            }
            char lc = s.charAt(l);
            window[lc]--;
            if (need[lc] > 0 && window[lc] < need[lc]) formed--;
            l++;
        }
    }
    return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Python:

def minWindow(self, s: str, t: str) -> str:
    need = [0] * 128
    for c in t: need[ord(c)] += 1
    required = sum(1 for n in need if n > 0)

    window = [0] * 128
    l = formed = 0
    min_len, start = float('inf'), 0

    for r, c in enumerate(s):
        window[ord(c)] += 1
        if need[ord(c)] > 0 and window[ord(c)] == need[ord(c)]:
            formed += 1
        while formed == required:
            if r - l + 1 < min_len:
                min_len = r - l + 1
                start = l
            lc = s[l]
            window[ord(lc)] -= 1
            if need[ord(lc)] > 0 and window[ord(lc)] < need[ord(lc)]:
                formed -= 1
            l += 1

    return "" if min_len == float('inf') else s[start:start + min_len]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

C++:

string minWindow(string s, string t) {
    vector<int> need(128), window(128);
    for (char c : t) need[c]++;
    int required = 0;
    for (int n : need) if (n > 0) required++;

    int l = 0, formed = 0, minLen = INT_MAX, start = 0;
    for (int r = 0; r < s.size(); r++) {
        char c = s[r]; window[c]++;
        if (need[c] > 0 && window[c] == need[c]) formed++;
        while (formed == required) {
            if (r - l + 1 < minLen) { minLen = r - l + 1; start = l; }
            char lc = s[l]; window[lc]--;
            if (need[lc] > 0 && window[lc] < need[lc]) formed--;
            l++;
        }
    }
    return minLen == INT_MAX ? "" : s.substr(start, minLen);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

复杂度:时间 O(N),空间 O(1)(128个字符)。

# 04. 滑动窗口题型总览

flowchart LR
    A["滑动窗口四种形态"] --> B["不定长<br/>单调条件"]
    A --> C["不定长<br/>字符频次"]
    A --> D["固定长"]
    A --> E["不定长<br/>全覆盖"]

    B --> B1["A015 最小子数组"]
    C --> C1["A016 无重复最长"]
    D --> D1["A017 异位词"]
    E --> E1["A018 最小覆盖"]
1
2
3
4
5
6
7
8
9
10
题目 窗口类型 收缩条件 优化技巧
A015 不定长 sum >= target 基本滑动窗口
A016 不定长 set.contains(c) int[128] 代替 Set
A017 固定长 窗口 == p.len() 频率数组直接比较
A018 不定长 formed == required formed 计数

# 05. 总结

核心启示 说明
formed 计数 O(1) 判断窗口覆盖,代替 O(26) 数组比较
双指针+哈希 窗口增长右扩,满足条件左缩
128 数组 字符集小,用数组代替 HashMap 更高效
滑动窗口模板 右扩更新 → 满足条件时左缩 → 更新最优

相关题目:A015 最小子数组、A016 无重复最长、A017 异位词

# A019 字符串相乘(Multiply Strings)

LeetCode 43 · ⭐⭐ · 大数模拟

大数乘法的核心在于"位置映射"。num1[i] × num2[j] → res[i+j+1] 这个公式一旦理解,大数乘法迎刃而解。

# 01. 题目

"123" × "456" = "56088"。不能使用 BigInteger。

# 02. 题目分析

2.1 竖式乘法回顾

     1 2 3
   × 4 5 6
   -------
     7 3 8   ← 123×6
   6 1 5     ← 123×5
 4 9 2       ← 123×4
 ---------
 5 6 0 8 8
1
2
3
4
5
6
7
8

2.2 关键洞察:位置映射

num1[i] 和 num2[j] 的乘积影响 res[i+j](进位)和 res[i+j+1](个位)。

$$num1[i] \times num2[j] \rightarrow \begin{cases} res[i+j] &\text{(进位)} \ res[i+j+1] &\text{(个位)} \end{cases}$$

结果数组长度最大为 m + n。

2.3 推导过程

flowchart TD
    A["初始化 res[m+n] = 0"] --> B["i 从 m-1 到 0"]
    B --> C["j 从 n-1 到 0"]
    C --> D["mul = num1[i] × num2[j]"]
    D --> E["sum = mul + res[i+j+1]"]
    E --> F["res[i+j+1] = sum % 10"]
    F --> G["res[i+j] += sum / 10"]
1
2
3
4
5
6
7

# 03. 代码

Java:

public String multiply(String num1, String num2) {
    if ("0".equals(num1) || "0".equals(num2)) return "0";
    int m = num1.length(), n = num2.length();
    int[] res = new int[m + n];
    for (int i = m - 1; i >= 0; i--)
        for (int j = n - 1; j >= 0; j--) {
            int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0');
            int p1 = i + j, p2 = i + j + 1;
            int sum = mul + res[p2];
            res[p2] = sum % 10;
            res[p1] += sum / 10;
        }
    StringBuilder sb = new StringBuilder();
    for (int num : res) if (!(sb.length() == 0 && num == 0)) sb.append(num);
    return sb.toString();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Python:

def multiply(self, num1: str, num2: str) -> str:
    if num1 == "0" or num2 == "0": return "0"
    m, n = len(num1), len(num2)
    res = [0] * (m + n)
    for i in range(m - 1, -1, -1):
        for j in range(n - 1, -1, -1):
            mul = (ord(num1[i]) - 48) * (ord(num2[j]) - 48)
            p1, p2 = i + j, i + j + 1
            s = mul + res[p2]
            res[p2] = s % 10
            res[p1] += s // 10
    i = 0
    while i < len(res) and res[i] == 0: i += 1
    return ''.join(str(x) for x in res[i:])
1
2
3
4
5
6
7
8
9
10
11
12
13
14

C++:

string multiply(string num1, string num2) {
    if (num1 == "0" || num2 == "0") return "0";
    int m = num1.size(), n = num2.size();
    vector<int> res(m + n, 0);
    for (int i = m - 1; i >= 0; i--)
        for (int j = n - 1; j >= 0; j--) {
            int mul = (num1[i]-'0') * (num2[j]-'0');
            int p1 = i+j, p2 = i+j+1;
            int sum = mul + res[p2];
            res[p2] = sum % 10;
            res[p1] += sum / 10;
        }
    string ans;
    for (int x : res) if (!(ans.empty() && x == 0)) ans += (x + '0');
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

复杂度:O(MN)/O(M+N)。

# 04. 总结

核心要点 说明
位置映射 num1[i] × num2[j] → res[i+j+1]
进位处理 先加到 res[p2],再向前进位到 res[p1]
结果长度 最多 m+n 位
去掉前导零 跳过开头的 0

相关题目:P004 字符串相加

# A020 字符串转换整数 (atoi)

LeetCode 8 · ⭐⭐ · 模拟/状态机

实现 C 语言 atoi 的核心逻辑。考察边界条件处理能力和代码鲁棒性。

# 01. 题目描述

实现 myAtoi(string s) 函数,将字符串转为 32 位有符号整数。

规则:①跳过前导空格 ②判断符号 ③读入数字直到非数字或结尾 ④溢出时返回 INT_MAX/INT_MIN。

示例:

输入:"42"            → 42
输入:"   -42"        → -42
输入:"4193 with words" → 4193
输入:"-91283472332"  → -2147483648
1
2
3
4

# 02. 题目分析

溢出判断

res > INT_MAX/10 或 res == INT_MAX/10 && digit > 7 时溢出。

# 03. 代码

Java:

public int myAtoi(String s) {
    int i = 0, n = s.length(), sign = 1, res = 0;
    while (i < n && s.charAt(i) == ' ') i++;
    if (i < n && (s.charAt(i) == '+' || s.charAt(i) == '-'))
        sign = s.charAt(i++) == '-' ? -1 : 1;
    while (i < n && Character.isDigit(s.charAt(i))) {
        int d = s.charAt(i++) - '0';
        if (res > Integer.MAX_VALUE / 10 ||
           (res == Integer.MAX_VALUE / 10 && d > 7))
            return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
        res = res * 10 + d;
    }
    return res * sign;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Python:

def myAtoi(self, s: str) -> int:
    s = s.lstrip()
    if not s: return 0
    sign = 1
    i = 0
    if s[0] == '-' or s[0] == '+':
        sign = -1 if s[0] == '-' else 1
        i = 1
    res = 0
    INT_MAX, INT_MIN = 2**31 - 1, -2**31
    while i < len(s) and s[i].isdigit():
        res = res * 10 + (ord(s[i]) - 48)
        if res * sign > INT_MAX: return INT_MAX
        if res * sign < INT_MIN: return INT_MIN
        i += 1
    return res * sign
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

C++:

int myAtoi(string s) {
    int i = 0, n = s.size(), sign = 1;
    long res = 0;
    while (i < n && s[i] == ' ') i++;
    if (i < n && (s[i] == '+' || s[i] == '-'))
        sign = s[i++] == '-' ? -1 : 1;
    while (i < n && isdigit(s[i])) {
        res = res * 10 + (s[i++] - '0');
        if (res * sign > INT_MAX) return INT_MAX;
        if (res * sign < INT_MIN) return INT_MIN;
    }
    return res * sign;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

复杂度:时间 O(N),空间 O(1)。

# 04. 总结

边界 处理方式
前导空格 while(s[i]==' ') i++
符号 判断 + / -
非数字 停止解析
溢出 res > INT_MAX/10 提前判断
#算法
上次更新: 2026/06/17, 12:46:05
README
链表算法题合集

← README 链表算法题合集→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式