面试算法高频总结:子数组和子序列到底有什么区别?
刷算法题时,很多人都会被这几个概念绕晕:子数组、子串、子序列、子集。
它们名字很像,但定义、做法、题型特征都不一样。
如果一开始没分清,后面做题很容易方向跑偏。
这篇文章就系统总结一下:子数组和子序列到底有什么区别,它们常见的题型分别怎么做。
目录
- 一、先说结论
- 二、什么是子数组?
- 三、什么是子序列?
- 四、字符串里的“子串”和“子序列”也一样
- 五、子数组和子序列的本质区别
- 六、最容易混淆的地方
- 七、怎么快速判断一道题是子数组还是子序列?
- 八、子数组常见题型与做法
- 九、子序列常见题型与做法
- 十、为什么同样是“子数组题”,方法也不一样?
- 十一、为什么同样是“子序列题”,方法也不一样?
- 十二、面试里最容易踩的坑
- 十三、面试里的快速判断模板
- 十四、总表总结
- 十五、最后总结
一、先说结论
最核心的一句话:
- 子数组 / 子串:必须连续
- 子序列:可以不连续,但必须保持相对顺序
这是区分它们的根本标准。
二、什么是子数组?
子数组(Subarray)是指:
从原数组中选出一个连续区间。
比如数组:
1 | [1, 2, 3, 4] |
它的子数组有:
1 | [1] |
但下面这些不是子数组:
1 | [1,3] |
因为它们不连续。
三、什么是子序列?
子序列(Subsequence)是指:
从原数组中按顺序选出若干元素,可以跳过一些元素,但不能改变原来的相对顺序。
还是这个数组:
1 | [1, 2, 3, 4] |
它的子序列可以有:
1 | [1] |
但是:
1 | [3,2] |
不是子序列,因为顺序变了。
四、字符串里的“子串”和“子序列”
例如字符串:
1 | "abcde" |
1)子串(Substring)
必须连续,比如:
1 | "a" |
"ace" 不是子串,因为不连续。
2)子序列(Subsequence)
可以跳过,比如:
1 | "ace" |
但 "aec" 不是子序列,因为顺序错了。
五、子数组和子序列的本质区别
1)子数组:连续区间问题
子数组的本质是:
选一个左端点和一个右端点,中间整段都要拿。
所以很多子数组题都和下面这些方法有关:
- 滑动窗口
- 双指针
- 前缀和
- 单调队列
- 单调栈
- Kadane 动态规划
2)子序列:选或不选问题
子序列的本质是:
每个位置都要考虑,这个元素要不要选。
所以很多子序列题都和下面这些方法有关:
- 动态规划
- 双指针
- 贪心
- 贪心 + 二分
六、最容易混淆的地方
1)子数组一定是子序列,但子序列不一定是子数组
例如:
1 | [1,2,3,4] |
[2,3]:既是子数组,也是子序列[1,3]:是子序列,但不是子数组
所以可以这样理解:
子数组是“更严格”的子序列。
2)子串一定是子序列,但子序列不一定是子串
例如:
1 | "abcde" |
"bcd":既是子串,也是子序列"ace":是子序列,但不是子串
3)子集又是另一个概念
子集(subset)通常不强调顺序,只强调“选了哪些元素”。
例如集合 {1,2,3} 的子集有:
1 | {} |
但子序列要求顺序不能乱。
所以:
- 子集:选哪些
- 子序列:按原顺序选哪些
- 子数组:连续选一段
七、怎么快速判断一道题是子数组还是子序列?
看到题目时,先问自己一句:
答案里的元素,能不能跳过中间元素?
- 能跳过:大概率是子序列
- 不能跳过:大概率是子数组 / 子串
八、子数组常见题型与做法
1)最大子数组和
题意
找一个连续子数组,使得和最大。
典型做法
动态规划 / Kadane 算法
为什么?
因为数组里通常可能有负数,不能直接用滑动窗口。
核心状态
定义:
1 | dp[i] = 以 nums[i] 结尾的最大子数组和 |
转移:
1 | dp[i] = max(nums[i], dp[i-1] + nums[i]) |
标准代码
1 | class Solution { |
2)和为 K 的子数组
题意
求有多少个连续子数组,它们的和等于 k。
典型做法
前缀和 + 哈希表
为什么?
因为数组里可能有负数,滑动窗口不稳定。
核心思想
如果当前前缀和是 sum,想找和为 k 的子数组,就要看之前是否出现过:
1 | sum - k |
标准代码
1 | class Solution { |
3)长度最小的子数组
题意
找和大于等于 target 的最短连续子数组长度。
典型做法
滑动窗口 / 双指针
为什么?
因为题目通常限定数组元素都是正数,此时区间和有单调性。
标准代码
1 | class Solution { |
4)乘积小于 K 的子数组
题意
求乘积小于 k 的连续子数组个数。
典型做法
滑动窗口
为什么?
数组元素通常为正数,窗口扩张时乘积单调变化。
核心技巧
当右端点固定时,如果当前窗口合法,那么以这个右端点结尾的合法子数组个数就是:
1 | right - left + 1 |
标准代码
1 | class Solution { |
九、子序列常见题型与做法
1)判断 s 是否是 t 的子序列
题意
判断 s 能不能通过删除 t 中一些字符得到。
典型做法
双指针
核心思想
i扫描sj扫描t- 遇到相同字符就让
i++ - 最后看
i是否扫完整个s
标准代码
1 | class Solution { |
2)最长公共子序列 LCS
题意
求两个字符串的最长公共子序列长度。
典型做法
二维动态规划
状态定义
1 | dp[i][j] = text1前i个字符 和 text2前j个字符 的最长公共子序列长度 |
转移
如果当前字符相等:
1 | dp[i][j] = dp[i-1][j-1] + 1 |
如果不等:
1 | dp[i][j] = max(dp[i-1][j], dp[i][j-1]) |
标准代码
1 | class Solution { |
3)最长递增子序列 LIS
题意
找数组中的最长严格递增子序列长度。
做法一:动态规划
定义:
1 | dp[i] = 以 nums[i] 结尾的最长递增子序列长度 |
转移:
1 | dp[i] = max(dp[j] + 1), 其中 j < i 且 nums[j] < nums[i] |
标准代码
1 | class Solution { |
做法二:贪心 + 二分
维护一个 tails 数组,表示不同长度递增子序列的最小结尾值。
1 | class Solution { |
4)不同的子序列
题意
问 s 的子序列中,有多少种不同方式可以构成 t。
典型做法
二维动态规划
状态定义
1 | dp[i][j] = s的前i个字符中,构成t的前j个字符的方案数 |
转移
如果当前字符相等:
1 | dp[i][j] = dp[i-1][j-1] + dp[i-1][j] |
解释:
- 用当前字符匹配
- 不用当前字符匹配
如果不相等:
1 | dp[i][j] = dp[i-1][j] |
初始化
1 | dp[i][0] = 1 |
因为构造空串永远只有一种方法:什么都不选。
标准代码
1 | class Solution { |
十、为什么同样是“子数组题”,方法也不一样?
很多人会问:
既然都是子数组,为什么有的用滑动窗口,有的用前缀和,有的用 DP?
关键在于:题目条件不同。
1)如果数组元素全是正数
通常优先考虑:
- 滑动窗口
- 双指针
因为区间和、区间积通常具备单调性。
代表题:
- 长度最小的子数组
- 乘积小于 K 的子数组
2)如果数组里可能有负数
通常要警惕:
- 滑动窗口可能失效
这时候更常考虑:
- 前缀和
- 哈希表
- 动态规划
代表题:
- 最大子数组和
- 和为 K 的子数组
十一、为什么同样是“子序列题”,方法也不一样?
关键要先看题目在问什么。
1)判断能不能匹配
通常用:
- 双指针
代表题:
- 判断
s是否是t的子序列
2)求最长
通常用:
- 动态规划
- 或贪心 + 二分优化
代表题:
- LIS
- LCS
3)求方案数
通常用:
- 动态规划
代表题:
- 不同的子序列
十二、面试里最容易踩的坑
1)“最长公共子串”和“最长公共子序列”不是一个题
最长公共子串
要求连续。
最长公共子序列
不要求连续。
这两个名字只差两个字,但做法完全不同。
2)“最长递增子序列”和“最长连续递增序列”不是一个题
最长递增子序列
不要求连续。
最长连续递增序列
必须连续。
一个是子序列问题,一个更像子数组问题。
3)看到“连续”两个字就要敏感
很多题解法差别就差在这两个字:
- 连续 → 子数组 / 子串
- 不连续但顺序不变 → 子序列
十三、面试里的快速判断模板
看到一道题时,先问自己这 3 个问题:
1)要求连续吗?
- 要求连续 → 子数组 / 子串
- 不要求连续 → 子序列
2)元素能不能有负数?
- 有负数 → 滑窗要小心
- 全正数 → 优先想滑窗
3)题目求什么?
- 求是否存在 → 双指针 / 前缀和
- 求最值 → DP / 滑窗 / 贪心
- 求个数 / 方案数 → 前缀和哈希 / DP
十四、总表总结
1)子数组 / 子串
| 类型 | 特点 | 常见做法 |
|---|---|---|
| 子数组 / 子串 | 必须连续 | 滑动窗口、前缀和、双指针、DP |
常见题:
| 题目 | 方法 |
|---|---|
| 最大子数组和 | DP / Kadane |
| 和为 K 的子数组 | 前缀和 + 哈希 |
| 长度最小的子数组 | 滑动窗口 |
| 乘积小于 K 的子数组 | 滑动窗口 |
2)子序列
| 类型 | 特点 | 常见做法 |
|---|---|---|
| 子序列 | 可不连续,但顺序不能变 | DP、双指针、贪心 + 二分 |
常见题:
| 题目 | 方法 |
|---|---|
判断 s 是否是 t 的子序列 |
双指针 |
| 最长公共子序列 LCS | 二维 DP |
| 最长递增子序列 LIS | 一维 DP / 贪心 + 二分 |
| 不同的子序列 | 二维 DP |
十五、最后总结
子数组和子序列看起来只差几个字,但它们本质上代表的是两类完全不同的问题。
子数组 / 子串
关注的是:
一个连续区间。
所以常见方法是:
- 滑动窗口
- 前缀和
- 双指针
- 区间 DP / Kadane
子序列
关注的是:
按原顺序选一些元素。
所以常见方法是:
- 动态规划
- 双指针
- 贪心 + 二分
如果用一句话总结:
连续就想子数组,不连续但顺序不变就想子序列。
这句话几乎能帮你在面试里快速判断大部分题目的方向。
附:一段适合放在结尾的总结话术
很多算法题一眼看上去差不多,但如果先把“连续”和“是否允许跳过元素”这两个问题想清楚,思路会立刻明朗很多。
- 连续区间问题,优先往子数组方向想;
- 可以跳过但顺序不能乱,优先往子序列方向想。
真正把这两个概念吃透之后,滑动窗口、前缀和、动态规划这些方法也会更容易找到切入点。