本系列博客是我在刷卡尔哥代码随想录上的题目的时候所做的笔记,每题的代码是我自己写的,可能与代码随想录中的思路和代码不太一样,另外还补充了一些类似的题目,大家可以结合代码随想录与本博客一起使用,希望能够帮助到大家,如果博客有错误也请大家多多指正。
代码随想录的确很不错,排版很舒服,题目难度循序递进,卡尔哥的算法公开课讲的也很不错,推荐大家去听一听。
文章目录
二分查找
对应leetcode题号为704.二分查找
使用二分法前提:有序数组、不能有重复元素
第一种写法:左闭右闭
target在区间[l, r]中
如[1, 3, 4, 5, 6]查找4,target的下标为2,最后的区间应该是[2, 2],即l = r = 2
如果查找2,查找不到,l会大于r,跳出循环后返回-1
每次mid取l和r区间的一半,即(l + r) / 2,为了防止溢出int,可以用l + (r - l) / 2,即l + (r - l) >> 1
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1; // 查找下标范围为[0, n - 1]
// []
while (l <= r)
{
int mid = l + ((r - l) >> 1);
if (nums[mid] > target)
{
r = mid - 1;
}
else if (nums[mid] < target)
{
l = mid + 1;
}
else return mid;
}
return -1;
}
};
由于区间定义为了[l, r],答案范围也就是[0, nums.size() - 1],当target > nums[mid]时,说明target比当前的mid大,要往右边去查,因此更新左边界到mid右侧去查,而且mid处一定不是target,此时答案在[mid + 1, r]中,因此更新l = mid + 1;当target < nums[mid]时,说明target要比当前的mid小,要往左边去查,因此更新右边界到mid左侧去查,而且mid处一定不是target,此时答案在[l, mid - 1]中,因此更新r = mid - 1
第二种写法:左闭右开
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size(); // 查找下标范围为[0, n)
// [ )
while (l < r)
{
int mid = l + ((r - l) >> 1);
if (nums[mid] > target)
{
r = mid;
}
else if (nums[mid] < target)
{
l = mid + 1;
}
else return mid;
}
return -1;
}
};
由于区间定义为了[l, r),答案范围也就是[0, nums.size()),当target > nums[mid]时,说明target比当前的mid大,要往右边去查,因此更新左边界到mid右侧去查,而且mid处一定不是target,此时答案在[mid + 1, r)中,因此更新l = mid + 1;当target < nums[mid]时,说明target要比当前的mid小,要往左边去查,因此更新右边界到mid左侧去查,而且mid处一定不是target,此时答案在[l, mid)中,因此更新r = mid
移除元素
对应题目为leetcode27. 移除元素
数组元素不能删除,只能覆盖
如果在C++的vector中使用erase()函数,虽然size()方法返回的大小会-1,但是物理位置上该容器仍然长度不变,只是删完后的有效元素会前移,后面的元素为无用元素。而且erase()函数时间复杂度并不是O(1)而是O(n),涉及到数组后面的元素统一前移的操作。
暴力做法
暴力做法,时间复杂度为O(n^2),空间复杂度为O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size(); // size表示当前数组元素的个数,也是最后删完所有val之后剩下的有效元素个数
for (int i = 0; i < size; i ++) // 原地修改,数组元素不能删除只能覆盖,遍历有效数据的个数即数组当前元素的长度,由于数组后面的数会往前移动,实际上最后遍历的仍是前面删完所有val后有效的元素个数
{
if (nums[i] == val)
{
for (int j = i + 1; j < size; j ++)
{
nums[j - 1] = nums[j]; // 把当前元素后面的都往前移动一个
}
size--; // 当前数组元素个数-1
i--; // 由于删掉了一个元素,而后面的元素前移了一个位置,因此i遍历的下一个元素应该仍在i的位置,而for循环里面的i++会往后退一个位置,因此需要先把i往左移动一个位置,这样经过i++之后i还会回到这个位置继续遍历实际上的下一个数
}
}
return size;
}
};
双指针做法
时间复杂度为O(n),空间复杂度为O(1)
是否可以优化掉内层的for循环呢?答案是可以,两层for循环遍历可以考虑使用双指针优化的方式,让i指针为遍历所有元素的指针,j指针为更新新数组元素的指针(即删完val之后的数组),又因为当i往右走的时候,j指向的新数组中元素个数要么不变要么增加,不会往后退,具备单调性,最后j的值也就是新数组的元素个数,新数组即前j个元素
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int i = 0, j = 0; // i指针用来遍历所有元素,j指针用来更新删完val后的新数组
for (i = 0; i < nums.size(); i ++) // i指针这时候需要从头把数组全遍历一遍 因此为num.size()
{
if (nums[i] != val) // 当数组元素不为val的时候赋值给j指针位置,表示这是新数组的元素,并让j++,表示新数组元素个数+1
{
nums[j] = nums[i];
j++;
}
}
return j; // 最后的j也就是新数组元素的个数
}
};
有序数组的平方
对应题目为leetcode977. 有序数组的平方
暴力做法
时间复杂度O(nlogn) + O(n),合起来为O(nlogn)
空间复杂度为O(1)
暴力方法为先遍历一遍让每个元素都变为其平方O(n),接着对其进行快排O(nlogn)令其有序
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++)
{
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end());
return nums;
}
};
双指针做法
时间复杂度为O(n)
空间复杂度为O(n),因为用了额外数组
由于原来的数组是有序数组,参考二次函数的图像,x从左到右取值(可以取相等的值),由图像可以看出来,平方后的数组最大值一定在左右两侧,不可能出现在中间位置。
原数组沿着从左往右或者沿着从右往左某一个单侧方向对应的平方值一定会递减(或者说单调不增),也可能这两个方向都会向内侧递减,由此我们想到了使用双指针来做的思路,我们可以设置左侧和右侧两个指针来向内侧遍历原数组,每次比较谁的平方值大谁就往中间走,直到两个指针重合就遍历完了整个原数组,时间复杂度为O(n)

