c++数组部分

本文介绍了数组的基础知识,包括连续内存空间、下标访问和元素不可删除特性。详细讲解了二分查找的实现及注意事项,以及两种不同的区间处理方式。接着讨论了如何在C++中删除数组元素,对比了暴力解法(O(n^2))和双指针法(O(n))。此外,还探讨了有序数组的平方问题,提出暴力排序和双指针优化的解决方案。最后,文章涉及了长度最小的子数组问题,分析了暴力解法的超时问题并提出了滑动窗口算法的优化思路。
摘要由CSDN通过智能技术生成

一、理论基础

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):空间是刚开始就开辟好的,后面只是往里面填数,需要的内存空间没有随着循环次数的增加而增加,所以是常数级别的。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值