2.最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
思路
简单动态规划思路,原问题要求最大和的连续子数组,而对于每个连续子数组,都唯一对应着一个末尾元素。在数组 nums 中,每个元素都可以作为某个子数组的末尾元素。所以我们只用求出以 nums[i] 结尾的最大连续子数组的最大和即可。
定义一个 dp[len] 数组,len 是数组的长度,dp[i] 表示以 nums[i] 结尾的最大子数组和。遍历一次 nums 数组,对于每个元素,判断 (dp[i - 1] + nums[i]) 和 nums[i] 哪个更大,将更大的值赋给dp[i],并实时更新max的值即可。代码如下
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int len = nums.size();
int dp[len + 5]; // dp[i]表示以nums[i]结尾的最大子数组和
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < len; ++i) {
if (nums[i] + dp[i - 1] > nums[i])
dp[i] = nums[i] + dp[i - 1];
else dp[i] = nums[i];
if (dp[i] > max) max = dp[i];
}
return max;
}
};
13.合并区间
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= starti <= endi <= 10^4
思路
对于题目给定的这样一个数组,我们可以先按照左边界进行升序排序。这样操作后,可以合并的区间一定在原来的数组中位置连续。
然后定义一个ans数组存放结果,将ans数组中最后一个元素的右边界(整个ans的右边界)与遍历到的区间的左边界进行比较,选择更新ans的右边界或者将该区间压入ans。具体代码如下
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) return {};
// 先按照左边界排序
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
for (int i = 0; i < intervals.size(); ++i) {
int left = intervals[i][0], right = intervals[i][1];
// 如果merged数组为空,或者与merged的最后一个数组没有重叠部分
if (!merged.size() || merged.back()[1] < left) {
merged.push_back({left, right});
} else { // 有重叠部分,更新merged末尾的右边界(合并)
merged.back()[1] = max(merged.back()[1], right);
}
}
return merged;
}
};
14.最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length
n == t.length
1 <= m, n <= 10^5
s
和t
由英文字母组成
思路
考虑到这是一个查找最小子串问题,我们可以往子集问题上联想,自然而然就会考虑使用哈希表来尝试解题。该问题的一个核心点是我们如何判断当前在字符串s 中的子串包含了字符串 t。对于找字符串 s 中的子串,我们可以用滑动窗口。我们再考虑哈希表,我们创建一个哈希表 hash_t 来存放字符串 t 中出现的字母与其出现的次数(这样定义键值对是考虑了字符串 t 中有重复字符),然后我们再创建另一个哈希表 hash_s 来维护当前字符串 s 中遍历到的子串中的字母及其出现次数。对于拿到的子串,这个子串对应的哈希表中要包含 t 的哈希表中的所有字符,并且对应的个数都不小于 t 的哈希表中各个字符的个数。
当我们拿到包含 t 的子串时,再考虑收缩滑动窗口,得到一个最小的子串。
具体代码如下
class Solution {
public:
// ori记录t字符串,cnt记录s字符串
unordered_map <char, int> ori, cnt;
// 判断是否包含子串
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
// 维护一个t串的哈希表
for (const auto &c: t) {
++ori[c];
}
// 左右指针
int l = 0, r = -1;
// len为当前s中的长度,ansL与ansR记录答案子串的首尾索引
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
// 扩展窗口
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
// 判断子串是否包含t字符串
while (check() && l <= r) {
// 长度更短则更新
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l; // 更新答案首部索引
}
// 尝试收缩滑动窗口
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? "" : s.substr(ansL, len);
}
};
15.轮转数组
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 10^5
-2^31 <= nums[i] <= 2^31 - 1
0 <= k <= 10^5
思路
直接模拟,最后一个测试点数据太大会爆超时
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int len = nums.size();
int i = len - 1;
while (k--) {
while (i > 0) {
swap(nums[i], nums[i - 1]);
i--;
}
i = len - 1;
}
}
};
考虑直接将末尾的 k 个元素扔到数组前面(通过新创建一个数组实现),就能得到答案。但这样的做法有一个前提:数组长度 len 要比 k 更大。所以可以分两种情况解答
- len > k 时采用将末尾的 k 个元素扔到数组前面
- len <= k 时直接模拟
代码如下
class Solution {
public:
void rotate(vector<int>& nums, int k) {
vector<int> ans;
int len = nums.size();
if (len > k) {
int p = len - k;
for (int i = p; i < len; ++i)
ans.push_back(nums[i]);
for (int i = 0; i < p; ++i)
ans.push_back(nums[i]);
nums = ans;
} else {
int i = len - 1;
while (k--) {
while (i > 0) {
swap(nums[i], nums[i - 1]);
i--;
}
i = len - 1;
}
}
}
};
为了代码更简短,可以将两种情况进行合并,我们只需要遍历原数组,将原数组下标为 i 的元素放到新数组下标为 (i + k) mod n 的位置
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
vector<int> newArr(n);
for (int i = 0; i < n; ++i) {
newArr[(i + k) % n] = nums[i];
}
nums.assign(newArr.begin(), newArr.end());
}
};
16.除自身以外数组的乘积
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内
请 **不要使用除法,**且在 O(n)
时间复杂度内完成此题
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 10^5
-30 <= nums[i] <= 30
- 保证 数组
nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内
思路
暴力的思路就是遍历一边 nums 数组,每对于遍历到的每个元素分别计算除它以外的乘积。但看数据范围,一定会超时。考虑问题的性质,这个问题可以通过前缀和的思想解决。
对于 nums 数组中的每个元素,我们可以通过下面的方法计算除它以外的乘积:计算结果分为两部分,以当前元素为界,该元素前半部分与该元素后半部分。在遍历 nums 数组前,我们先行分别计算这两部分的结果存放到数组中,然后再遍历 nums 数组,对于每个取到的元素,分别到前面创建的两个数组里取值相乘即可。代码如下
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> ans;
vector<int> pre_front; // 前缀乘积
vector<int> pre_back; // 后缀乘积
int len = nums.size();
int tmp = 1;
// 计算前缀乘积
for (int i = 0; i < len; ++i) {
tmp *= nums[i];
pre_front.push_back(tmp);
}
tmp = 1;
// 计算后缀乘积
for (int i = len - 1; i >= 0; --i) {
tmp *= nums[i];
pre_back.push_back(tmp);
}
for (int i = 0; i < len; ++i) {
if (i == 0) {
ans.push_back(pre_back[len - 2]);
} else if (i == len - 1) {
ans.push_back(pre_front[len - 2]);
} else {
ans.push_back(pre_front[i - 1] * pre_back[len - 2 - i]);
}
}
return ans;
}
};
17.缺失的第一个正数
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。
提示:
1 <= nums.length <= 10^5
-2^31 <= nums[i] <= 2^31 - 1
思路
先不考虑如何优化,我们要找出没有出现的最小正数,先对给定的数组排序,然后遍历该数组,找到第一个正数。因为事先已经排好序,所以这个一定是最小正数。如果这个正数不是1,那么答案就直接返回1,否则就继续判断。
我们从最小正数的下一位开始,找到两个相邻且两者之差不为1或0的数 nums[i]和nums[i-1],返回 nums[i-1] + 1 即为答案。如果一直找到末尾都没有找到这样的两个数,那么就说明数组中的正整数都连续,返回最后一个元素 + 1 即可。
代码如下
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int len = nums.size();
long long ans;
sort(nums.begin(), nums.end());
// 假设给定数组的正整数部分都连续,答案就是最后一个元素+1
if (nums[len - 1] > 0) {
// 有一个数据点是INT_MAX,这里先强转一下防溢出
ans = (long long)nums[len - 1] + 1;
} else { // 若数组中没有正整数,答案就为1
ans = 1;
}
int min = 1;
int flag; // 记录最小正整数的index
for (int i = 0; i < len; ++i) {
if (nums[i] > 0) {
min = nums[i];
flag = i;
break;
}
}
if (min != 1) {
return 1;
} else { // 寻找不连续的两个正整数
for (int i = flag + 1; i < len; ++i) {
if (nums[i] - nums[i - 1] != 1 && nums[i] - nums[i - 1] != 0) {
ans = nums[i - 1] + 1;
break;
}
}
}
return ans;
}
};
18.矩阵置零
给定一个 m x n
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
提示:
m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-2^31 <= matrix[i][j] <= 2^31 - 1
思路
最直观的方案是使用额外的O(mn)的空间,记录原矩阵中哪些是0,将其所在的行列均置为0即可。代码如下
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size(); // 行数
int n = matrix[0].size(); // 列数
bool isZero[205][205] = {false}; // 判断原来矩阵中哪些是0
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
if (matrix[i][j] == 0) isZero[i][j] = true;
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
if (isZero[i][j]) {
for (int p = 0; p < m; ++p)
matrix[p][j] = 0;
for (int q = 0; q < n; ++q)
matrix[i][q] = 0;
}
}
};
对于记录0的位置,可以进一步减小内存到O(m+n)。我们并不需要用整个矩阵存储为0的坐标,只需要用vector记录为0的坐标即可。代码如下
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size(); // 行数
int n = matrix[0].size(); // 列数
vector<pair<int, int>> flag; // 记录0的坐标
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
if (matrix[i][j] == 0) flag.push_back({i, j});
for (int i = 0; i < flag.size(); ++i) {
for (int p = 0; p < m; ++p)
matrix[p][flag[i].second] = 0;
for (int q = 0; q < n; ++q)
matrix[flag[i].first][q] = 0;
}
}
};
还可以进一步将空间复杂度降为O(1)。我们可以用矩阵的第一行和第一列代替标记数组,但这样会导致原数组的第一行和第一列被修改,无法记录它们原来是否包含0。所以我们需要额外使用两个标记变量分别记录第一行和第一列是否包含0。
实际操作起来,我们先预处理两个标记变量,接着使用其他行和列去处理第一行和第一列,再反过来根据第一行和第一列更新剩余矩阵,最后用两个标记变量更新第一行与第一列即可。
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
int flag_col0 = false, flag_row0 = false;
for (int i = 0; i < m; i++) {
if (!matrix[i][0]) {
flag_col0 = true;
}
}
for (int j = 0; j < n; j++) {
if (!matrix[0][j]) {
flag_row0 = true;
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][j]) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][0] || !matrix[0][j]) {
matrix[i][j] = 0;
}
}
}
if (flag_col0) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
if (flag_row0) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
}
};
19.螺旋矩阵
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100
思路
直接模拟就行,从左往右、从上往下、从右往左、从下往上分别模拟,具体可以参考以下代码,模拟方法比较巧妙
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0) {
return {};
}
vector<int> ans;
// 行的开头和结尾
int lineBegin = 0, lineEnd = matrix[0].size() - 1;
// 列的开头和结尾
int listBegin = 0, listEnd = matrix.size() - 1;
while (1) {
// 从左到右
for (int i = lineBegin; i <= lineEnd; ++i)
ans.push_back(matrix[listBegin][i]);
if (++listBegin > listEnd) break;
// 从上往下
for (int i = listBegin; i <= listEnd; ++i)
ans.push_back(matrix[i][lineEnd]);
if (--lineEnd < lineBegin) break;
// 从右往左
for (int i = lineEnd; i >= lineBegin; --i)
ans.push_back(matrix[listEnd][i]);
if (--listEnd < listBegin) break;
// 从下往上
for (int i = listEnd; i >= listBegin; --i)
ans.push_back(matrix[i][lineBegin]);
if (++lineBegin > lineEnd) break;
}
return ans;
}
};
20.旋转图像
给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
提示:
n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000
思路
经过观察,旋转后的矩阵的每一行的内容,是从原来矩阵每一列的最后一个元素开始一直到第一个元素为止,所以很容易能想到先创建一个新矩阵,再用这种方法遍历原矩阵,将旋转后的结果存储到新矩阵中,最后用新矩阵将原矩阵覆盖掉即可。代码如下
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
vector<vector<int>> tmp(n);
for (int i = 0; i < n; ++i) {
for (int j = n - 1; j >= 0; --j) {
tmp[i].push_back(matrix[j][i]);
}
}
matrix = tmp;
}
};
但题目要求不能新建矩阵,所以下面换一个思路:通过翻转矩阵实现。
注意到目标矩阵可以通过原矩阵进行翻转变换得到,具体翻转过程是先通过水平轴翻转一次,再通过主对角线翻转一次。
水平轴翻转
主对角线翻转
水平轴翻转的坐标变换如下
主对角线翻转变换的坐标变换如下
具体代码如下
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 水平翻转
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < n; ++j) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
// 主对角线翻转
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};