class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int len = nums.size(); // 后面代码要经常用到nums.size()
int l = 0, r = len - 1, k = r; // l为左指针,r为右指针,k为更新新数组的指针,初始指向新数组最右侧(由于要求按照从小到大返回,而原数组两侧平方后最开始为最大值所在位置,因此更新的数组要从后往前)
vector<int> ans(len, 0); // 初始化vector大小为原数组大小,初始值为0
while(l <= r) // 这里如果用 < 会丢掉最后l和r相遇的最后一个元素,因此用 <=
{
if (nums[l] * nums[l] > nums[r] * nums[r]) // 左指针平方更大
{
ans[k--] = nums[l] * nums[l]; // 向左更新新数组
l++; // 左侧指针向右
}
else // 右指针平方更大或者相等
{
ans[k--] = nums[r] * nums[r]; // 向左更新新数组
r--; // 右侧指针向左
}
}
return ans;
}
};
代码的关键就是新数组要从最右边向左更新,以及新数组要初始化指定大小还有while循环里面要用<=而不是<
长度最小的子数组
对应题目为leetcode209. 长度最小的子数组
暴力做法(TLE)
时间复杂度O(n^2),该题第十八个样例会超时
空间复杂度O(1)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len = INT32_MAX; // 每轮满足的长度
int min_len = INT32_MAX; // 更新最终答案的最短长度
int f = 0;
for (int i = 0; i < nums.size(); i ++)
{
int sum = 0;
len = 0;
for (int j = i; j < nums.size(); j ++)
{
if (nums[j] + sum < target)
{
sum += nums[j];
len++;
}
else
{
len++;
sum += nums[j];
f = 1; // 找到了
break;
}
}
if (sum >= target) min_len = min(len, min_len);
}
if (f == 0) min_len = 0; // 如果没找到返回0
return min_len;
}
};
第一次写的时候的代码,TLE了,代码中有很多可以改进的地方,首先不管啥情况len++是没有意义的,应该直接在维护区间和的变量sum>=target时更新len = i - j + 1。这样判断是否有答案的f变量也就不需要了,只有有答案时才会更新len,因此直接用return len == INT32_MAX ? 0 : len;通过判断len是否改变就知道有没有答案了,而且·min_len变量也不需要了,每次有答案的时候直接用len与i-j+1取min即可
因为要不断优化取小,初始值取得是很大的值,我最开始用的是算竞常用的0x3f3f3f3f,看卡哥代码学到了用INT32_MAX来表示最大整型
双指针法(滑动窗口法)
时间复杂度O(n)
空间复杂度O(1)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len = INT32_MAX;
int sum = 0;
for (int i = 0, j = 0; i < nums.size(); i ++)
{
sum += nums[i];
while(sum >= target)
{
len = min(len, i - j + 1);
sum -= nums[j++];
}
}
return len == INT32_MAX ? 0 : len;
}
};
这个题跟leetcode 3.无重复字符的最长子串一样都有单调性的性质,因此可以用双指针来优化,博客链接如下:
【leetcode刷题day01】LeetCode 1.两数之和、LeetCode 2.两数相加、LeetCode 3.无重复字符的最长子串-CSDN博客
假设左指针为j,右指针为i,遍历所有以i为结尾的子数组,用j到i区间的子数组表示>=target的最短子数组,当i指针向右移动的时候,j指针一定要么不动,要么也往右移动,因此具备单调性(反证法,如果i指针向右移动到i’时j指针向左移动到j’,四个指针的位置关系为j'——j——i——i',由于数组每个元素都是正整数(这里必须保证是正数,有0都不行),如果j’到i’是>=target的最短子数组,那么j到i一定会<target(因为减掉了正数后一定会变小,而且是最短子数组,少了一个都不会>=target),与原来的j到i是>=target的最短子数组矛盾,因此得证i增大时j要么不动要么往前移动,也就是具备了单调性
用sum维护j到i的区间和,i往右移动,代表当前以i为结尾的子数组,遍历一个值的时候就把当前nums[i]加入到sum中,如果当前sum满足>=target,就记录当前j到i子数组的长度更新len,同时让j往前移动,sum减掉j经过的元素,直到sum<target为止(这样就找到了以i结尾的sum>=target的子数组的长度),接着让i继续往右移动,继续遍历下一个元素,直到遍历完所有i(以i结尾的子数组)
i和j都要遍历一遍数组,时间复杂度为O(2n),即O(n)
螺旋矩阵II
对应leetcode 59.螺旋矩阵II
偏移量技巧(模拟)
时间复杂度O(n^2)
空间复杂度O(1) 因为题目要返回一个数组而参数没有给数组,因此定义的答案数组不应参与空间复杂度的计算
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> mp(n, vector<int>(n));
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1}; // 偏移量 上右下左 分别代表d = 0 1 2 3
int d = 1; // 当前的方向,撞墙后 0->1 1->2 2->3 3->0 因此更新的方法为(d + 1) % 4,初始的方向为向右,即d = 1
int x = 0, y = 0; // 初始的位置
for (int i = 1; i <= n * n; i++) // 可以推广n * m
{
mp[x][y] = i; // 更新矩阵
// 求下一步的坐标
int a = x + dx[d], b = y + dy[d];
// 判断下一步是否出界 出界有两种情况 一是数组越界 二是走到了已经走过的格子
// 如果出界 换下一个方向重新计算下一步坐标
// 蛇形矩阵只需要换一次方向就能找到下一次方向了 不需要用while
if (a < 0 || a >= n || b < 0 || b >= n || mp[a][b])
{
d = (d + 1) % 4;
a = x + dx[d], b = y + dy[d];
}
// 把修正后的正确坐标再赋值给x和y
x = a, y = b;
}
return mp;
}
};
这题没有看卡哥的代码,感觉用偏移量做更方便且更万能(把n*n换成n*m就是矩形蛇形矩阵),d表示当前前进的方向,用偏移量数组代表了上右下左(对应d的值0 1 2 3)顺时针的四个方向,先按照当前方向前进一步试一试,如果越界了(超出数组的外边界或者走到了已经走到的格子)就更改前进的方向0->1 1->2 2->3 3->0,即(d + 1) % 4,将修正后的坐标作为下一步前进的坐标
螺旋数组
对应leetcode 54.螺旋矩阵
偏移量技巧(模拟)
时间复杂度O(n*m)
空间复杂度O(n*m)
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1}; // 偏移量 上右下左 分别代表d = 0 1 2 3
int d = 1; // 当前的方向,撞墙后 0->1 1->2 2->3 3->0 因此更新的方法为(d + 1) % 4,初始的方向为向右,即d = 1
int x = 0, y = 0; // 初始的位置
int m = matrix.size(); // 二维vector数组的行数
int n = matrix[0].size(); // 二维vector数组的列数
vector<int> ans(m * n); //答案是一维数组
vector<vector<int>> mp(m, vector<int>(n, 0)); // 标记是否遍历过
for (int i = 0; i < m * n; i++)
{
ans[i] = matrix[x][y];
mp[x][y] = 1;//遍历过了已经
// 求下一步的坐标
int a = x + dx[d], b = y + dy[d];
// 判断下一步是否出界 出界有两种情况 一是数组越界 二是走到了已经走过的格子
// 如果出界 换下一个方向重新计算下一步坐标
// 蛇形矩阵只需要换一次方向就能找到下一次方向了 不需要用while
if (a < 0 || a >= m || b < 0 || b >= n || mp[a][b])
{
d = (d + 1) % 4;
a = x + dx[d], b = y + dy[d];
}
// 把修正后的正确坐标再赋值给x和y
x = a, y = b;
}
return ans;
}
};
如何获得参数二维vector数组的行数和列数?matrix.size()可以获得行数,matrix[0].size()可以获得列数
剩下的基本跟螺旋数组II一模一样,只是遍历的二维数组变成了一个给定的数组,要用额外的数组或者哈希表表示是否遍历过,最后输出的是一维数组
区间和
这个题在卡码网OJ上,没在力扣里,主要就是练前缀和以及C++输入输出的,涉及到多组输入的输入方式
而且这个题由于数据太大,用cin和cout会超时,就算关了同步也还是会超时,必须用scanf和printf
最蛋疼的还是他的下标是从0开始的,而写前缀和数组一般都是下标从1开始,因此需要多写几个特判注意边界条件
前缀和
每次使用前缀和求区间和时间复杂度为O(1)
前缀和数组s[i]表示前i个元素之和,s[i] = s[i - 1] + a[i],a为原数组。用前缀和求区间[l, r]元素之和为s[r] - s[l - 1],如果a和s下标都从1开始,这俩公式可以直接用,因为s和a定义在全局变量区(堆区域)中,a[0]和s[0]有初始值0,但是如果下标从0开始,就要特判i-1和l-1避免访问下标为-1的元素了
c++多组输入直到文件结束用cin时为while(cin >> a >> b) {},用scanf时为while(scanf("%d %d", &a, &b) != EOF),这个是由scanf和cin函数读取文件结束符EOF返回值不同导致的,scanf读取到EOF会返回-1,EOF也被宏定义为-1,但cin读到EOF会返回0
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int q[N]; // 原数组
int s[N]; // 前缀和数组
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++)
{
scanf("%d", &q[i]);
if (i) s[i] = s[i - 1] + q[i]; // 更新前缀和数组
else s[i] = q[i];
}
int a, b;
while (scanf("%d %d", &a, &b) != EOF) // 多组输入,直至文件结束
{
if (!a) printf("%d\n", s[b]); // 打印区间和
else printf("%d\n", s[b] - s[a - 1]);
}
return 0;
}
开发商购买土地
这道题也是力扣上没有,在卡码网OJ上的题目
二维前缀和
前缀和可以由一维拓展到二维,使用前缀和计算坐标(1, 1)为左上角,(i, j)为右下角的矩形区域的元素之和的复杂度为O(1)
二维前缀和计算公式为s[i][j] = s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1] + a[i][j],左上方的区域即(1, 1)到(i - 1, j - 1)的矩形之和加了两次,需要减掉一次
由于不知道哪一块区域的和更大,直接用abs函数求两块区域和的差值的绝对值即可,用前缀和可以求上侧区域(横着切)或者左侧区域(竖着切)的区域和,另一侧的区域和可以直接用所有元素的总和sum与之相减得到
n行可以在中间切n - 1刀,m列可以在中间切m - 1刀,因此需要对这n - 1以及m - 1种情况都求一下两块区域和的差值,并更新min
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 1e2 + 10;
int q[N][N]; // 原数组
int s[N][N]; // 前缀和数组,s[i][j]表示左上角(1, 1),右下角(i, j)的矩形区域的和
int main()
{
scanf("%d %d", &n, &m);
int sum = 0;
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= m; j ++)
{
scanf("%d", &q[i][j]);
sum += q[i][j];
}
}
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= m; j ++)
{
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + q[i][j];
}
}
int ans = INT32_MAX;
// 横着切
for (int i = 1; i <= n - 1; i ++)
{
int res = abs(sum - s[i][m] - s[i][m]); // sum - s[i][m]是下侧的区域和,s[i][m]是上侧的区域和
ans = min(ans, res);
}
// 竖着切
for (int i = 1; i <= m - 1; i ++)
{
int res = abs(sum - s[n][i] - s[n][i]); // sum - s[n][i]是右侧的区域和,s[n][i]是左侧的区域和
ans = min(ans, res);
}
printf("%d\n", ans);
return 0;
}
代码随想录里面的总结篇内容很不错,这里直接而放上链接,方便后续复盘使用
数组总结篇链接

1万+

被折叠的 条评论
为什么被折叠?



