参考博客:陈童学哦 - C++中vector的用法
导图 - 数组篇
1.1 数组理论基础
-
连续内存空间上的相同数据类型的集合
-
下标从0开始;通过下标索引访问数据
-
数组中的元素不能删除,只能覆盖
-
二维数组在不同的编程语言中,存储方式不一样
-
C++:连续的内存空间地址
-
Java:不连续,可能是邻接表
-
1.2 704.二分查找
思路
-
二分法的前提:①有序数组;②无重复元素
-
二分法,即折半查找
-
循环不变量:区间的定义(左闭右闭、左闭右开)
- 区间如何定义,决定了循环条件、区间边界如何变化/区间如何调整
代码实现
左闭右闭(大多用)
class Solution {
public:
int search(vector<int>& nums, int target) {
// 左闭右闭
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target) {
right = mid - 1;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else {
return mid;
}
}
return -1;
}
};
// 时间复杂度:O(logn)
// 空间复杂度:O(1)
左闭右开
class Solution {
public:
int search(vector<int>& nums, int target) {
// 左闭右开
int left = 0;
int right = nums.size(); // right取不到
int mid = 0;
while (left < right) { // right取不到,"="无意义
mid = (left + right) / 2;
if (nums[mid] > target) {
right = mid; // right取不到
}
else if (nums[mid] < target) {
left = mid + 1;
}
else {
return mid;
}
}
return -1;
}
};
// 时间复杂度:O(logn) 2^执行次数 = n 👉 执行次数 = logn
// 空间复杂度:O(1)
1.3 27.移除元素
思路
-
暴力法:一层循环找
==val
的元素,找到后再一层循环把后面所有的元素前移(时:O(n^2))(具体代码见 代码随想录-移除元素) -
变量记录元素向前移动的长度(本质其实就是快慢双指针,不过是用变量代替了)(时:O(n))
-
快慢指针法
-
相向指针法
代码实现
变量记录元素向前移动的长度
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 变量记录元素向前移动的长度
// 所有变量都向前移动mov的长度,每当遇到==val的元素,mov++
// 当mov==0时,即不用移动,每次循环体中必有,nums[i]前移mov个长度
int mov = 0;
for (int i = 0; i < nums.size(); i++) {
nums[i-mov] = nums[i];
if (nums[i] == val) {
mov++;
}
}
return nums.size() - mov;
}
};
// 时间复杂度:O(n) 假设数组长度为n,每个元素都遍历了一边,故O(n)
// 空间复杂度:O(1) 题目要求原地修改
快慢指针法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 快慢双指针
// 快的找新数组需要的元素,慢的更新数组下标: fast遇到 !=val 的元素,把fast处的值赋值给slow处,slow++
// 每次循环都有fast++
// fast遇到几个 !=val 的元素,slow就加几,即slow为最终长度
int slow = 0;
int fast = 0;
int length = nums.size();
while (fast < length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};
// 时间复杂度:O(n) 假设数组长度为n,每个元素都遍历了一边,故O(n)
// 空间复杂度:O(1) 题目要求原地修改
相向指针法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 相向指针法
// left找 ==val的
// right找 !=val的
// 都找到后,nums[left] = nums[right]
// 无论如何,right最终指向新数组的最后一个元素,left指向新数组的最后一个元素的后一个元素
int left = 0;
int right = nums.size() - 1; // 左闭右闭
while (left <= right) { // 相等时还有最后一个元素需要判断
// 当left或right有连续变化时,一定要有前提:left<=tight,否则,可能会多移动
while (left <= right & nums[left] != val) // 此循环结束后,nums[left]==val
left++;
while (left <= right & nums[right] == val) // 此循环结束后,nums[right]!=val
right--;
// 所以必然存在 left != right
if (left < right){
nums[left] = nums[right];
left++;
right--;
}
}
return left;
}
};
// 时间复杂度:O(n) 假设数组长度为n,每个元素都遍历了一边,故O(n)
// 空间复杂度:O(1) 题目要求原地修改
1.4 977.有序数组的平方
思路
- 暴力解法:先平方,再排序(时间复杂度取决于排序效率,快排 O(logn))
- 双指针法:O(n)
代码实现
双指针法
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// 双指针法
int left = 0;
int right = nums.size() - 1; // 循环不变量:左闭右闭
vector<int> res(nums.size()); // 创建一个 nums相同长度 的数组
int count = nums.size() - 1; // 逆序添加元素至res数组中
while (left <= right) { // left==right时,还有最后一个元素要加到res数组中
int num_left = nums[left] * nums[left];
int num_right = nums[right] * nums[right];
if (num_left > num_right) {
res[count--] = num_left;
left++;
}
else { // 等于的时候就无所谓添加谁了,并到其中一个分支即可
res[count--] = num_right;
right--;
}
}
return res;
}
};
// 时间复杂度:O(n)
// 空间复杂度:O(n)
代码随想录参考答案(977.有序数组的平方),更简洁一点
1.5 209.长度最小的子数组
思路
- 暴力法:O(n^2)
- 滑动窗口(本质还是双指针)
代码实现
双指针法
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 双指针法
// slow和fast都从0开始,求slow到fast之间所有元素之和sum
// 大于等于target,sum减去下标为slow的值,slow++
// 否则,fast++,sum加下标为fast的值
// 设一个变量min_len不断更新最小长度,直至slow遍历至表尾结束,return min_len
int slow = 0;
int fast = 0;
int min_len = INT_MAX;
int sum = 0;
// 循环不变量:左闭右开
while (fast < nums.size()) {
sum += nums[fast];
// 这里用while循环,而非if,如果用if,每次还要 减去nums[fast],过于冗余
// 之所以用循环就是可以不用更新fast对应的值,而每层循环必然会更新fast的值
// 用第2层循环隔绝fast的更新,可以在不更新fast的前提下,更新slow,实现滑动窗口前侧的变化
while (sum >= target) {
min_len = min(min_len, fast-slow+1);
sum -= nums[slow++];
}
fast++;
}
return min_len != INT_MAX? min_len : 0; // 成立正常返回,==INT_MAX返回0
}
};
// 时间复杂度:O(n) 每个元素至多被遍历2次 O(2n) 👉 O(n)
// 空间复杂度:O(1)
遇到的问题
int
最大值不会表示- C++三元运算符不会写
- 循环条件格式写错:
while (slow <= fast < nums.size())
(Python的条件判断可以这样,C++不行)
导致一直报下图错误,我还以为超出int
最大值了,但是给的测试数据又不可能,我还模拟了半天,提交也是0个测试用例通过正确写法:
while (slow <= fast && fast < nums.size())
(而且实际上,本题如果用while循环的话,条件应该是fast < nums.size()
就足够了,因为slow
永远不可能超过fast
)
代码随想录参考答案(209.长度最小的子数组)给的for
循环的版本,我觉得更好使一点
个人猜想(未证实)
此题移动滑动窗口的前端时,为了保证另一端不动,直接用多套一层循环来移动
那么应该只移动前端,或者仅移动一端的时候,都可以直接用循环咯?
参考博客
1.6 59.螺旋矩阵Ⅱ
思路
模拟过程,考察代码掌控能力
本题的关键在于,画圈的同时,坚持循环不变量,每圈对每条边的处理就可以看成是相同的
代码实现
我的注释比较多,都是我写代码的过程中的思考
代码随想录参考答案(59.螺旋矩阵Ⅱ)会简洁一些
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
// 模拟转圈;循环不变量:左闭右开,每次的最后一个不处理,下一次的第一个会处理
// 转几圈? n/2
// 创建一个n*n的二维数组,初始化所有数据全为0,最终return result
vector<vector<int>> result(n, vector<int>(n, 0));
int loop = n / 2; // 转圈数
int count = 1; // 从1开始,每次赋值后++
int start_x = 0, start_y = 0; // 每一圈的起始位置,x对应行,y对应列
int offset = 1; // 偏移量
while (loop) { // loop不为0,就不还有圈要转
// 每一圈i要从x开始,j要从y开始
int i = start_x, j = start_y; // i与start_x对应,j与start_y对应
// 从上边的行开始,横向移动,变化的是列,即j
while (j < n-offset) { // 左闭右开,第1圈最后一个不处理,第2圈后俩不处理,所以要设置一个偏移量offset来记录
result[start_x][j++] = count; // 行不变,列随着j变
count++;
} // 遍历结束后 j == n-offset
// 接下来处理右边的列,纵向移动,变化的是行,即i
while (i < n-offset) { // 左闭右开,同上
result[i++][j] = count;
count++;
} // 遍历结束后 i == n-offset
// 接下来处理下边的行,横向移动,变化的是列,即j。此时 j==n-offset
while (j > start_y) { // 左闭右开,start_y的地方留给左边的列的开头
result[i][j--] = count;
count++;
} // 遍历结束后 j == start_y
// 接下来处理左边的列,纵向移动,变化的是行,即i。此时 i==n-offset
while (i > start_x) { // 左闭右开,start_x的地方留给左边的列的开头
result[i--][j] = count;
count++;
} // 遍历结束后 j == start_y
// 进行完上面4个循环,即转了1圈
loop--; // 圈数--
start_x++;start_y++;// 起始位置变成 1,1
offset++; // 偏移量也++
}
// 如果 n 是奇数,那么最后会漏一个元素
if (n % 2 == 1) {
result[n/2][n/2] = count;
}
return result;
}
};
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)
遇到的问题
- 不会用
vector
创建二维数组 - 最外层循环,
i
和j
的初始值不总是0,即每圈i
和j
的初始值不为0;而分别是start_x
和start_y
参考博客