2.手把手刷数组算法
小而美的算法技巧:前缀和数组
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
一维数组中的前缀和
链接: 区域和检索-数组不可变.
class NumArray {
private:
vector<int> preSum;
public:
NumArray(vector<int>& nums) {
// preSum[0] = 0,便于计算累加和
preSum.resize(nums.size() + 1);
//计算 nums 累加和
for (int i = 1; i < preSum.size(); i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
/* 查询闭区间 [left, right] 的累加和 */
int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
};
二维矩阵中的前缀和
链接: 二维区域和检索-矩阵不可变.
class NumMatrix {
public:
vector<vector<int>> sum;
NumMatrix(vector<vector<int>>& matrix) {
int n = matrix.size(), m = n == 0 ? 0 : matrix[0].size();
// 与「一维前缀和」一样,前缀和数组下标从 1 开始,因此设定矩阵形状为 [n + 1][m + 1](模板部分)
sum.resize(n + 1, vector<int>(m + 1,0));
// 预处理除前缀和数组(模板部分)
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1];
}
}
}
int sumRegion(int x1, int y1, int x2, int y2) {
// 求某一段区域和 [i, j] 的模板是 sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];(模板部分)
// 但由于我们原数组下标从 0 开始,因此要在模板的基础上进行 + 1
x1++; y1++; x2++; y2++;
return sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1];
}
};
二维前缀和模板
// 预处理前缀和数组
{
sum.resize(n+1, vector<int>(m+1,0));
// 预处理除前缀和数组(模板部分)
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
// 当前格子(和) = 上方的格子(和) + 左边的格子(和) - 左上角的格子(和) + 当前格子(值)【和是指对应的前缀和,值是指原数组中的值】
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1];
}
}
}
// 首先我们要令左上角为 (x1, y1) 右下角为 (x2, y2)
// 计算 (x1, y1, x2, y2) 的结果
{
// 前缀和是从 1 开始,原数组是从 0 开始,上来先将原数组坐标全部 +1,转换为前缀和坐标
x1++; y1++; x2++; y2++;
// 记作 22 - 12 - 21 + 11,然后 不减,减第一位,减第二位,减两位
// 也可以记作 22 - 12(x - 1) - 21(y - 1) + 11(x y 都 - 1)
ans = sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1];
}
和为 K 的子数组
链接: 和为 K 的子数组.
力扣官方答案
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1;
int count = 0, pre = 0;
for (auto& x:nums) {
pre += x;
if (mp.find(pre - k) != mp.end()) {
count += mp[pre - k];
}
mp[pre]++;
}
return count;
}
};
小而美的算法技巧:差分数组
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
链接: 什么是差分数组.
航班预订统计
链接: 航班预订统计.
// 差分数组工具类
class Solution {
private:
vector<int> diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public:
void difference(vector<int>& nums) {
diff.resize(nums.size());
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.size()) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
vector<int> result() {
vector<int> res;
res.resize(diff.size());
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.size(); i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
// 初始化全为0
vector<int> nums(n, 0);
// 构造差分解法
difference(nums);
for (auto& booking : bookings) {
// 注意转成数组索引要减一
int i = booking[0] - 1;
int j = booking[1] - 1;
int val = booking[2];
// 对区间 nums[i..j] 增加 val
increment(i, j, val);
}
// 返回最终的结果数组
return result();
}
};
拼车
链接: 拼车.
// 差分数组工具类
class Solution {
private:
vector<int> diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public:
void difference(vector<int>& nums) {
diff.resize(nums.size());
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.size()) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
vector<int> result() {
vector<int> res;
res.resize(diff.size());
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.size(); i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
bool carPooling(vector<vector<int>>& trips, int capacity) {
// 最多有 1001 个车站
vector<int> nums(1001, 0);
difference(nums);
for (auto& trip : trips) {
// 乘客数量
int val = trip[0];
// 第 trip[1] 站乘客上车
int i = trip[1];
// 第 trip[2] 站乘客已经下车,
// 即乘客在车上的区间是 [trip[1], trip[2] - 1]
int j = trip[2] - 1;
// 进行区间操作
increment(i, j, val);
}
vector<int> res = result();
// 客车自始至终都不应该超载
for (int i = 0; i < res.size(); i++) {
if (capacity < res[i]) {
return false;
}
}
return true;
}
};
双指针秒杀7道数组题目
数组问题中比较常见且难度不高的的快慢指针技巧,是让你原地修改数组。
删除有序数组中的重复项
链接: 删除有序数组中的重复项.
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
int fast = 0, slow = 0;
while (fast < nums.size()) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return slow + 1;
}
};
删除排序链表中的重复元素
链接: 删除排序链表中的重复元素.
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr) return nullptr;
ListNode* fast = head, *slow = head;
while (fast != nullptr) {
if (fast->val != slow->val) {
slow->next = fast;
slow = slow->next;
}
fast = fast->next;
}
slow->next = nullptr;
return head;
}
};
除了让你在有序数组/链表中去重,题目还可能让你对数组中的某些元素进行「原地删除」。
移除元素
链接: 移除元素.
- 注意这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow] 赋值然后再给 slow++,这样可以保证 nums[0…slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.size()) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};
移动零
链接: 移动零.
class Solution {
public:
void moveZeroes(vector<int>& nums) {
// 去除 nums 中的所有 0
// 返回去除 0 之后的数组长度
int p = removeElement(nums, 0);
for (; p < nums.size(); p++) {
nums[p] = 0;
}
}
// 双指针技巧,复用 [移除元素] 的解法。
int removeElement(vector<int>& nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.size()) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};
左右指针的常用算法
两数之和II-输入有序数组
链接: 两数之和II-输入有序数组.
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0, right = numbers.size() - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return {left + 1, right + 1};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return {-1, -1};
}
};
反转字符串
链接: 反转字符串.
前面和后面对称的字母交换。
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0, right = s.size() - 1;
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
};
最长回文子串
首先判断一个字符串是不是回文串
回文串就是正着读和反着读都一样的字符串。
bool isPalindrome(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] != s[right]) {
return false;
}
left++;
right--;
}
return true;
}
链接: 最长回文子串.
-
寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串
-
找回文串的关键技巧是传入两个指针
l
和r
向两边扩散,因为这样实现可以同时处理回文串长度为奇数和偶数的情况。
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(s, i, i + 1);
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1); // 从start开始的(end - start + 1)个
}
};
二维数组的花式遍历技巧
顺/逆时针旋转矩阵
链接: 旋转图像.
-
我们可以先将 n x n 矩阵 matrix 按照左上到右下的对角线进行镜像对称
-
然后再对矩阵的每一行进行反转
-
发现结果就是 matrix 顺时针旋转 90 度的结果
-
旋转二维矩阵的难点在于将「行」变成「列」,将「列」变成「行」,而只有按照对角线的对称操作是可以轻松完成这一点的,对称操作之后就很容易发现规律了。
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 先沿对角线反转二维矩阵
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 然后反转二维矩阵的每一行
for (auto &row : matrix) {
reverse(row);
}
}
// 反转一维数组
void reverse(vector<int> &arr) {
int i = 0, j = arr.size() - 1;
while (j > i) {
swap(arr[i], arr[j]);
i++;
j--;
}
}
};
如何将矩阵逆时针旋转 90 度呢?
- 只要通过另一条对角线镜像对称矩阵,然后再反转每一行,就得到了逆时针旋转矩阵的结果:
// 将二维矩阵原地逆时针旋转 90 度
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 沿左下到右上的对角线镜像对称二维矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][n - i - 1];
matrix[n - j - 1][n - i - 1] = temp;
}
}
// 然后反转二维矩阵的每一行
for (auto &row : matrix) {
reverse(row);
}
}
// 反转一维数组
void reverse(vector<int> &arr) {
int i = 0, j = arr.size() - 1;
while (j > i) {
swap(arr[i], arr[j]);
i++;
j--;
}
}
};
矩阵的螺旋遍历
链接: 螺旋矩阵.
解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界:
随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组:
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int upper_bound = 0, lower_bound = m - 1;
int left_bound = 0, right_bound = n - 1;
vector<int> res;
// res.size() == m * n 则遍历完整个数组
while (res.size() < m * n) {
if (upper_bound <= lower_bound) {
// 在顶部从左向右遍历
for (int j = left_bound; j <= right_bound; j++) {
res.push_back(matrix[upper_bound][j]);
}
// 上边界下移
upper_bound++;
}
if (left_bound <= right_bound) {
// 在右侧从上向下遍历
for (int i = upper_bound; i <= lower_bound; i++) {
res.push_back(matrix[i][right_bound]);
}
// 右边界左移
right_bound--;
}
if (upper_bound <= lower_bound) {
// 在底部从右向左遍历
for (int j = right_bound; j >= left_bound; j--) {
res.push_back(matrix[lower_bound][j]);
}
// 下边界上移
lower_bound--;
}
if (left_bound <= right_bound) {
// 在左侧从下向上遍历
for (int i = lower_bound; i >= upper_bound; i--) {
res.push_back(matrix[i][left_bound]);
}
// 左边界右移
left_bound++;
}
}
return res;
}
};
螺旋矩阵II
链接: 螺旋矩阵II.
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> matrix(n, vector<int>(n));
int upper_bound = 0, lower_bound = n - 1;
int left_bound = 0, right_bound = n - 1;
// 需要填入矩阵的数字
int num = 1;
while (num <= n * n) {
if (upper_bound <= lower_bound) {
// 从左到右
for (int j = left_bound; j <= right_bound; j++) {
matrix[upper_bound][j] = num++;
}
upper_bound++;
}
if (left_bound <= right_bound) {
// 从上到下
for (int i = upper_bound; i <= lower_bound; i++) {
matrix[i][right_bound] = num++;
}
right_bound--;
}
if (upper_bound <= lower_bound) {
// 从右到左
for (int j = right_bound; j >= left_bound; j--) {
matrix[lower_bound][j] = num++;
}
lower_bound--;
}
if (left_bound <= right_bound) {
//从下到上
for (int i = lower_bound; i >= upper_bound; i--) {
matrix[i][left_bound] = num++;
}
left_bound++;
}
}
return matrix;
}
};
滑动窗口算法
框架:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
最小覆盖子串
链接: 最小覆盖子串.
初始状态:
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ? "" : s.substr(start, len);
}
};
字符串排列
链接: 字符串排列.
class Solution {
public:
bool checkInclusion(string s1, string s2) {
unordered_map<char, int> need, window;
for (char c : s1) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s2.size()) {
// c 是将移入窗口的字符
char c = s2[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while (right - left >= s1.size()) {
// 在这里判断是否找到了合法的子串
if (valid == need.size())
return true;
// d 是将移出窗口的字符
char d = s2[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
// 未找到符合条件的子串
return false;
}
};
找所有字母异位词
链接: 找所有字母异位词.
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
unordered_map<char, int> need, window;
for (char c : p) need[c]++;
int left = 0, right = 0;
int valid = 0;
vector<int> res; // 记录结果
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while (right - left >= p.size()) {
// 当窗口符合条件时,把起始索引加入 res
if (valid == need.size()) {
res.push_back(left);
}
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
return res;
}
};
最长无重复子串
链接: 最长无重复子串.
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left = 0, right = 0;
int res = 0;
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
window[c]++;
// 判断左侧窗口是否要收缩
while (window[c] > 1) {
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
window[d]--;
}
res = max(res, right - left);
}
return res;
}
};