LeetCode题目总结——一维数组
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 机器学习入门之pandas的使用
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
1. 按规则移动指针
定义一个索引指针,按题目给定要求移动指针并搜索符合条件的元素。
- 0031 - 下一个排列
首先,从数组末尾向左移动指针寻找第一个降序元素a;然后,从a向右移动指针寻找大于a且最小的元素,并与a交换位置;将交换前a位置右侧所有元素倒序。 - 0058 - 最后一个单词的长度
倒序遍历字符串,找到不为空格的字符停止,再继续倒序遍历,找到空格停止,两索引之间的长度即为结果。 - 0066 - 加一
2. 单次遍历并记录
遍历数组,同时使用HashMap记录已遍历过的信息。
- 0001 - 两数之和
1)注意处理重复元素:先判断map中是否存在target - n,再将n存入map
2)若数组有序,可用双指针法,消除map运算负担。(比如处理三数之和、四数之和的情况)
遍历数组,同时统计已遍历过的信息,如最大值、最小值、和等。
-
0055 - 跳跃游戏
维护目前为止可跳跃的最远距离,若最远距离大于数组长度,则返回true,若最远距离小于当前元素位置,则返回false。 -
0121 - 买卖股票的最佳时机
遍历数组,同时记录之前所有元素的最小值。
然后将当前元素与之前元素中的最小值对减,计算当前最大利润,最后取最大利润即可(最大利润小于0则返回0)。
3. 双指针
常见定式1 : 一个主指针与一个副指针,主指针依序遍历数组,副指针根据条件移动。主指针可以是快指针,也可以是慢指针。
# 主指针循环条件
j = 0
i = 0
ans = ...
while i < N and j <= N:
# 副指针移动与处理
while ...:
j += 1
# 结果检测
ans = ...
# 主指针移动前处理
...
i += 1
return ans
- 0003 - 无重复字符的最长字串
主指针为快指针,依次遍历数组并记录每个字符最后一次出现的位置;副指针为慢指针,根据记录的字符最后一次出现位置进行跳转。 - 0076 - 最小覆盖子串
常见定式2 : 两指针分别指向数组的左右侧,两指针基于贪心策略相向运动,直至相遇。
# 左右指针初始化
l = 0
r = len(nums) - 1
while l < r:
# 结果检测
ans = ...
# 指针移动
if cond1(): l += 1
if cond2(): r -= 1
return ans
- 0011 - 盛最多水的容器
每次移动左右指针中指向线条较短的指针。
其他
- 0075 - 颜色分类
采用单次遍历且不使用额外空间的算法时,需要使用双指针法。
4. 排序后操作
用于最终算法复杂度在nlogn(如n2)以上的算法。
- 0015 - 三数之和
1)固定一个元素,双指针法求该元素后方有序数组的两数之和
2)注意跳过重复元素
3)注意剪枝 - 0016 - 最接近的三数之和
- 0018 - 四数之和
多一层循环的三数之和。 - 0976 - 三角形的最大周长
排序后,倒序查找第一组满足三角形周长条件(A[i] < A[i - 1] + A[i - 2])的连续三元素。 - 0621 - 任务调度器
按任务出现频率排序,从出现频率最多的元素开始,模拟填桶,最终公式:
max(N, (n + 1) * (fs[0] - 1) + add),其中,n为最小任务间隔,N为任务长度,fs为排序后的任务出现频率,add为出现频率最高的任务个数。
5. 单调栈
- 0020 - 有效的括号
- 0042 - 接雨水
维护最小栈,累加当前元素与之前元素之间所能盛水量。
// 栈维护逻辑
while (!s.empty() && height[i] >= height[s.top()]) {
int bottom = height[s.top()];
s.pop();
if (!s.empty()) {
res += (min(height[s.top()], height[i]) - bottom) * (i - s.top() - 1);
}
}
- 0084 - 柱状图中最大矩形
维护最大栈,寻找某元素两侧首先出现小于该元素的位置a,b,选择height * (b - a - 1)中的最大值。
// 栈维护逻辑
while (!stack.empty() && heights[i] < heights[stack.top()]) {
int height = heights[stack.top()];
stack.pop();
res = max<int>(res, height * (i - stack.top() - 1));
}
stack.push(i);
- 0085 - 最大矩形
可转换为多次0084问题进行求解。
6. 二分搜索
对于有序的数组,可以使用二分法将搜索复杂度从n降低至logn。根据题目要求,设计左右边界移动条件。
# 左右边界初始化
l = 0
r = len(nums) - 1
# 结束条件,若右边界移动条件位r = m或左边界移动条件位l = m时,必须使用l < r
while l <= r:
# 防止(l + r)越界
# 若右边界移动条件位r = m时,必须使用:
m = l + (r - l) >> 2
# 若左边界移动条件位l = m时,必须使用:
m = l + (r - l + 1) >> 2
# 终止条件,可无
if nums[m] ...:
...
# 左边界移动条件
if ...
l = m or l = m + 1
# 右边界移动条件
if ...
r = m or r = m - 1
- 0033 - 搜索旋转排序数组
1)首先判断nums[m]位于数组的前半段还是后半段,若nums[m] > nums[i]则位于前半段,否则位于后半段。
2)再分别讨论前半段后半段两种情况下,target于nums[m]的关系,从而确定i,j走向。前半段时,nums[i] <= target < nums[m]则j = m - 1,否则i = m + 1;后半段时,nums[m] > target >= nums[j]则i = m + 1,否则j = m - 1。 - 0034 - 在排序数组中查找元素的第一个和最后一个位置
- 0035 - 搜素插入位置
- 0081 - 搜索旋转排序数组 II
相对于搜索旋转数组 I,本题增加了数组元素可能相等的条件,因此,若nums[m] > nums[i]则位于前半段,若nums[m] < nums[i]则位于后半段,若nums[m] == nums[i]则不确定位于前半段还是后半段,但可以确定nums[i] = nums[m] != target,所有这种情况下就仅将i移动一步即可(i = i + 1)。
7. 动态规划
7.1 单数组一维动态规划
遍历一次数组,动态规划记录以当前元素结尾时的状态。
- 0053 - 最大子序和
# 状态转移方程:
dp[i] = max(nums[i], dp[i] + nums[i])
res = max(res, dp[i])
- 0070 - 爬楼梯
# 状态转移方程:
dp[i] = dp[i - 1] + dp[i - 2]
- 0091 - 解码方法
// 转移方程
if (s[i] == '0') {
if (s[i - 1] == '1' || s[i - 1] == '2') dp[i] = dp[i - 2];
else return 0;
}
else if (s[i - 1] == '1' || (s[i - 1] == '2' && s[i] >= '0' && s[i] <= '6')) {
dp[i] = dp[i - 1] + dp[i - 2];
}
else {
dp[i] = dp[i - 1];
}
- 0096 - 不同的二叉搜索树
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
- 0118 - 杨辉三角
简单的动态规划题目。
// 状态转移方程
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
// 优化后
dp[[j] += dp[j - 1];
- 0119 - 杨辉三角II
- 0120 - 三角形最小路径和
杨辉三角的变种题目,使用动态规划求解。
// 状态转移方程
dp[i][j] += min(dp[i - 1][j], dp[i - 1][j - 1]);
// 优化后
dp[j] += min(dp[j], dp[j - 1])
- 0123 - 买卖股票的最佳时机 III
// 状态转移方程
p1_min = min(p, p1_min);
res1 = max(res1, p - p1_min); // 与0121相同,计算买卖一次的情况下,至某元素为止的最大利润
p2_min = min(p2_min, p - res1); // 对于第二次买卖,当前买入股票的价格,相当于:当前买入价格 - 当前元素之前进行买卖一次的最大收益
res2 = max(res2, p - p2_min); // 第二次买卖的最大收益即为最终收益
7.2. 双数组二维动态规划
遍历两数组的元素对,动态规划记录以每对元素结尾时的状态。注意动态规划表格可以简化为一维形式。
- 0072 - 编辑距离
# 状态转移方程:
if w1[i] == w2[j]:
dp[i][j] = dp[i - 1][j - 1]
else
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
- 0097 - 交错字符串
// 状态转移方程
dp[0][0] = true;
for (int i = 0; i <= M; ++i)
for (int j = 0; j <= N; ++j) {
if (i > 0 && dp[i - 1][j] && s3[i + j - 1] == s1[i - 1]) {
dp[i][j] = true;
}
else if (j > 0 && dp[i][j - 1] && s3[i + j - 1] == s2[j - 1]) {
dp[i][j] = true;
}
}
- 0115 - 不同的子序列
// 状态转移方程
for (auto& d : dp[0]) d = 1;
for (int i = 1; i <= M; ++i)
for (int j = 1; j <= N; ++j)
{
if (s[j - 1] == t[i - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
}
else {
dp[i][j] = dp[i][j - 1];
}
}
7.3. 区间动态规划
- 0005 - 最长回文字串
// 转移方程
if (l == 0) {
dp[i][j] = true;
}
else if (l == 1) {
dp[i][j] = (s.charAt(i) == s.charAt(j));
}
else {
dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]);
}
- 0087 - 扰乱字符串
// 转移方程
for (int k = 1; k < len; ++k) {
if (dp[i][j][k] && dp[i + k][j + k][len - k]) {
dp[i][j][len] = true;
break;
}
if (dp[i][j + len - k][k] && dp[i + k][j][len - k]) {
dp[i][j][len] = true;
break;
}
}
8. 贪心
- 0122 - 买卖股票的最佳时机 II
遍历数组,当prices[i] > prices[i - 1]时,res += prices[i] - prices[i - 1]
9. 树状数组
用于解决区间更新及求和类问题。
// 总共有N个元素
vector<int> tr(N, 0);
// 添加k个元素n
void add(int n, int k) {
for (int i = n; i <= N; i += i &(-i)) {
tr[i] += k;
}
}
// 返回小于等于n的元素个数
int sum(int n) {
int res = 0;
for (int i = n; i > 0; i -= i&(-i) {
res += tr[i];
}
return res;
}
- 5564 - 通过指令创建有序数组
10. 优先级队列
- 周赛221 - 5638 - 吃苹果的最大数目
使用优先级队列,按照坏掉的日期从近到远的顺序存储当前的苹果,每次贪心地吃坏掉日期最近的一个苹果。若坏掉日期小于当前日期,则直接从队列中排除。
特殊算法
Manacher 算法:
- 0005 - 最长回文字串
KMP 算法:
# 建立next数组
nxt = [0] * len(pattern)
k = 0
for i in range(len(pattern)):
while k != 0 and pattern[k] != pattern[i]:
k = nxt[k - 1]
if pattern[k] == pattern[i]:
k += 1
nxt[i] = k
# 字符串匹配
k = 0
for i in range(len(string)):
if k != 0 and pattern[k] != string[i]:
k = nxt[k - 1]
if pattern[k] == string[i]:
k += 1
if k > len(pattern):
# 匹配成功
return True
# 匹配失败
return False
- 0028 - 实现strStr()