【算法笔记】数组篇-双指针以及滑动窗口

前言

本篇重点练习数组删除相关的例题,主要介绍两种方法双指针法、滑动窗口。

例题

  1. 移除元素

分析:解1 暴力解法:从前往后遍历,遇到相等的就整体往前
后面的挪到前面 位置发生了变化
时间复杂度O(n2);空间复杂度O(1)

 int removeElement(vector<int>& nums, int val) {
    
        int len=nums.size();
        
        for(int i=0;i<len;i++){
           if(nums[i]==val){
            
               for(int j=i;j<len-1;j++){//整体挪动
                   nums[j]=nums[j+1];
               }
               i--;//后面的挪到了前面,所以要i--
               len--;//找到了相等,长度就减1
           }
        }
        return len;
    }

解2 双指针法:设两个指针p1 p2,初始值都为0,如果不等,p1 p2同时后移;如果遇到相等的,p1不动,p2后移到不等的位置,然后覆盖掉相等的,p1 p2 同时后移。
最后p1就是去除val后的长度。
时间复杂度O(n);空间复杂度O(1)

int removeElement(vector<int>& nums, int val) {
        int len=nums.size();
        int p1=0,p2=0;
        for(p2=0;p2<len;p2++){
            if(nums[p2]!=val){//遇到不等就覆盖,p1 p2同时后移;遇到相等,p2后移,p1不动
                nums[p1++]=nums[p2];
            }
        }
        
        return p1;
  1. 删除排序数组中的重复项

分析:此题和上一题差不多,如果还是采用暴力解法肯定会超时。可以采用双指针法。

 int removeDuplicates(vector<int>& nums) {
        int len=nums.size();
        if(len<=1){
            return len;
        }
        int p1=1;
        int p2=1;
        for(;p2<len;p2++){
            if(nums[p2]!=nums[p2-1]){
                nums[p1++]=nums[p2];
            }
        }
       
        return p1;
    }

3. 移动零

分析:暴力解法:直接挪动(效率很低)

void moveZeroes(vector<int>& nums) {
        int len=nums.size();
        for(int i=0;i<len-1;i++){
            if(nums[i]==0){
                int j=i;
                for(;j<len-1;j++){
                    nums[j]=nums[j+1];
                }
                nums[j]=0;
                i--;
                len--;
            }
        }
       
    }

双指针 和第一个基本一样,区别在于此题要在末尾填0

 void moveZeroes(vector<int>& nums) {
        int len=nums.size();
        int cur=0;
        int fin;
        for(fin=0;fin<len;fin++){
            if(nums[fin]!=0){
                nums[cur++]=nums[fin];
            }
            
        }
        for(;cur<len;cur++){//填0
            nums[cur]=0;
        }
       
    }

4. 比较含退格的字符串

补充string的相关函数
length获取字符串长度
s.substr(pos, n) 截取s中从pos开始(包括0)的n个字符的子串,并返回

分析:此题也可以用双指针,只是在遇到‘#’时cur指针要回退,其他的不变。

class Solution {
public:
        string swap(string s) {//转换字符串即去掉#
        int len = s.length();
    
        int cur = 0;
        int fin = 0;
        for (; fin < len; fin++) {
            if (s[fin] != '#') {
                s[cur++] = s[fin];
            }
            else {
                if (cur != 0) {
                    cur--;
                }

            }

        }
        s = s.substr(0, cur);//只取前cur个字符
        return s;
    }
    bool backspaceCompare(string s, string t) {
        s = swap(s);
        t = swap(t);
    
        if (s == t) {
            return true;
        }
        else{
            return false;
        }
    
    }
};

5.有序数组的平方

解1:先求平方,然后排序

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int len=nums.size();
        for(int i=0;i<len;i++){
            nums[i]*=nums[i];
        }
        sort(nums.begin(),nums.end());
        return nums;
    }
};

解2:双指针法,设两个指针,平方后的最大值一定在两端,所以可以新设一个与原数组一样大小的数组,从大往小即从后往前求。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int len=nums.size();
        vector<int> re(len);
        int k=len;
        int p1=0;
        int p2=len-1;
        while(k--){
            if(nums[p2]*nums[p2]>nums[p1]*nums[p1]){
                re[k]=nums[p2]*nums[p2];
                p2--;
            }
            else{
                re[k]=nums[p1]*nums[p1];
                p1++;
            }
        }
        return re;
    }
};

6 长度最小的子数组

