力扣刷题笔记 #01 数组
本文最后更新于:2025年3月9日 晚上
本文包含「数组」类型题中的:哈希表、前缀和/差分、离散化、排序/选择、双指针等技巧。持续更新中。
题目描述:
方法1:
方法2:
方法3:
坑点:
哈希表
1. 两数之和 (E+)
题目描述:给定一个数组,在数组中找出两个整数,使两数之和为指定值,并返回两数下标。
方法1:暴力枚举,两层循环顺序遍历,复杂度
方法2:哈希表,注意到内层循环本质是「快速查找目标值的索引」,用哈希表可
坑点:当数组中有两个 key 相同时,如果先建哈希表,则会发生覆盖,然而由于每种输入只会对应一个答案,因此此时这两个 key 就必定是结果。解决方法是边建表边遍历,每次查找的都是遍历过的值。
128. 最长连续序列 (M)
题目描述:给定一个无序数组 nums
,找出其中可以组成的最长连续序列(不要求序列元素在原数组中连续)的长度。
方法1:排序,先进行快速排序,再用一个指针扫描数组即可。时间复杂度
方法2:哈希集合,先将原数组存入哈希集合,既能去重又能快速定位元素。遍历哈希集合,对每个
方法3:哈希表 + 并查集,维护每个集合的大小,对于元素
169. 多数元素 (E)
题目描述:给定一个大小为
方法1:哈希表,直接统计每个元素出现的次数,同时维护最大值,时间复杂度
方法2:排序,由于众数次数超过了一半,下标为
方法3:随机选取一个数,遍历检查,如果不是再重新选。期望次数为
方法4:半数投票法,顺序扫描,假设有一个擂台,
1124. 表现良好的最长时间段 (M)
题目描述:给定一个仅包含
方法1:前缀和,预处理得到前缀和之后,暴力枚举区间左右端点,时间复杂度
方法2:前缀和连续性 + 哈希表,只枚举区间右端点,此时我们希望找到最小的
由于
,所以要根据 是否大于零来讨论:
- 如果
,则左端点最小就是 ,此时的最长长度为 。 - 如果
,则要在其左边找到满足 的位置。为什么是 而不是更小的数?因为 ,而前缀数组每次只能 ,所以 一定比 更靠左!
前缀和/差分
前缀和需要
- 这类题目通常要求「访问、找到、计数」一个连续子数组,并且统计量具有前缀性质。
- 此外,前缀和还有其他变式:前缀和的模、前缀积、前缀出现次数等。
- 约定:
pre[i+1]=pre[i]+nums[i]
表示nums[0]
到nums[i]
之和,pre[0]
初始化为 0,查询的时候 用pre[j]-pre[i]
。
差分数组也需要
- 这类题目通常要求「改变、划分」多个连续子数组,并且每次对多个连续元素执行操作。
- 如果要求除了加减外的其他操作,则应该考虑单次操作
的线段树维护。 - 约定:
diff[i]
表示nums[i]-nums[i-1]
,diff[0]
表示nums[0]
,还原的时候nums[i]=nums[i-1]+diff[i]
。
有时候需要二维数组的前缀和、差分,用于求二维数组中给定矩阵区间和,或者给矩阵区间的每个值都加上常数,下面给出常用代码。
1 |
|
2406. 将区间分为最少组数 (M)
题目描述:给定一些整数区间,将其划分为若干区间组,使得每组区间都不相交(区间端点也不能重叠)。返回最少需要划分的组数。
方法1:贪心 + multiset,先按区间起始值排序,在 multiset 中记录已有区间组的末尾值,每次新区间加入时在集合中找到可以插入的最大值(最小化碎片,但实际上不影响),并更新该值为新区间的末尾值(multiset 自动排序)。时间复杂度为
方法2:贪心 + 优先队列,由于排过序,每次新区间加入时可以直接插入任意一个可插入的区间,因为无论选哪个,下一个新区间面临的选择还是那些。因此选择已有的最小末尾值(最容易插入的),维护最小堆,时间复杂度为
方法3:差分 + 前缀和,如果有重叠数字则必须分到不同区间组,最高的重叠次数就是答案。从前往后枚举,用差分数组记录对应每个区间的计数,最后通过前缀和还原数组取最值即可。时间复杂度为
拓展:本题的另一个问法 253. 会议室 II,给定
238. 除自身以外数组的乘积 (M)
题目描述:给定一个整数数组 nums
,返回另一个答案数组,要求每个位置为 nums
中除 nums[i]
外其余各元素的乘积。保证不会溢出,禁止使用除法。
方法1:尝试使用除法,直接计算所有元素的乘积再除以对应位置的值,但在数组中有零元素时会出错,需要特判。
方法2:前后缀乘积,预处理出两个前后缀数组,每个位置直接算出答案。时空复杂度均为
方法3:前后缀 + 在线计算,正向遍历时用
以下为「前缀和 + 哈希表计数」类型题,这类题目通常要求统计一类连续子数组的个数,需要枚举一侧端点。
560. 和为 K 的子数组 (M)
题目描述:给定整数数组 nums
和整数 k
,统计并返回该数组中和为 k
的连续子数组的个数 。
方法1:暴力,本题不能用滑动窗口的原因是数组有正有负,不能提前移动左指针,因此使用两层循环。外层枚举左端点,内层枚举右端点,区间和累加计算,复杂度
方法2:前缀和 + 哈希表,枚举左端点
注意,由于数组有正有负,前缀和为
的数组可能在 的左侧,记得要扣除这部分。实际操作中只需要在枚举完一个左端点 ,将其前缀和 的次数减去 即可,确保枚举过的左侧点不再参与计算。
方法3:空间优化,先枚举右端点,在枚举
523. 连续的子数组和 (M)
题目描述:给定整数数组 nums
和整数 k
,判断是否含有总和为 k
的倍数、长度至少为
方法1:暴力,两层循环枚举左端点和右端点,区间和累加,时间复杂度
方法2:前缀和 + 哈希表,由同余定理,哈希表只要存储
方法3:前缀和 + 哈希集合,由于本题距离下限固定为
坑点:方法 2 中要初始化
1248. 统计「优美子数组」(M)
题目描述:给定整数数组 nums
和整数 k
,如果某个连续子数组中恰好有 k
个奇数数字,则称为「优美子数组」。
方法1:暴力,两层循环枚举左端点和右端点,区间和累计,时间复杂度
方法2:乘法原理,建立一个纯奇数的
方法3:前缀次数 + 哈希表,哈希表记录前缀次数为
坑点:本题可能会让人联想到滑动窗口,因为奇数数字是线性递增的,实际上也是可以的。但对于同一个右端点会存在多个适合的左端点,所以在枚举完右端点后还需要枚举左端点——应使用定界法。
437. 路径总和 III (H)
题目描述:给定一个二叉树的根节点和一个目标整数,求二叉树中节点之和为目标值的路径数目。这里的路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的。
方法1:暴力 DFS,DFS 枚举每个节点作为路径头时的情况,再向下 DFS 到叶节点,总共有两层 DFS,时间复杂度
方法2:前缀和 + 哈希表,DFS 遍历时记录根节点到当前节点的和 hash[0]=1
,如果从根节点到当前节点恰好等于目标,差值就是
坑点:本题的方法 1 中,记忆化数组并不能优化时间复杂度,因为需要存储的关键字有两个,必须用 pair,且只能用 map 映射,常数较大。而且两个关键字组成的目标域太过稀疏,重复访问的概率很小。
2488. 统计中位数为 K 的子数组 (H)
题目描述:给定一个
方法1:暴力,找出中位数,从中间开始向两边遍历,同时累计大于、小于
方法2:前缀次数 + 哈希表,
方法3:正负性转换 + 哈希表,大于
离散化
解决数据范围大但样本点少的情况,将大范围的样本(连续取值)映射到小范围(离散取值)。
1 |
|
850. 矩形面积 II (H)
题目描述:给定一组 X-Y 轴对齐的矩形(左上角和右下角坐标),计算平面中所有矩形覆盖的总面积,重叠部分只算一次。
方法1:离散化 + 扫描线,对所有矩形的左右边界排序,横坐标离散化成
扫描线算法:用一条竖直的直线从平面的最左端扫到最右端,扫描过程中直线会被给定的矩形覆盖,对覆盖的线段进行积分。每次扫到矩形的左边界时,覆盖的长度可能会增加;扫到矩形的右边界时,覆盖的长度可能会减少。
离散化,分别对横纵坐标处理:
由于矩形的横坐标是连续值(
),而矩形的个数 ,可以将其转化为 个离散的横坐标。 纵坐标将扫描线划分作
个线段(共有 个离散的坐标,去掉两端的射线),因此可以用两个长度为 的数组维护。 表示第 个线段被覆盖的次数, 表示第 个线段的长度。遇到一个左边界时,我们就将左边界覆盖到的线段对应的 全部加 ;遇到一个右边界时,我们就将右边界覆盖到的线段对应的 全部减 。处理完一批后, 如果大于 ,说明它被覆盖,将 累加,即可得到「覆盖的线段长度」。
方法2:离散化 + 扫描线 + 线段树,维护
双指针
常见的有分段双指针、并行双指针、快慢双指针、对撞双指针、滑动窗口双指针:
- 分段双指针:两个指针将序列分为三段,最右边是未访问的,左边和中间的序列往往具有特定属性。
- 并行双指针:两个指针分别遍历两个序列,遍历到的值进行某种操作,并使其中一个指针前进。
- 快慢双指针:常用于链表中,解决需要「计数」的问题。
- 对撞双指针:两个指针从两端开始向中间靠拢,通常使用
while(left < right)
。 - 滑动窗口双指针:关注窗口内的值,题目中通常会有「最长连续子 XX」,关键在于「连续」。
283. 移动零 (E)
题目描述:将数组 nums
中的所有 0
移动到数组的末尾,同时保持非零元素的相对顺序,要求原地操作。
方法1:冒泡,不改变相对顺序,将所有 0
冒泡移动末尾,复杂度
方法2:分段双指针,左指针左边均为处理过的非零数,右指针左边直到左指针本身均为零,右指针右边是未处理的序列。右指针向右移动,每次遇到非零数,就和左指针指向的零交换,同时左指针右移。复杂度
方法3:直接遍历,遍历时计数 0
的个数,每次遇到非零数直接往前填充,最后再填充所有零。复杂度
11. 盛最多水的容器 (M)
题目描述:给定一个长度为 n
的数组表示 n
条垂线的高,找出其中两条线,使它们构成的容器能装最多的水。
方法1:暴力,两层循环,先选中左边界,再遍历右边界,时间复杂度
方法2:贪心 + 对撞双指针,双指针从两端开始遍历,选定两个边界中的短板,向中间收窄一格。时间复杂度
如果长板向内收窄则不可能得到更优解,因为「短板」只可能更短,而横轴也变短了。
15. 三数之和 (M)
题目描述:给定一个整数数组,返回其中所有不重复的三元组,使其和为零。三元组中可以有相同数。
方法1:排序 + 暴力,将数组从小到大排序后,三层循环遍历,且每层循环都大于上一层循环枚举的元素,可以保证三元组 a<=b<=c
,不会有其他顺序。同时对每一层循环跳过相同的数,否则也会重复。时间复杂度
方法2:排序 + 二分查找,方法 1 的第三层遍历中,目标数值是确定的,因此可以直接二分查找 binary_search(num+b_idx, num+n, c)
,时间复杂度
方法3:排序 + 对撞双指针,方法 1 的第三层遍历中,如果 b
匹配到了一个元素 c
,则下一次 b<=b'
时一定有 c'<=c
,因此无需完整遍历,只需用双指针从两端向中靠拢。时间复杂度
拓展:这题的解法也可以照搬到 四数之和,只是多了一层循环。
16. 最接近的三数之和 (M)
题目描述:给定一个整数数组和一个目标值 target
,从数组中选出选出三个整数,使它们的和与 target
最接近。
方法1:暴力,三层循环遍历,时间复杂度
方法2:排序 + 对撞双指针,外层循环不变,内层循环改用对撞双指针,同时维护一个变量
五种情况分别为:和小于
时左指针移动,和大于 时右指针移动;其他情况需要更新 值,并且根据和与 的关系判断三种情况。
方法3:排序 + 对撞双指针,不需要方法 2 那么麻烦,直接仿照上一题,每次先判断
75. 颜色分类 (M)
题目描述:给定一个由数字 0 1 2
组成的数组,代表三种颜色(荷兰国旗问题),按照 0<1<2
的顺序原地排序。
方法1:单指针两次遍历,用指针维护「已归位」元素下标,第一次遍历找出数字
方法2:分段双指针,用指针
方法3:对撞双指针,左指针
此时遍历用的迭代变量可以理解为
,因为本题的目的就是划分三个区间,用三个指针是最符合直觉的。
42. 接雨水 (H)
题目描述:给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
方法1:暴力,对于每个坐标,其能接的水量取决于左右两边最高的柱子中较矮者,两层循环遍历,复杂度
方法2:DP 预处理最大值,两次扫描,记录每个柱子的左右两边最大值,再扫描累计,时空复杂度均为
方法3:单调不增栈 + 模拟,每遍历到一个柱子,如果小于等于栈顶元素则入栈,如果更大则说明前面的柱子可以形成水洼,则依次将所有较小数弹出,并计算栈顶元素和新柱子的距离差值(表示水洼的左右墙壁,如果此时栈空,则表示没有左墙,该水洼不成立),再把新柱子入栈。时空复杂度均为
方法4:贪心 + 对撞双指针,双指针从两端开始遍历,再用两个变量存储左右两边最大值,每次最值较小者向中间收窄一格,同时计算出当前柱子能接的水量(当前柱子与这一侧最值的高度差,该值就是当前柱子能接到的最大水量)。时间复杂度
最值较小侧可以先计算的原因是:每个位置能接的水量取决于左右两边最高的柱子中较矮者,而最值较小侧的指针所指的位置,其所在侧的最值已经是较矮的(确定性),另一侧的最值未知但只会变得更大,所以此时该位置的答案已经可以提前确定。
方法5:分治 + 面积法,最高柱子将全局分为两边,两边各有各自的次高柱子,次高柱子与最高柱子中间的区域显然取值就是「次高柱子 - 当前柱子」,同理以此类推到两端。时间复杂度
因此只需遍历一次,从左往右取左边最大值,从右往左取右边最大值,答案增加「左高 + 右高 - 当前柱子」。这里的减去的「当前柱子」只是为了减去所有柱子的面积(合并在一次遍历中完成)。
最后减去「最高柱子
总长度」,因为两次扫描都会经过最高柱子,经过后再接着扫描的面积无意义,形成了一个大矩形的面积,需要扣除。
坑点:最左边、最右边两个柱子不可能接到水,要跳过,可以直接从 1
遍历到 n-2
。
滑动窗口
双指针中较为困难的一类题:关注窗口内的值,题目中通常会有「连续子 XX」,关键在于「连续」。
- 要求「最长连续子 XX」的长度,此时应该尽量滑动右指针,在窗口不满足要求时滑动左指针,一直到右指针的元素满足要求(如果要求最短则反之)。同时优化判断「窗口是否满足要求」的复杂度。此时维护的窗口类型:
- 哈希集合:维护窗口内有哪些元素;
- 哈希表:维护窗口内元素出现的次数、最后出现的位置;
- 双哈希表:对子串乱序匹配问题,需要一个
need
存储目标次数、一个window
维护窗口内出现的次数; - 单个变量:利用位运算进行存储、或者压位操作。
- 要求「连续子 XX」的个数,此时应该枚举右指针并固定(定界法),然后再枚举左指针,计数左指针的枚举次数。左指针不需要复位,防止退化为
。此时需要维护的变量:- 不该出现的元素的最后出现的位置,代表窗口左指针的最左取值;
- 必须出现的元素的最后出现的位置,代表窗口左指针的最右取值。
3. 无重复字符的最长子串 (M)
题目描述:给定一个字符串 s
,找出其中不含有重复字符的最长连续子串的长度。
方法1:双指针滑动窗口,右指针每前进一格,在窗口内进行 s.find
,如果找到重复字符,则左指针快速前移,复杂度
方法2:滑动窗口 + 哈希集合,左指针多次 erase()
直到 hash.count() == 0
代替 find
,复杂度
方法3:滑动窗口 + 哈希表,比上一个方法多用一个数来存储字符在数组中的下标,左指针快速前移时不需要一直多次 count()
,复杂度
438. 找到字符串中所有字母异位词 (M)
题目描述:给定两个字符串 s
和 p
,找到 s
中所有 p
的字母异位词子串(相同字母重排列形成)。
方法1:定长滑动窗口,窗口长度限定为 p
的长度,枚举窗口的每个位置,用哈希表 + 计数来判断窗口内是否匹配所有字母,每次只考虑一进一出的两个字母,时间复杂度
方法2:滑动窗口 + 双哈希表,哈希表 need
记录目标字符次数,用双指针扫描母串并记录窗口内字符次数 window
,左指针只在 p
的长度时记录答案(此时两个哈希表必然相等)。时间复杂度
30. 串联所有单词的子串 (H)
题目描述:给定一个字符串 s
和一个字符串数组 words
。words
中所有字符串长度相同,串联子串指的是将 words
中的所有字符串以任意顺序排列串联起来,找到 s
中可能出现的串联子串的起始索引。
方法:滑动窗口 + 双哈希表,跟上一题一模一样的操作,区别在于此时的单位由字母变成了单词,因此不能再逐单词前移。考虑到每个字母都有可能充当单词的起点,最好在外层循环就将起点定好,内层循环再滑动窗口。时间复杂度
2401. 最长优雅子数组 (M)
题目描述:正整数组成的数组 nums
中找出最长连续子数组,使其满足子数组中所有元素两两 &
结果等于 0。
方法:滑动窗口 + 位运算,窗口内所有元素取 |
使二进制 1 位合并,右侧元素只需和整体进行 &
就能判断是否加入窗口,如果不能加入,则左侧元素需要弹出,使用 ^
运算去除二进制 1 位。
使用异或必定可以去除:因为窗口内任何二进制位上的 1 必定只出现一次(满足优雅子数组)。
76. 最小覆盖子串 (H)
题目描述:给定字符串 s
和 t
,找出 s
中涵盖 t
所有字符的最小子串,顺序可以打乱。
方法1:滑动窗口 + 双哈希表,哈希表 need
记录目标字符次数,用双指针扫描母串并记录窗口内字符次数 window
。与前几题不一样的是,本题的目标是满足要求的最小子串,因此要在窗口满足要求时尽量滑动左指针,左指针在
方法2:滑动窗口 + 单哈希表,哈希表 need
记录距离目标字符剩余次数,只有遇到 t
中存在的字符时才更新窗口,用
795. 区间子数组个数 (M)
题目描述:给定一个整数数组 nums
和两个整数:left
及 right
。找出连续、非空且最大值在 [left,right]
内的子数组,统计个数。
方法1:单调栈 + 乘法原理,需要找到每个数作为最大值时的两边边界(一边大于、一边大于等于,避免重复计算),正好可以用单调栈 + 反用同时满足,乘法计算出个数贡献,时空复杂度均为
方法2:定界法,显然 >right
的数不能包含,记上一个出现的位置为
坑点:使用单调栈时,最后栈中剩余的元素可以认为是右侧没有更大的值,因此右边界需要直接取
2817. 统计定界子数组的数目 (H)
题目描述:给定一个整数数组 nums
和两个整数 minK
以及 maxK
,若一个连续子数组中的最小值等于 minK
,最大值等于 maxK
,则称为定界子数组。返回定界子数组的数目。
方法1:分治 + 滑动窗口双指针,首先去掉范围之外的数,将原数组分为若干个子数组,再对每个子数组讨论。每个子数组采用双指针扫描,时间复杂度
对每个子数组,先枚举右端点,右指针滑动并计数
miCnt
和mxCnt
,当miCnt > 0 && mxCnt > 0
时,说明右端点已经满足,此时左端点有多种取值。枚举左端点,左指针滑动并减少
miCnt
和mxCnt
,当不满足条件时,左端点枚举过的距离就是该右端点对应的定界子数组个数。继续枚举下一个右端点。对于下一个右端点,即使滑动窗口之间不满足
miCnt > 0 && mxCnt > 0
,也可以增加左端点枚举过的距离,因为这段距离中任何一个左端点都可以和当前右端点构成定界子数组。
方法2:定界法,记录上一个范围之外的数的下标
坑点:不要试图将两边端点的取值范围找出来后相乘,因为这题每个点有两种角色,需要都满足时才能产生贡献。
原地置换
原地置换是数组中经典的套路题,通常要求使用空间复杂度为
1 |
|
剑指 Offer 03. 数组中重复的数字 (E)
题目描述:一个含
方法1:哈希集合,记录每个数字,当哈希冲突时即可返回,时空复杂度均为
方法2:原地置换 + 哈希,由于每个数都在区间内,考虑利用原数组哈希节省空间。将每次遇到的数字 a
放到 nums[a]
,如果该位置已经有 a
则说明 a
就是重复的数字。空间复杂度
448. 找到所有数组中消失的数字 (E)
题目描述:一个含
方法1:哈希表,用一个长为
方法2:原地置换 + 哈希,由于原数组的长度就是 a
放到 nums[a]
,如果该位置已经有 a
则停下。整个数组遍历完后,每个索引上非对应值者就是未出现的数字。空间复杂度
189. 轮转数组 (M)
题目描述:给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
方法1:辅助数组,用一个辅助数组遍历 + 拷贝,下标 i
的元素放到下标 (i+k)%n
的位置,时空复杂度均为
方法2:原地置换,相距 swap
循环即可完成。总共需要 gcd(k,n)
个置换环。空间复杂度
方法3:镜像翻转,先将所有元素翻转,这样尾部的 k%n
个元素就被移至数组头部,然后我们再翻转 [0,k%n−1]
区间的元素和 [k%n,n−1]
区间的元素即能得到最后的答案。空间复杂度
287. 寻找重复数 (M)
题目描述:给定一个包含
方法1:二分答案,定义
方法2:二进制,预处理
方法3:抽象成环形链表寻找入口,快慢指针,先用
方法4:原地置换 + 哈希,跟 剑指 Offer 03. 数组中重复的数字 一模一样,比前面的方法都简单。
41. 缺失的第一个正数 (H)
题目描述:给你一个未排序的整数数组 nums
,找出其中没有出现的最小的正整数。
方法1:哈希集合,遍历数组放入集合,由于答案只可能是
方法2:原地置换 + 哈希,将可能是答案的元素交换到对应下标占位,最后顺序访问数组,第一个不与下标对应的元素就是答案。如果都对应,则答案为
坑点:本题在判断不应交换时的条件较多,容易遗漏:
- 超出
的不用管; - 如果当前元素已经在其对应下标,也不应交换;
- 如果当前元素的对应下标已经被占位(例如
[1,1]
,此时第一个位置已经被占),也不应交换。