一、理论基础
1.数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
数组内存空间的地址是连续的。
数组元素不能删除,只能覆盖。
C++中,二维数组在内存的空间地址是连续的。
2.二分查找
使用二分法的前提条件:数组为有序数组,且无重复元素。
区间的定义非常重要,在循环中要坚持根据查找区间的定义来做边界处理。
两种区间:左闭右闭,左闭右开。处理在于right的不同。
左闭右闭:right=middle-1;左闭右开:right-middle。
二、做题笔记
2.二分查找
重点:“循环不变量”原则,每次的区间都要保持左闭右闭或者左开右闭。我们选左闭右闭。
因为是左闭右闭,所以left=right是有意义的,写循环条件时要注意这一点。
class Solution {
public:
int search(vector<int>& nums, int target) {
//数组和目标值已经传进来了,接下来需要定义二分查找的区间
//这里我们使用左闭右闭的区间
int left=0;
int right=nums.size()-1;
while(left<=right)
{
int middle=left+((right-left)/2);
//target是数值,middle是数组元素下标,要取数值的话是nums[middle],注意区分
//对mid的计算要写在循环里面,因为每次循环区间长度会变
if(nums[middle]>target)
{
right=middle-1;
//target在左区间,肯定不是middle,所以下次搜索截止到middle左边一个
}
else if (nums[middle]<target)
{
left=middle+1;
//target在右区间,肯定不是middle,所以下次搜索从middle右边一个开始
}
else
{
return middle; //刚好就是middle
}
//return -1; 写在循环里面的话,不算函数的返回值,会报错
}
return -1;
}
};
二分法的时间复杂度:O(logn)
3.移除元素
数组的内存空间是连续的,所以没有办法直接删除其中的某一个元素。所谓的删除元素,实际上是把后面的元素挪过来覆盖掉它。
(1)暴力解法
所以暴力解法实际上就是遍历数组中的每个元素,每次遍历的时候,遇到要删除的元素,就把后面的数据循环往前移动,从而覆盖掉它。
由此可见,暴力解法需要两个for循环:第一个for循环用来遍历数组元素,找到需要删除的元素;第二个for循环在找到要删除的元素后,用来把要删除的元素后面的数往前挪,组成新的数组,组成之后下标和数组长度都要更新。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size=nums.size();
for(int i=0 ; i<size ; i++) //遍历数组元素
{
if(nums[i]==val) //找到和val相同值的元素,把后面的每个元素都向前移动一位
{
for(int j=i ; j<size-1 ; j++) //如果不是j<size-1,就会读到nums[size]这个值,报错
{
nums[j]=nums[j+1];
}
//如果找到了,才需要这样操作,没找到的话就不需要,所以这两句还在if里:
i--;
size--;
}
}
return size;
}
};
显然,暴力解法的时间复杂度为:O(n^2)
(2)双指针法(快慢指针)
慢指针指向下一个要填充的位置,最后组成新数组,在找到要删除的元素后,slow就先停下了;快指针指向要判断的元素,如果快指针指向元素不等于val,就填充到慢指针所指位置,然后slow再继续往下走;否则慢指针不动,就不填充这个,快指针继续判断下一个元素。
class Solution {
public:
int removeElement(vector<int>& nums, int val)
{
int size=nums.size();
int slow=0;
int fast=0;
for(fast=0;fast<size;fast++)
{
if(nums[fast]!=val)
{
nums[slow]=nums[fast];
slow++;
}
}
return slow;
}
};
最后返回的应该是新数组的长度,slow在上面那一步完成后已经++了,所以即便新数组第一个元素下标为0,现在的slow也等于新数组长度。
时间复杂度:O(n)
4.有序数组的平方
(1)暴力解法
遍历数组,每个数组平方,再由小到大排序。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i=0; i<nums.size();i++)
{
nums[i]=nums[i]*nums[i];
}
sort(nums.begin(),nums.end());
return nums;
}
};
(2)双指针法
因为原数组是有序的,所以平方后的最大值一定在数组的两端,要么左边要么右边。所以考虑双指针法,i指向起始位置,j指向结束位置,两个相互比较着一点一点往中间靠。
让k指向新数组的末尾,先存放i和j比较后最大的数,然后一点一点往前走。
因为出现了一个新数组,所以需要初始化一个数组来存放结果,大小和原数组一致。
代码随想录版本:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int>result(nums.size(),0);
//初始化一个数组来存放结果,数组大小和原数组一致,先都存放0
int k=nums.size()-1; //结果数组的指针,要从最后一个元素开始往前走
for(int i=0, j=nums.size()-1; i<=j;)
//原数组的双指针:i指向开头,j指向结尾,如果执行循环的条件是i<j,就会有一个数没法被放进去,所以最后要i<=j
{
if(nums[i]*nums[i]<nums[j]*nums[j])
//开头的元素平方小于结尾元素平方,所以应该先把结尾元素平方放进结果数组的最后一个
{
result[k]=nums[j]*nums[j];
k--;
j--; //把j的元素放进去了,就要把j往前移一个
}
else
{
result[k]=nums[i]*nums[i];
k--;
i++; //把i的元素放进去了,就要把i往后移一个
}
}
return result;
}
};
/*
for循环原来写的是
for(int i=0; int j=nums.size()-1; i<j)
这个写错了,for循环第一个是循环变量,有两个循环变量的话就写在一起,一样类型的话声明一次就行
第二个是结束循环的条件
第三个是循环变量的增减,这个后面有写,这里可以省略,但那个分号不要忘了
*/
//时间复杂度分析:每一轮循环中,每个元素只被操作一次,所以复杂度为O(n)
我自己的题解:使用的是while循环
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
//先创建一个新数组来存放结果
vector<int>result(nums.size(),0);
//对各数组下标进行初始化
int i=0;
int j=nums.size()-1;
int k=result.size()-1;
while(i<=j)
{
if(nums[i]*nums[i]>nums[j]*nums[j])
{
result[k]=nums[i]*nums[i];
k--;
i++;
}
else
{
result[k]=nums[j]*nums[j];
k--;
j--;
}
}
return result;
}
};
5.长度最小的子数组209
(1)暴力解法(超时)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result=INT32_MAX;
//先初始化最终结果,因为我们要找最小的长度,所以刚开始要把最终结果最大化
int sum=0;
int subleng=0;
for(int i=0; i<nums.size(); i++)
{
sum=0; //j循环完一轮走到这里要重新开始,所以把sum清零
for(int j=i; j<nums.size(); j++)
{
sum=sum+nums[j]; //如果不是从j=i开始的话,这一步就会漏掉nums[i]这个数
if(sum>=target)
{
//找到符合条件的子序列,计算长度
subleng=j-i+1;
result=result<subleng ? result : subleng; //更新最小数组长度
break; //找到符合条件的,直接结束
}
}
}
return result== INT32_MAX ? 0 : result;
//注意这里是两个等号,因为第一个表达式是要判断result是否为INT32_MAX,不是直接赋值
}
};
其实也可以理解为有两个指针,一个快一个慢。慢指针是i,遍历整个数组;快指针j遍历i以后的数,负责把i后面的数一个一个加到i上求和,期间可能会出现sum>=target的情况。第一次出现的时候,因为这时result非常大,所以必然大于subleng,那么result=subleng。
然后,如果i不变的话(也就是开头数没变),后面的subleng必然大于前面的result=subleng,也就是说没有出现更小的数组长度,那么result保持不变。
如果i指针往后移动了的话,就要重新开始,所以令sum=0.i指针变化后,可能会出现subleng比前面的result更小的情况,即出现了更小的数组长度,那么就重新令result=subleng,更新了result。
总结:一个for循环为滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环完成了一个不断搜索区间的过程。
时间复杂度:两个嵌套的for循环,所以是O(n^2)。
空间复杂度:需要的变量和内存空间没有随着迭代次数的增加而增加,是常的,所以O(1)。
(2)滑动窗口(数组操作中另一个重要方法)
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环为滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环完成了一个不断搜索区间的过程。
那么滑动窗口如何用一个for循环来完成这个操作呢。
窗口就是 满足其和 ≥ s 的长度最小的连续子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。那么就令起始位置i++,同时sum减掉i位置的值。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引,正常移动。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result=INT32_MAX;
int sum=0;
int subleng=0;
int i=0; //起始位置
for(int j=i; j<nums.size(); j++)
{
sum=sum+nums[j];
while(sum>=target)
{
subleng=j-i+1;
result=result<subleng? result : subleng;
//滑动窗口体现在这里:
sum=sum-nums[i];
i++;
}
}
return result== INT32_MAX ? 0 : result;
};
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。
为什么用while循环?因为需要不断比较子序列是否符合条件,从而判断是否需要更新起始位置i,由于数大小的问题,有时候并不是i加一次就可以达到sum<target的,可能要很多次。
时间复杂度:O(n)。不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
空间复杂度:O(1)。
6.螺旋矩阵59
要点:坚持“循环不变量”原则,一圈要画四条边,每条边都要坚持左闭右开或左开右闭原则(我们选左闭右开)。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); //初始化一个n*n的二维数组,元素为0
int startx=0, starty=0; //开始循环的初始位置
int loop=n/2; //一共要循环多少圈,比如n=5,n/2就是2,要循环两圈,最中间的值单独处理
int mid=n/2; //最中间的位置,比如n=5,中间位置就是[2,2]
int offset=1; //每一圈的缩进,每循环一圈,缩进+1
int count=1; //初始化要填入的数据
int i,j;
while(loop--) //当loop=0时循环停止,所以n=5时就循环loop=2和loop=1这两圈
{
i=startx;
j=starty;
//下面开始画圈,分为四步
//上行,从左到右,左闭右开,i不动,j递增
for(j=starty;j<n-offset;j++)
{
res[i][j]=count;
count++;
}
//j和count都是操作结束后再+1,所以以n=5为例,这一轮结束后j=4,count=5
//右列,从上到下,左闭右开,j不动,i递增
for(i=startx;i<n-offset;i++)
{
res[i][j]=count; //此时j一直=4
count++;
}
//下行,从右到左,左闭右开,i不动,j要递减!递减!!!
for(;j>starty;j--) //j从现在的值开始递减,一直到j=starty停止
{
res[i][j]=count; //i保持4不变,j从4递减到1
count++;
}
//左列,从下到上,左闭右开,j保持现在的值不变,i要从现在的值开始递减到1
for(;i>startx;i--)
{
res[i][j]=count;
count++;
}
//到此为止,已经画完一圈了,现在要开始下一圈
//那么需要更新起始位置,第一圈起始位置为(0,0),第二圈就是(1,1)
startx++;
starty++;
//更新缩进值,每循环一圈,需要再缩进一格,则缩进值加1
offset++;
}
//通过循环画圈,但最中间的值需要单独处理
if(n%2) //如果n为奇数
{
res[mid][mid]=count; //count在上面又+1之后才到这里的
}
return res;
}
};
- 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间
- 空间复杂度 O(1):空间是刚开始就开辟好的,后面只是往里面填数,需要的内存空间没有随着循环次数的增加而增加,所以是常数级别的。