1.数组理论基础
1.1 数组定义
数组定义:一组相关变量能够一个接一个地存储在计算机存储器的一块连续区间内。我们将这样的表示法称为数组 array
2.LeetCode 相关题目
2.1_704二分查找
2.1.1 算法思路
二分查找思路:
在 left+right 正常情况下 mid 是会偏左,所以如果想让 mid 偏右必须要进行 +1
模板1:
二分查找 mid 选取哪个值比较关键。如果说
int mid = (left+right)/2;
那么分成的区间就是 [l,mid] & [mid+1,r]。如果满足条件了就让 r 或者 l 移动到对方最后或者起始的位置。因为每次都要 check mid 值是否满足,所以假如说 mid 在 r 的范围内,且 mid 满足条件,就说明 l 不满足条件。那么就需要移动 l 的位置了。否则移动 l 的位置
所以模板如下:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = (l + r)/2;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
模板2:
当想让 mid 在右边时就要进行 +1 操作
int mid = ( l + r + 1 ) /2;
上面的方法分成的区间就在 [l,mid-1]&[mid,r]
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = ( l + r + 1 ) /2;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
2.1.2 代码实现
1.左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0;
int r = nums.size();
while(l<r){
int mid = (l+r)/2;
if(nums[mid]==target) return mid;
else if(nums[mid]>target) r = mid; // mid 应该在左边
else l = mid+1;
}
return -1;
}
};
2.1.3 时空复杂度
时间复杂度:O(logn)
空间复杂度:O(1)
2.2_27移除元素
2.2.1 算法描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmvmuhpG-1644493452105)(https://gitee.com/xuboluo/images/raw/master/img/202202062342067.gif)]
暴力解法:
本题可以使用双重循环,先遍历每个元素,如果遇到了需要移除的元素则使用另一重循环将后面的元素逐个向前移动,将其覆盖
2.2.2 C++ 代码实现
双指针解法:
i 指针速度慢,j 指针速度快。实现 j 元素覆盖 i元素。
当该元素不需要移除时,j 直接覆盖带 i 上;当该元素需要移除时,i 指针不动,j 指针跑到那个不需要移除的元素的元素的地方再将 i 进行覆盖
i 代表下一步需要覆盖的位置,左最后输出 i 就是已经进行处理了的元素个数
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int i = 0;
int j = 0;
int n = nums.size();
while(j<n){
if(nums[j]!=val){ // j 位置上的值和 val 不相等则将 j 位置上的值给 i
nums[i] = nums[j];
i++;
j++;
}
else j++; // 相等就继续判断下一个 j
}
return i;
}
};
常见错误:
①最后需要返回这个数组的长度,即 i
2.2.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(1)
2.3_977有序数组的平方
2.3.1算法描述
这个题本身就是顺序排下来的,只不过存在负数的时候,如果将其平方后它的值会变大,也就是说负数存在的情况下,如果对数组取平方,那么越靠左的值可能越大,而中间的值反而最小,就比如说 0 是一个中间数,在取平方后反而最小了
我们很难找到数组最小值在哪一块,但是我们可以确定在平方之后数组的最大值有可能就在数组两边
①定义两个指针 i,j ,分别指向数组的左右两端,先存储大的数据
②定义一个 result 用于存放计算好的平方值,从后向前存储,因为步骤 ① 是先处理平方值大的数据
2.3.2 C++ 代码实现
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int l = 0;
int r = nums.size()-1;
vector<int>res(nums.size(),0);
for(int i =nums.size()-1;i>=0;i--){
if(nums[l]*nums[l]>nums[r]*nums[r]){
res[i] = nums[l]*nums[l];
l++;
}
else{
res[i] = nums[r]*nums[r];
r--;
}
}
return res;
}
};
2.3.3 时空复杂度
时间复杂度:O(logn)
空间复杂度:O(N)
2.4_209 长度最小子数组
2.4.1 算法描述
1.求最小子序列可以用滑动窗口解决,滑动窗口中的和为判断数据
2.left ,right 初始化指向 0 index
3.right 向右移动和变大,left 向左移动和变小
4.当滑动窗口中的值大于 target 的时候,left 指针就要逐个向右移动,直到让滑动窗口中的和小于 target
2.4.2 C++ 代码实现
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int right = 0;
int sums = 0; // 滑动窗口的大小
int res = INT_MAX; // 最小窗口是无限大
while(right<nums.size()){
sums+=nums[right];
while(sums>=target){ // 当滑动窗口大于 target 的时候就要移动窗口
res = min(res,(right-left+1)); // 判断是否是最小窗口
sums-=nums[left]; // 减掉前面的值
left++; // 移动 left ,一定要先减掉当前值再移动 left
}
// 不满足 sums>target 时,进一步放大窗口
right++;
}
return res == INT_MAX ? 0:res;
}
};
2.4.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(1)
2.5_59螺旋矩阵②
2.5.1 算法描述
螺旋矩阵的路程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
- 。。。。。。
**在定义变量时是左闭右闭,在循环时使用 = 。有点像二分法,因为填充的时候当前格的 index 是可以填充的状态,所以在 for 循环遍历的时候可以使用 = **
这里在最外层循环时使用 while(1) ,通过变量 left,right ,top,down 的关系判断是否发到了临界值
第一重循环控制 n~n² 的累增,嵌套的循环放置累增的数值,每判断完一行或者一列就要对相应的方位变量进行 ++ ,指向下一次要判断哪
Top 和 Down 在 if 判断时为什么不用等于:
因为这是先 – 再判断的,等于的时候是有被处理掉的
2.5.2 C++ 代码实现
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n, vector<int>(n));
int top = 0, right = n - 1, left = 0, x = 1, bottom = n - 1;
while (true) {
for (int i = left; i <= right; ++i) ans[top][i] = x++;
if (++top > bottom) break;
for (int i = top; i <= bottom; ++i) ans[i][right] = x++;
if (--right < left) break;
for (int i = right; i >= left; --i) ans[bottom][i] = x++;
if (--bottom < top) break;
for (int i = bottom; i >= top; --i) ans[i][left] = x++;
if (++left > right) break;
}
return ans;
}
};
2.5.3时空复杂度
时间复杂度:O(N²)
空间复杂度:O(N²)
3.其他题目
数组内部的增删改查:
两个指针:i,j 。
i 指向 j 下一次要赋值的 index ,只有 j 遍历的元素满足某个要求时才会将值赋值给 i
j 是不断在数组中走动的元素,所以对 j 进行 for 循环判断是否满足题目条件。
1.i,j 初始化
i,j 最开始是同步的,如果 j 满足要求了直接将其赋值给 i
2.for 循环判断 j
(1)nums[j] == 条件 TODO
(2)nums[j] != 条件 {
nums[i++] = nums[j];
}
3.1_283移动零
3.1.1 算法描述
对数组内的数据进行修改,将 0 都放在最后的位置。对于 j 指向的元素如下:
if(nums[j]==0) continue;
else{
swap(nums[i],nums[j]);
i++;
}
3.2_844比较含退格的字符串
3.2.1 算法描述
1.i,j 初始化
i,j 都初始化为 0
2.for 循环判断 j
(1)j == #
i 先判断是否可以推格,然后再推格
(2)j!=#
将 j 赋值给 i 。i,j 同时向前走
3.2.2 C++ 代码实现
class Solution {
public:
void backword(string &str){
// 初始化 i,j
int i =0;
int j =0;
for(int j =0;j<str.size();j++){
if(str[j]=='#'){
if(i>0) i--;
else i=0;
}else{
str[i]=str[j];
i++;
}
}
// 对 str 进行 resize 的操作
str.resize(i);
}
bool backspaceCompare(string s, string t) {
// 处理 s
backword(s);
backword(t);
return s==t;
}
};
3.2.3 时空复杂度
时间复杂度:O(m+n)
空间复杂度:O(1)
3.2.4 知识扩展
1.如何对字符串进行 resize
这里删除完字符后还要对字符串进行 resize 操作。最后的 i 是停留在了当前操作的字符上,所以直接对 i 进行 resize
str.resize(i);
3.3_80删除有序数组中的重复项 II
3.3.1 算法描述
这里实现在保留 k=2 个字符的情况下进行删除。
举例:
[1,1,1,1,1,1,2,2,2,2,2,2,3]
首先先对前两个 1 进行保留,所以将 i,j 初始化为 2
这个 j 是否进行保留其实是判断的 i = 0 的这种情况。如果 nums[2] != nums[0] 则这个 nums[j] 是可以保留的也就是赋值给 nums[i] 。假设说 j = 8,那么它需要判断的 index 就是 index = 6 ,而不是 index =7
总结:
因为可以保留 k 个元素,所以前 k 个元素无论是什么都可以保留,所以 i 和 j 从 k 开始便利。
j 一直往前走,我们不用管和 j 相邻的那个 i 是多少,我们只要关注 j 的前 k 个那个 i 是否和 j 相同。
如果不相同那这个 j 是可以赋值给 i 的,相同了则继续判断
3.3.2 C++ 代码实现
class Solution {
public:
int del(vector<int>&nums,int k){
int i =k;
int j =k;
for(;j<nums.size();j++){
if(nums[i-k]!=nums[j]) nums[i++] = nums[j];
}
return i;
}
int removeDuplicates(vector<int>& nums) {
if(nums.size()<2) return nums.size();
int len = del(nums,2);
return len;
}
};
3.3.3 时空复杂度
时间复杂度:O(n)
空间复杂度:O(1)
3.4_33搜索旋转排序数组
二分法
3.4.1算法描述
本题反转后的数组可以被拆分成两个有序的子序列。因为不知道反转位置所以两个子序列的各自的长度未知。
那么根据子序列的长度可以分成两种情况:
①mid 之后是顺序的
![image-20211223112059099](https://img-blog.csdnimg.cn/img_convert/434fe341d79818dcc35e7eac26e8edda.png)
②mid 之前是顺序的
![image-20211223112157679](https://img-blog.csdnimg.cn/img_convert/5cf8c01b711eb54110f7adc8b0e90ac5.png)
所以就先判断哪半边是顺序的。然后判断 target 在左半边还是右半边。
于是就分成了四种情况:
左边是顺序 target 在左边–> right 指针正常走
左边不是顺序 target 在左边-- left 指针不是移动到该侧,而是移动到另一侧
同理顺序在右边也是如此
3.4.2 C++ 代码实现
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = (int)nums.size();
if (!n) {
return -1;
}
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[0] <= nums[mid]) { // 顺序的在左边
if (nums[0] <= target && target < nums[mid]) { // target 在顺序方
r = mid - 1;
} else {
l = mid + 1;
}
} else { // 顺序的在右边
if (nums[mid] < target && target <= nums[n - 1]) { // target 不在顺序方
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
};
3.4.3 时空复杂度
时间复杂度:O(logn) 二分查找
空间复杂度:O(1)
3.5_69 Sqrt(x)
二分法
3.5.1 算法描述
这道题是求平方,正常思路是从 mid 开始找找 [1,mid] 所有数看那个数符合结果,这属于一种暴露解。
还有一种方法是使用二分,二分相比于暴力解可以更快的找到答案
这个题的二分法比较特殊,不像查找中的二分法必须要找到才能返回,这里是无论放如何都能找到,所以在 <=x 的时候多出来一个变量 res 用于一直记录结果
因为最终结果需要转换成 int ,所以在 mid*mid <x 的时候对 mid 进行取值
举例:
![image-20211224131804195](https://img-blog.csdnimg.cn/img_convert/00762b9eaed07a069d9f1e3620971b17.png)
3.5.2 C++ 代码实现
1.二分法
public:
int mySqrt(int x) {
int left = 0;
int right = x;
int res = 0;
while(left<=right){
int mid = left+(right-left)/2;
long ji = (long)mid*mid;
if(ji<=x){
res = mid;
left = mid+1;
}else{
right = mid-1;
}
}
return res;
}
};
2.暴力解
class Solution {
public:
int mySqrt(int x) {
if(x<=1) return x;
int mid = x/2;
for(int i=1;i<=mid;i++){
if((long)i*i==x) return i;
long qian = (long)(i-1)*(i-1);
long hou = (long)(i+1)*(i+1);
if(qian<x&&hou>x) return i;
}
return -1;
}
};
3.5.3 时空复杂度
时间复杂度:O(logN)
空间复杂度:O(1)
3.6_41 缺失的第一个正数
index 和 x 的映射+原地哈希
3.6.1 算法描述
这个题使用的是:原地哈希的方法
因为数组长度是 N ,其中没有出现的最小正整数只能在 [1,N+1] 中选
步骤:
1.先判断 nums 中是否有 1 ,因为不考虑负数,所以将所有不再范围内的数都替换为 1
[3,4,-1,-2,1,5,16,0,2,0] -->[3,4,1,1,1,5,1,1,2,1]
2.遍历 nums 中的每一个数 x 给 index = x-1 的值添加负号
第一个数字是 3 ,给 index = 2 的位置添加负号标记,表示 3 已经出现过了
[3,4,1,1,1,5,1,1,2,1]–>[3,4,-1,1,1,5,1,1,2,1]
当遍历到 -1 时代表 1 出现了所以给 index = 0 的位置添加负号
[3,4,-1,1,1,5,1,1,2,1]–>[-3,4,-1,1,1,5,1,1,2,1]
但是数组中可能与多个 x=1 的值,那这样 index=0 的数一会正一会负,就不知道这个 index 对应的值是否出现过了。所以在添加负号时先将其变为正,假装之前没出现过然后再对其添加负号
。。。。。
[-3,-4,-1,-1,-1,5,1,1,2,1]
3.所有出现过的 x 它所对应的 index 都变为了负数
我们只需要找到那个最小的没有出现的 x 就好,那么再遍历一遍数组,看哪个 index 上的数为正数代表这个 index 对应的 x 没有出现过,从上面看是 index = 5 ,也就是 x=6 没有出现过,那么最终答案就是 6
在代码中将 step1 变为了替换成 n+1
3.6.2 C++ 代码实现
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
// 将所有负数替换成 n+1 (这里不是替换成 1 )
for(int& num:nums){
if(num<=0){
num = n+1;
}
}
// 映射
for(int i =0;i<n;i++){
int num = abs(nums[i]); // 将其转为正数
if(num<=n){ // 需要判断的那 [1,N] 的值
nums[num-1] = -abs(nums[num-1]);
}
}
// 遍历哪个 index 是第一个出现的正数
for(int i =0;i<n;i++){
if(nums[i]>0) return i+1;
}
return n+1;
}
};
3.6.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(1)
3.7_4寻找两个正序数组的中位数
3.7.1算法描述
方案1 :双指针
可以使用双指针法,我们知道中位数出现在哪个位置,判断指针移动的个数
方案2:第 k 个数
这里使用的思路是最小第 k 个正数,求中位数的话无非这个 k 就是中间的那个数
如果一个数组是奇数个,第 k 个数为 (n/2)+1
如果一个数组是偶数个,第 k 个数为 ((n/2)+(n/2)+1)/2.0
这里使用的方法是折半删除法:取目标值的一半进行删除
现在有下面两个数组,一共有 17 个数组成,找到中位数,也就是第 9 个数
S1:找到前 8 位数
因为 k = 9,每个数组分割出一个可删除的子数组出来
9/2 -1 = 3,即从当前 index 的位置出发,向后取 3 位,也就是前 4 个数
S2:比较最后一个值的大小
4<=12 ,也就是说 [1,2,3,4] 这个子数组是可以完全删除的,因为中位数有可能在 [5,6,7,8] 中,也有可能在第二个数组中
S3:再进行一次折半删除
通过 S1,前 4 个小的数已经找到,所以 k 变为了在剩下的数组中找到第 k-4 小元素
这时 k-= windows +1
个数
那么在这一轮 window
的个数也发生改变,并且每一次都是 k/2-1
A 组子数组位置变化,B 组子数组大小变,位置不变
S4:不断的折半删除
退出条件:
如果 nums1 用尽了,则返回 nums2 中的第 k 小的元素。
如果 nums2 用尽了,则返回 nums1 中的第 k 小的元素。
如果 k 为 1,则返回当前两个数组中第一个元素的最小值。
3.7.2 代码实现
class Solution {
public:
// 寻找第 k 个值
int findK(vector<int>& nums1, vector<int>& nums2,int k){
int m = nums1.size();
int n = nums2.size();
int index1 = 0;
int index2 = 0;
while(true){
// 判断边界条件
// 1. 其中一个数组中没有值了,返回另一个数组的第 k 个值
if(index1 >= m) return nums2[index2+k-1];
if(index2 >= n) return nums1[index1+k-1];
// 2. k == 1
if(k==1) return min(nums1[index1],nums2[index2]);
// 折半删除:判断两个数组子数组最后一个值,删除整个子数组
int window = k/2-1; // 设置窗口大小
int index1_end = min(index1+window,m-1);
int index2_end = min(index2+window,n-1);
int val1 = nums1[index1_end];
int val2 = nums2[index2_end];
if(val1<=val2){
k -= index1_end-index1+1;
index1 = index1_end+1;
}else {
k -= index2_end-index2+1;
index2 = index2_end+1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
// 奇数
if((m+n)%2!=0){
return findK(nums1,nums2,(m+n)/2+1);
} else return (findK(nums1,nums2,(m+n)/2)+findK(nums1,nums2,(m+n)/2+1))/2.0;
}
};
3.7.3 时空复杂度
3.8_34在排序数组中查找元素的第一个和最后一个位置
3.8.1 算法描述
本题:
因为不知道最后是否有这个值所以最后还要判断一下 nums[r]是否等于 target
3.8.2 代码实现
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1,-1};
// 二分查找
int l = 0;
int r = nums.size()-1;
// 查找元素的开始位置
while(l<r){
int mid = (r+l)/2;
if(nums[mid]>=target) r = mid;
else l = mid+1;
}
if(nums[r]!=target) return {-1,-1}; // 右边没有左边一定没有
int L = r;
l = 0;
r = nums.size()-1;
// 查找元素的结束位置
while(l<r){ // 这里不论是调用模板 2 还是模板 1 都 ok
int mid = (r+l)/2;
if(nums[mid]>=target) r = mid;
else l = mid+1;
}
return {L,r}; // 最后结束的条件一定是 l=r 的所以返回 r 和 返回 l 都行
}
};