分析:解1: 暴力解法 设两个循环,外层循环表示开始的位置,内层循环计算子数列的长度
时间复杂度 O(n2)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len=nums.size();
        int re=1000000;
        int sublen=0;
        int sum=0;
        for(int i=0;i<len;i++){
            sum=0;
            sublen=0;
            for(int j=i;j<len;j++){
                sum+=nums[j];
                sublen++;
                if(sum>=target){//找到符合的子数列就更新当前的结果
                    re=re<sublen?re:sublen;
                    break;
                }
            }
        }
        return re==1000000?0:re;
    }
    
};

解2 介绍一种新的方法 滑动窗口,其实也不能说是新方法,就是双指针的变形,本质上是一致的。
设两个指针不断调整开始位置与结束位置。
时间复杂度O(n)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len=nums.size();
        int be=0;//开始位置
        int en=0;//结束位置
        int sum=0;//子数列和
        int sublen=0;//子数列长度
        int re=1000000;//最终结果
        for(;en<len;en++){
            sum+=nums[en];
            while(sum>=target){//找到符合要求的,即更新re,同时调整窗口的开始位置(这里一定要用循环来找,因为be后移可能还是符合的)
                sublen=en-be+1;
                re=re<sublen?re:sublen;
                sum=sum-nums[be];//be后移
                be++;
            }
            
        }

        return re==1000000?0:re;//若re没有更新说明没有找到,即返回0
    }
    
};

扩张条件:curSum<target
收缩的条件:curSum>=target
收集结果的时刻:在收缩时

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int l=0;
        int r=0;
        int re=INT_MAX;
        int curSum=0;
        while(r<nums.size()){//左闭右开区间,当累加和小于target就扩张,当累加和大于等于target时就收缩
            curSum+=nums[r++];
            if(curSum<target){
                continue;
            }
            while(curSum>=target){//收缩条件:curSum>=target
                re=re<=r-l?re:r-l;
                curSum-=nums[l++];
            }
          
        }
        if(re!=INT_MAX){
            return re;
        }
        return 0;
    }
};

7. 水果成篮

分析:此题关键就是如何确定窗口的开始位置和结束位置,这里我先找可以放的两个类别,然后通过判断当前类别是否在刚才找的类别中,若在,说明可以放篮子里,en后移;若不在,说明篮子放不下了,因此be后移。然后重新找可以放的两个类别……
扩张的条件:进入窗口的水果是两类水果中的一类
收缩的条件:正好与扩张相反,当前水果不能再放入篮子了
收集结果的时机:由于是求最大值,当然是在扩张时收集结果

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int len=fruits.size();
        int be=0;//开始位置,左闭右开区间[be,en)
        int en=0;//结束位置
        int re=0;//最终结果
        int sum=0;//中间结果
        int first;//篮子里第1种水果
        int second;//篮子里第2种水果
        for(;be<len;be++){
       		if(be>0 && fruits[be-1]==fruits[be]){//优化1
       			continue;
       		}
            first=fruits[be];//找篮子里可以放的第1种水果
            for(int j=be+1;j<len;j++){//找篮子里可以放的第2种水果
                if(fruits[j]!=first){
                    second=fruits[j];
                    break;
                }
            }           
            while(fruits[en]==first|| fruits[en]==second){//只要当前水果种类符合要求,en就后移,即扩张
                
                sum=en-be+1;
                re=re>sum?re:sum;
                
                en++;
                if(en==len){//en已到末尾,不可能再有大的了,//优化2
                    return re==0?0:re;
                }
            }
        }
        return re;
    }
};

76. 最小覆盖子串
扩张:窗口中还未包含t中所有字符
收缩:与扩张相反

class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int>tMap;//t的词频
        unordered_map<char, int>window;//s的词频(仅统计t中出现的字符)
        int l = 0;
        int r = 0;
        int k = 0;//窗口中包含t中字符的个数(>=)
        int change = 0;
        string re = s;
        for (char c : t) {
            tMap[c]++;
        }
        while (r < s.size()) {

            if (k < tMap.size()) {//还未包含t中所有字符
                char c = s[r];
                ++r;
                if (tMap.count(c)) {
                    window[c]++;
                    if (window[c] == tMap[c]) {
                        ++k;
                    }
                }
            }
            
            while (k == tMap.size()) {
                char d = s[l];
                if ((r - l) <= re.size()) {
                    change = 1;
                    string newStr = s.substr(l,r-l);
                    re = newStr;
                }

                if (tMap.count(d)) {
                    window[d]--;
                    if (window[d] < tMap[d]) {
                        --k;
                    }
                }
                ++l;

            }

        }
        return change==1 ? re : "";//如果change==0,说明s中不存在子串
    }
};

总结

双指针和滑动窗口可以大大降低时间复杂度,针对数组的删除问题用双指针会比较快;滑动窗口非常巧妙,通过调整开始与结束位置,难的是如何确定窗口的开始与结束,在纸上模拟一下可能会有妙计。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值