Leetcode双指针操作合集(快慢指针&滑动窗口&双指针)

目录

快慢指针

滑动窗口

76. 最小覆盖子串

567. 字符串的排列

438. 找到字符串中所有字母异位词 

3. 无重复字符的最长子串

1248. 统计「优美子数组」

209. 长度最小的子数组

30. 串联所有单词的子串

239. 滑动窗口最大值

面试题57 - II. 和为s的连续正数序列

Hash + 滑动窗口 

424. 替换后的最长重复字符

最大连续1的个数系列问题

剑指 Offer 48. 最长不含重复字符的子字符串 

双指针

1. 两数之和 

15. 三数之和

16. 最接近的三数之和 

9. 回文数

11. 盛最多水的容器

面试题 16.06. 最小差 

240. 搜索二维矩阵 II

面试题21. 调整数组顺序使奇数位于偶数前面

1471. 数组中的 k 个最强值

581. 最短无序连续子数组

283. 移动零

75. 颜色分类


快慢指针

之前在链表总结部分,提及到了快慢指针,凡是牵扯到链表中的精准定位问题,选择使用快慢指针,保准没错。

环的个数/起点,中间位置,两条链表找相交处,看了题目描述,直接快慢指针,手到擒来。

LeetCode 链表总结:https://blog.csdn.net/qq_41605114/article/details/105385252

关于链表中的快慢指针使用,此处不再多嘴,详情见上方链接

好文分享:

https://leetcode-cn.com/problems/find-the-duplicate-number/solution/qian-duan-ling-hun-hua-shi-tu-jie-kuai-man-zhi-z-3/

 

滑动窗口

字符串或者数字之间求交集(在字符串中是子串),或者符合要求的连续子集或者子字符串,都可以使用滑动窗口进行解答。

 

滑动窗口的核心,就是在右侧区间不断扩大的同时,根据完成解的要求,右移左侧指针,依次找到最优解。

 

@powcai对滑动窗口的题目进行了汇总:

 https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/

@labuladong关于滑动窗口,总结了一套模板:https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7.md

模板如下:

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {

    //(1)
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;
    //(2)
    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

作者:labuladong
链接:https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以下内容也主要是参考上面提到的多篇文章并加入自己的理解。

 

模板介绍:

(1)中,初始化两个Hash表,window代表我们的滑动窗口,needs代表我们需要找的目标值及个数

(2)中,初始化滑动窗口的左右指针,整个过程中滑动窗口都是一个左闭右开的区间,即[left,right),其中valid表示窗口中满足needs条件的字符个数。

此模板是左闭右开,务必注意right和left更新的位置,更新的位置不一样,那么我们得到的解也就不一样


 

滑动窗口的核心,就是先找到一个满足要求的解,然后不断的收缩空间,尝试得到最优解。

right不断右移,扩大窗口范围,在扩大的过程中,不断判断,是否得到了一个满足要求的解,一旦得到一个解,在更新解的同时

开始进行窗口left的右移,我们开始缩小窗口,直到在窗口中失去解,完成lef的移动,right继续右移

我们也可以这么认为:

不断找到解,不断失去解,再次找到解。

首先找到一个解,然后移动left指针,直到区间失去了解,失去解是为了在剩下的内容中找到另一组解

失去解后,right不断右移,直到我们再次找到解。以此循环,知道遍历所有要查找内容。

如果说二分查找的精华在区间中实在有解,滑动窗口的精髓在于舍弃解后,再次寻找解。

 

下面找一道题目作为依托,进一步阐述滑动窗口的原理

76. 最小覆盖子串

https://leetcode-cn.com/problems/minimum-window-substring/

class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char,int> window,need;
        for(auto item:t) need[item]++;//目标元素出现次数
        int left = 0,right = 0,valid = 0;
        int length = INT_MAX,begin = 0;
        int size = need.size();
        while(right < s.size())
        {
            char Rtemp = s[right];//当前字符
            right++;
            if(need[Rtemp])//一旦当前字符是目标值之一,那么我们进行记录,目前窗口包含目标值
            {
                window[Rtemp]++;//更新窗口包含目标值的情况
                if(window[Rtemp] == need[Rtemp])
                //一旦某个目标值元素在窗口中的个数满足要求,那么更新valid
                valid++;
            }

            while( valid == size )//一旦条件成立,说明窗口中目前已经包含了解
            {
                //更新解
                if(right - left<length)
                {begin = left;length = right - left;}
                //下面收缩左区间边界,尝试寻找最优解
                char Ltemp = s[left];//当前字符
                left++;
                if(need[Ltemp])
                {
                    window[Ltemp]--;
                    if(window[Ltemp] < need[Ltemp])//此处特别注意,判断符合应该是小于
                    valid--;
                }
            }

        }

        return length == INT_MAX?"":s.substr(begin,length);
        //substr,begin规定起点,length规定了包括起点在内的元素长度

        
    }
};

下面我们来图解一下整个过程:

  

right不断地右移,知道指向C,此时valid == 3,right照常自增1。第一次找到解,此时更新解,len = 6,begin = 0;

之后left右移,从解中删除A,left自动增1,valid == 2,区间内不包含解,那么直接break,right继续右移

 

知道right找到最后的A,right自增1,valid == 3,找到第二个解,但是不如第一个解短,不更新解,left开始右移

直到将C移除区间,right继续右移

当right在等于s.size()的时候,选择C,然后自增1,此时valid等于3,left开始右移

当left选中E的时候,left自增,到达了B的位置,此时进入新的一轮循环,更新解,排除B后,valid也不在等于3,跳出循环

此时right也不符合条件,跳出循环。

最优解已经被记录,完成。

最重要的细节部分,是对区间和最优解的更新位置,务必注意,因为左闭右开,right在更新解前自增,left在更新解后自增

567. 字符串的排列

 https://leetcode-cn.com/problems/permutation-in-string/

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        unordered_map<char,int> window,need;
        for(auto item : s1) need[item]++;
        int left = 0, right = 0;
        int len = INT_MAX;
        int valid = 0;
        int nsize = need.size(); //避免重复元素
        while(right<s2.size())
        {
            char Rtemp = s2[right];
            right++;
            if(need[Rtemp])
            {
                window[Rtemp]++;
                if(window[Rtemp] == need[Rtemp])
                valid++;
            }
            while(valid == nsize)
            {
                if(right - left <len)
                {
                    len = right - left;
                    if(len == s1.size()) return true;
                }
                char Ltemp = s2[left];
                left++;
                if(need[Ltemp])
                {
                    window[Ltemp]--;
                    if(window[Ltemp]<need[Ltemp]) 
                    valid--;
                }
            }
        }

        return false;
    }
};

架构完全一样,改都没有改,本题只需要增加一些判断即可,需要是子串,而且要求无视排列。

那么只要在s2中找到一个子串,均包含s1,且长度和s1相等,那一定就是找到了,否则就是没有找到。

438. 找到字符串中所有字母异位词 

https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        unordered_map<char,int> window,need;
        for(auto item:p) need[item]++;
        int right = 0,left = 0;
        int valid = 0,len = p.size();
        int nsize = need.size();//避免重复元素
        vector<int> Res;
        while(right<s.size())
        {
            char rtemp = s[right];
            right++;
            if(need[rtemp])
            {
                window[rtemp]++;
                if(window[rtemp] == need[rtemp]) 
                valid++;
            }
            while(valid == nsize)
            {
                if((right - left) == len)//长度相等,元素都有,排列不相等
                Res.push_back(left);
                char ltemp = s[left];
                left++;
                if(need[ltemp])
                {
                    window[ltemp]--;
                    if(window[ltemp] < need[ltemp]) 
                    valid--;
                }
            }
        }
        return Res;

    }
};

此题和上一道题非常相似,都是子串,那么我们还是从长度入手进行解题。

 

但是有时我们需要的题目不一定如此复杂,下面的第3题和209题,就是滑动窗口的另外两种风格。

但是核心不变,就像二分查找的区间,不管左右区间如何迭代,解都是要在区间内的

滑动窗口也是如此,解一定要保持在窗口内,上面几道字符串类型的题目,都体现了滑动窗口的核心:

不断找到解,不断失去解,再次找到解。

首先找到一个解,然后移动left指针,直到区间失去了解,失去解是为了在剩下的内容中找到另一组解

失去解后,right不断右移,直到我们再次找到解。以此循环,知道遍历所有要查找内容。

如果说二分查找的精华在区间中实在有解,滑动窗口的精髓在于舍弃解后,再次寻找解。

下面的题目更是体现了这点。

3. 无重复字符的最长子串

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

还是左右指针,滑动窗口的套路,

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char,int>window;
        int right = 0,left = 0;
        int len = 0;
        while(right<s.size())
        {
            char temp = s[right];
            right++;
            window[temp]++;
            while(window[temp]>1)//清空部分
            {
                char ltemp = s[left];
                left++;
                window[ltemp]--;

            }
            len = max(len,right - left);
        }
        return len;
        
    }
};

window[temp]大于1的时候,说明有重复的了,那么我们把重复的排除出去即可,不断移动left指针,直到区间内再次恢复为只含

有单独元素的情况

 

滑动窗口的变形:

1248. 统计「优美子数组」

https://leetcode-cn.com/problems/count-number-of-nice-subarrays/

本题在某种意义上,甚至是有些违背滑动窗口的一般结题思路的

滑动窗口一般是找到解,然后将解抛出,继续寻找最优解。

此题我们在找到解的时候,需要特别注意,如果理解抛出解,会少算很多情况

比如示例3

以第一个2开头,就有四种情况,以第二个2开头也有四种,总共16种,所以我们的滑动窗口需要进行一下修改

本题我们需要在找到一组解后,计算这个解中两端奇数前后的偶数个数,算出排列组合的情况

class Solution {
public:
    int numberOfSubarrays(vector<int>& nums, int k) {
        //滑动窗口
        int size = nums.size();
        if(size<k) return 0;
        int right = 0,left = 0;
        int valid = 0,res = 0;
        deque<int> target;
        int leftnumber = 0,rightnumber = 0;
        while(right<size)
        {
            int Rtemp = nums[right];
            if(Rtemp%2 != 0) 
            {
                target.push_back(right);//有效值的位置
                valid++;
            }//是奇数,增加
            right++;
            if(valid == k)
            {
                leftnumber = target.front() - left + 1;//第一个奇数到左边界的个数
                while(right<size)//找到下一个奇数
                {
                    int temp = nums[right];
                    if(temp%2 == 0) //是偶数
                        right++;
                    else break;
                }
                rightnumber = right - target.back();
                while(valid == k)
                {
                    int Ltemp = nums[left];
                    if(Ltemp%2 != 0)//奇数
                    valid--;
                    left++;
                }
                target.pop_front();
                cout<<rightnumber<<"   "<<leftnumber<<endl;
            }
            
            res+=rightnumber*leftnumber;//更新组合数

        }
        return res;

    }
};

使用两个int变量,leftnumber 和 rightnumber,分别记录第一个奇数距离left的距离,和最后一个奇数距离right的距离

找到排列组合之后,我们要进行滑动窗口的老操作了,就是边界收缩,让left不断收缩,直到没有解

这是一道非常非常有趣的滑动窗口题目。反常规。

 

 

209. 长度最小的子数组

https://leetcode-cn.com/problems/minimum-size-subarray-sum/

 

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int right = 0,left = 0;
        int sum = 0,len = INT_MAX;
        while(right<nums.size())
        {
            sum += nums[right];
            right++;
            while(sum>=s) 
            {
                len = min(len,right - left);
                sum -= nums[left];
                left++;
            }
        }
        return len == INT_MAX?0:len;
    }
};

当和到达要求的时候,此时可以求长度了,我们得到了解,下面就要舍弃解,不断移动left,直到失去解

然后我们才能去找更优的解。

30. 串联所有单词的子串

https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/

参考内容:

https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/solution/30-by-ikaruga/

本题看似非常简单,不就是把找元素换找单词吗?步长从1改成单词长度,不就分分钟的事情吗?

但是此题的情况远比这复杂,因为题目可没有说,其他干扰项的长度也是步长

比如:

​
"lingmindraboofooowingdingbarrwingmonkeypoundcake"
["fooo","barr","wing","ding","wing"]


"aaaaaa"
["aaa","aaa"]

​

 看了简直要死,第一个例子,第一个大的干扰项长度是13,但是平常的步长是4

那么本题该如何应对,我们在滑动窗口外侧加循环,让整个滑动窗口的起点,从0开,一直到步长4为止,起点分别是

0,1,2,3,按照一个步长的循环来进行,这样总能规避掉各种长度的干扰项目。

题目要求找到的解,必须长度和words所有元素的长度加起来一致,但是排列无所谓,所以求子串是否是我们想要的解,判断条件就是长度相等

 

本题细节非常多,可谓是滑动窗口之最了

先看核心代码

            int left = i;
            int right = i,valid = 0;
            unordered_map<string, int> window;
            while(right<s_size)
            {
                string Rtemp = s.substr(right,step);
                right+=step;
                if(need[Rtemp])
                {
                    window[Rtemp]++;
                    if(window[Rtemp] == need[Rtemp])
                    valid++;
                }
                while(valid == validsize)
                {
                    if(right - left == length)//①有要求,解必须是只包含所要的单词
                    Res.push_back(left);
                    string Ltemp = s.substr(left,step);
                    left+=step;
                    if(need[Ltemp])
                    {
                        window[Ltemp]--;
                        if(window[Ltemp] < need[Ltemp])//②此处
                        valid--;
                    }
                }

①处,因为题目要求,不能含有其他多余字符,所以整个长度必须是匹配的

②处,这个地方写快了很容易出错,一旦少一个字符,我们就要将valid减去1

因为字典中每个字符都要出现,我们还是用Hash表进行记录,但是这样有时候会记录重复的内容

所以我们在加完temp后,立即比较,如果数量够了就将valid加1,之后有重复的也不管了

完整版本(有改进)

class Solution {
public:
    vector<int> Res;
    vector<int> findSubstring(string s, vector<string>& words) {
        if(s.size() == 0 || words.empty()) return {};
        unordered_map<string,int>M;
        int len = 0,strlen = words[0].size();
        for(auto item:words) {len++;M[item]++;}
        for(int i = 0;i<strlen;++i) SubSolution(s,M,len,strlen,i);
        return Res;
    }
    void SubSolution(string s, unordered_map<string,int>M,int len,int strlen,int begin){
        int left = begin,right = begin;
        int value = 0;
        unordered_map<string,int>Temp;
        while(right<s.size()){
            string Rtemp = s.substr(right,strlen);
            right += strlen;
            if(M.find(Rtemp) != M.end()){
                Temp[Rtemp]++;
                if(Temp[Rtemp] == M[Rtemp]) value++;
            }
            while(value == M.size()){
                if((right - left) == len*strlen) Res.push_back(left);
                string Ltemp = s.substr(left,strlen);
                left += strlen;
                if(M.find(Ltemp) != M.end()){
                    Temp[Ltemp]--;
                    if(Temp[Ltemp] < M[Ltemp]) value--;
                }
                cout<<left<<" "<<right<<endl;
            }
        }
        return;
    }
};

 

 

最后一题,借滑动窗口之名,而无滑动窗口之实。

239. 滑动窗口最大值

https://leetcode-cn.com/problems/sliding-window-maximum/ 

面试题59 - I. 滑动窗口的最大值  

https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/

 

要求线性时间:我们可以这么做,维护一个栈,每次都选择滑动窗口中的最大值压入,之后每次窗口移动一次,只把串口right处的值和栈顶元素比较即可,线性时间复杂度,需要注意的是这个最值,一定要在滑动窗口的范围内:我们尝试实现一下代码:

我们先处理第一个滑动窗口:

        //先处理第一个滑动窗口
        int MAXnum = 0,index = 0;
        for(int i = 0;i<k;++i) 
        {
            if(MAXnum<nums[i]) {MAXnum = nums[i];index = i;}
        }
        MAXIndex.push(index);//压入第一个滑动窗口的最大值索引
        Res.push_back(nums[MAXIndex.top()]);
        cout<<nums[MAXIndex.top()]<<endl;

比较了k次,选出最值,那么我们下面从第二个滑动窗口开始:

int left = 1,right = k;//我们从第二个滑动窗口开始
        while(right<size)
        {
            //栈中要有数字
            if(MAXIndex.size())
            {
                //该最值元素不在窗口内,那么没有办法,我们只能把这个窗口中的所有值全部比一遍
                if(MAXIndex.top()<left||MAXIndex.top()>right) 
                {
                    MAXIndex.pop();
                    MAXnum = nums[left],index = left;
                    //只比较left到right-1这个区间内的值,下面还有一个if比较right的值
                    for(int i = left + 1;i<right;++i)
                    {
                        if(nums[i]>MAXnum) {MAXnum = nums[i],index = i;}
                    } 
                    MAXIndex.push(index);
                }
                //一般情况下:只比较栈中的最值和right指针的位置
                if(nums[right]>nums[MAXIndex.top()]) {MAXIndex.pop();MAXIndex.push(right);}
            }
            else MAXIndex.push(right);
            //选择最值并添加到结果中,整个滑动窗口右移
            cout<<nums[MAXIndex.top()]<<endl;
            Res.push_back(nums[MAXIndex.top()]);
            right++;
            left++;
        }

 完成代码如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.empty()) return {};
        vector<int>Res;
        stack<int>MAXIndex;
        int size = nums.size();
        //先处理第一个滑动窗口
        int MAXnum = 0,index = 0;
        for(int i = 0;i<k;++i) 
        {
            if(MAXnum<nums[i]) {MAXnum = nums[i];index = i;}
        }
        MAXIndex.push(index);//压入第一个滑动窗口的最大值索引
        Res.push_back(nums[MAXIndex.top()]);
        cout<<nums[MAXIndex.top()]<<endl;
        int left = 1,right = k;//我们从第二个滑动窗口开始
        while(right<size)
        {
            if(MAXIndex.size())
            {
                //不在窗口内
                if(MAXIndex.top()<left||MAXIndex.top()>right) 
                {
                    MAXIndex.pop();
                    MAXnum = nums[left],index = left;
                    for(int i = left + 1;i<right;++i)
                    {
                        if(nums[i]>MAXnum) {MAXnum = nums[i],index = i;}
                    } 
                    MAXIndex.push(index);
                }

                if(nums[right]>nums[MAXIndex.top()]) {MAXIndex.pop();MAXIndex.push(right);}
            }
            else
            MAXIndex.push(right);
            cout<<nums[MAXIndex.top()]<<endl;
            Res.push_back(nums[MAXIndex.top()]);
            right++;
            left++;
        }
        return Res;

    }
};

程序一定要注意,最值要在范围内:

 

 

下面是官方解法:

(官方解法:https://leetcode-cn.com/problems/sliding-window-maximum/solution/hua-dong-chuang-kou-zui-da-zhi-by-leetcode-3/) 

(C++版本:https://leetcode-cn.com/problems/sliding-window-maximum/solution/dan-diao-dui-lie-by-labuladong/

本题总的来说,不算是滑动窗口,只是名字一样,实际方法是双项队列deque

为了降低时间复杂度,我们观察一下窗口移动的过程类似于队列出队入队的过程,每次队尾出一个元素,然后队头插入一个元素,求该队列中的最大值

每次的值都和队尾元素比较,将小的弹出,大的暂时放入,队首一直都是最大值(在滑动窗口范围内)

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.empty()) return {};
        vector<int>Res;
        deque<int>MAXindex;
        //处理第一个窗口
        for(int i = 0;i<k;++i)
        {
            while(MAXindex.size()&&nums[MAXindex.back()]<nums[i])//注意细节,是和最后的部分比
            MAXindex.pop_back();//弹出尾部
            MAXindex.push_back(i);
        }
        Res.push_back(nums[MAXindex.front()]);
        // cout<<MAXindex.size()<<endl;
        int left = 1,right = k,size = nums.size();
        while(right<size)
        {
            //提出不在范围内的值
            while(MAXindex.size()&&(MAXindex.front()>right||MAXindex.front()<left))                        MAXindex.pop_front();//弹出头部
            //最值比较
            while(MAXindex.size()&&nums[MAXindex.back()]<nums[right])//注意细节,是和最后的部分比
            MAXindex.pop_back();//弹出尾部
            MAXindex.push_back(right);
            Res.push_back(nums[MAXindex.front()]);
            left++,right++;
            // cout<<MAXindex.size()<<endl;
        }
        return Res;
    }
};

以上情况,正好是线性的时间复杂度 

面试题57 - II. 和为s的连续正数序列

https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/

算法参考:https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/solution/shi-yao-shi-hua-dong-chuang-kou-yi-ji-ru-he-yong-h/

从头开始滑动窗口,找到解后,因为以这个为开头的解只可能有一个,所以左侧窗口边界收缩

这个方法非常巧妙,但是要注意细节,注意循环的break条件,否则会找不全内容

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        int Uplimit = target/2;
        //滑动窗口
         vector<vector<int>> Res;
        int left = 1,right = 1;
        int Sum = 0;
        while(left<=Uplimit)//小细节
        {
            if(Sum>target) 
            {
                Sum -= left;
                left++;
            }
            else if(Sum<target)
            {
                Sum += right;
                right++;
            }
            else if(Sum == target)
            {
                vector<int>Temp;
                for(int i = left;i<right;++i) Temp.push_back(i);
                Res.push_back(Temp);
                Sum -= left;
                left++;
            }
        }
        return Res;
    }
};

Hash + 滑动窗口 

下面三道题,滑动窗口的判断和普通判断稍有不同,需要注意! 

424. 替换后的最长重复字符

 https://leetcode-cn.com/problems/longest-repeating-character-replacement/

本题思路:统计滑动窗口之间,出现次数最多的元素,那么剩下的元素,就是要被替换的内容,当被替换的内容长度大于k的时候,就要开始收拾左侧边界了,收缩左边界的时候,要统计收缩后的出现最多元素的个数,此时需要遍历Hash table

//本题整体思路比较不好想,难在判断合格条件上
class Solution {
public:
    int characterReplacement(string s, int k) {
        if(s.empty()) return 0;
        unordered_map<char,int>M;
        int left = 0,right = 0,maxlen = 0,MAX = 0;
        while(left<=right&&right<s.size()){
            //左右指针之间,统计最多元素的个数,剩下的就是要替换的内容
            char rstr = s[right++];
            M[rstr]++;
            maxlen = max(maxlen,M[rstr]);//时刻去确定right和left之间最长重复元素的个数
            //left 到 right 之间,最多重复元素的个数
            if((right - left - maxlen)<=k) MAX = max(MAX,right - left);
            //在不超范围的情况下去计算最长距离
            while((right - left - maxlen)>k){
                //循环条件是,left和right之间,可替换的数量已经大于k了,需要减少
                char lstr = s[left++];
                M[lstr]--;
                for(auto item:M) maxlen = max(maxlen,item.second);//这个时候要从头计算
            }
        }
        return MAX;

    }
};

最大连续1的个数系列问题

485. 最大连续1的个数

标准的动态规划

class Solution {
public:
    int findMaxConsecutiveOnes(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        vector<int>dp(size+1,0);
        int MAX = 0;
        for(int i = 1;i<=size;++i){
            if(nums[i-1] == 1){
                dp[i] = dp[i-1] + 1;
            }
            else dp[i] = 0;
            MAX = max(MAX,dp[i]);
        }
        return MAX;
    }
};

487. 最大连续1的个数 II https://leetcode-cn.com/problems/max-consecutive-ones-ii/

滑动窗口:统计1和0的个数,当0的个数为1时,我们进行左右边界的移动,先移动右侧窗口,直到下一个不是1的位置,然后收缩左侧窗口,直到排除0。

class Solution {
public:
    int findMaxConsecutiveOnes(vector<int>& nums) {
        if(nums.empty()) return 0;
        int left = 0,right = 0;
        int Zero = 0,One = 0,res = 0;
        while(left<=right&&right<nums.size()){
            int rtemp = nums[right++];
            if(rtemp == 1) One++;
            else Zero++;
            while(Zero){
                //right再往右移动,直到下一个0
                while(right<nums.size()&&nums[right] == 1) {right++;One++;}
                res = max(res,One + Zero);
                int rtemp = nums[left++];
                if(rtemp == 1) One--;
                else Zero--;                
            }
        }
        return res == 0?nums.size():res;
    }
};

1004. 最大连续1的个数 III https://leetcode-cn.com/problems/max-consecutive-ones-iii/

class Solution {
public:
    int longestOnes(vector<int>& A, int K) {
        if(A.empty()) return 0;
        int left = 0,right = 0;
        int Zero = 0,One = 0,res = 0;
        while(left<=right&&right<A.size()){
            int rtemp = A[right++];
            if(rtemp == 0) Zero++;
            else One++;
            if(Zero<=K) res = max(res,One + Zero);//判断位置要对,可以省去很多繁琐的操作
            while(Zero>K){ 
                int ltemp = A[left++];
                if(ltemp == 0) Zero--;
                else One--;
            }
        }
        return res;
    }
};

剑指 Offer 48. 最长不含重复字符的子字符串 

https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/

class Solution {
public:
    int lengthOfLongestSubstringTwoDistinct(string s) {
        //最多包含两个不同元素的子串
        unordered_map<char,int>M;//数组做hash map
        if(s.empty()) return 0;
        int left = 0,right = 0;
        int diff = 0,MAX = 0;
        while(left<=right&&right<s.size()){
            char rstr = s[right++];
            if(M[rstr] == 0) {//初见这个元素
                diff++;//统计不同元素的个数
            }
            M[rstr]++;
            if(diff <= 2) MAX = max(MAX,right - left);
            while(diff > 2){
                char lstr = s[left++];
                M[lstr]--;
                if(M[lstr] == 0) {
                    diff--;//统计不同元素的个数
                }   
            }
        }
        return MAX;
    }
};

 

双指针

有下面一道题目,有一个排序数组,现在需要在线性的时间复杂度下,找出目标和,我们应该怎么做,Hash表是很好的方法

1. 两数之和 

https://leetcode-cn.com/problems/two-sum/

leetcode天字一号题目,两数之和,又回到了梦开始的地方

Hash表:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> m;
        for(int i = 0;i<nums.size();++i)
        {
            if(m.find(target - nums[i])!=m.end())
                return{m[target - nums[i]],i};
            else
                m[nums[i]] = i;
        }
        return{};

    }
};

 

但是稍稍改变题目,返回的是数组的元素而不是下标,双指针法也毫不逊色:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int size = nums.size();
        sort(nums.begin(),nums.end());//排序
        vector<int> Res;
        int left = 0,right = size-1;
        while(left<right)
        {
            if((nums[left]+nums[right])==target)
            {
                Res.push_back(left);
                Res.push_back(right);
                while(left<size-1&&nums[left] == nums[left+1])left++;
                while(right>0&&nums[right] == nums[right-1])right--;
                left++,right--;
            }
            else if((nums[left]+nums[right])>target) right--;
            else left++;
            
        }
        return Res;
        
    }
};

 

以上是一个典型的双指针技巧。

从排序数组的两端,不断向中间逼近,下面我们看图解:

 

 

如果说快慢指针是数学问题,滑动窗口的一个非常傲娇的过程,先找到解,再将解移除区间,然后再次寻找解

双指针的核心,就是根据数列的性质(从小到大排序),让两个指针移动。

这些性质也就省去了很多冗余不必要的计算。

https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/

 

双指针一般出现在数组问题中,数组问题是个非常庞大的系列,其中使用到的方法也是各式各样。

15. 三数之和

 https://leetcode-cn.com/problems/3sum/

初见此题,在没有任何准备的情况下,暴力解法就是一种解法,在暴力解法的基础上,Hash表也是一个优化的选择。

但是面对数组问题,可以尝试排序后使用双指针的方法进行解决。

下面收录了一些非常好的解法和解题思路讲解:

https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-cshi-xian-shuang-zhi-zhen-fa-tu-shi/

https://leetcode-cn.com/problems/3sum/solution/man-hua-jue-bu-wu-ren-zi-di-xiang-kuai-su-kan-dong/

https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/

以上三篇文章,都是从不同的角度对本问题提出了分析的方法

变化太多的情况,逻辑上我们就要把他变成变化少的情况,固定一个数的位置,去找剩下两个数字。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> Res;
        int size = nums.size();
        if(size <= 2) return Res;
        sort(nums.begin(),nums.end());
        int pre = 0,right = size-1,left = pre+1;
        while(pre<=size-3)
        {
            int sum = 0;
            
            while(left<right)
            {
                vector<int> Temp;
                sum = nums[pre]+nums[left]+nums[right];
                if(sum == 0)
                {
                    Temp.push_back(nums[pre]);
                    Temp.push_back(nums[left]);
                    Temp.push_back(nums[right]);
                    left++,right--;
                }
                else if(sum>0) right--;
                else left++;
                if(!Temp.empty()) Res.push_back(Temp);

            }
            pre++,right = size-1,left = pre+1;
        }
        return Res;


    }
};

会发现,计算重复了,那么,我们需要剔除重复的结果

如果pre重复了,那么我们继续加,直到遇到一个新的pre

对于right和left也是一样的,既然重复,那么就不断循环,直到找到一个区别于刚才一组解的值

代码如下:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> Res;
        int size = nums.size();
        if(size <= 2) return Res;
        sort(nums.begin(),nums.end());
        int pre = 0,right = size-1,left = pre+1;
        while(nums[0]<=0&&pre<=size-3)
        {
            int sum = 0;
            while(left<right)
            {
                vector<int> Temp;
                sum = nums[pre]+nums[left]+nums[right];
                if(sum == 0)
                {
                    Temp.push_back(nums[pre]);
                    Temp.push_back(nums[left]);
                    Temp.push_back(nums[right]);
                    //位置一定要放对了,一定要是在等于0之后,剔除重复的内容
                    while(right>0&&nums[right] == nums[right-1]) right--;
                    while(left<size-1&&nums[left] == nums[left+1]) left++;
                    left++,right--;
                }
                else if(sum>0) right--;
                else left++;
                if(!Temp.empty()) Res.push_back(Temp);

            }
            while(pre<size-1&&nums[pre] == nums[pre+1]) pre++;
            pre++,right = size-1,left = pre+1;
        }
        return Res;


    }
};

版本二:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        if(nums.empty()) return {};
        int size = nums.size();
        vector<vector<int>>Res;
        sort(nums.begin(),nums.end());//排序
        int PCur = 0,left = 1,right = size-1;
        while(PCur<=size-2)//倒数第二个
        {
            if(PCur-1>=0&&nums[PCur] == nums[PCur-1]) {PCur++;continue;}
            //和自己的前一个比较,如果一样,那么我们就要跳过这个值
            left = PCur+1,right = size-1;
            while(left<right)
            {
                int temp = nums[PCur]+nums[left]+nums[right];
                // cout<<temp<<endl;
                if(temp<0) left++;
                else if(temp>0)right--;
                else if(temp == 0)
                {
                    vector<int> Vtemp;
                    Vtemp.push_back(nums[PCur]);
                    Vtemp.push_back(nums[left]);
                    Vtemp.push_back(nums[right]);
                    Res.push_back(Vtemp);
                    right--;left++;
                    //放置于正确的位置,当等于零时,移动左右指针,知道遇到不一样的内容为止,放置重复
                    while(right>=0&&nums[right] == nums[right+1]) right--;
                    while(left<=size-1&&nums[left] == nums[left-1]) left++;
                }

            }
            PCur++;
        }
        return Res;

    }
};

下面的解中,包含了各种个数的求和总结:

https://leetcode-cn.com/problems/3sum/solution/man-hua-jue-bu-wu-ren-zi-di-xiang-kuai-su-kan-dong/

那么将上面的题目稍作改变: 

16. 最接近的三数之和 

https://leetcode-cn.com/problems/3sum-closest/solution/

类比上一道题目。本题可以选择重复内容,只要求接近,那么我们不断去更新最小值,再根据目前三个数的和与target的关系,移动指针,不断去逼近正确答案。

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        int size = nums.size();
        sort(nums.begin(),nums.end());
        if(size < 3) return 0;
        int Min = INT_MAX,pre = 0,right = size-1,left = pre+1,Res = 0;
        while(pre<size-2)
        {
            while(left<right)
            {
                int sum = (nums[pre]+nums[right]+nums[left]);
                int delta = abs(sum-target);
                if(Min>delta) 
                {
                    Min = delta;
                    Res = sum;
                }
                if(sum<target) left++;
                else right--;
            }
            pre++,right = size-1,left = pre+1;
        }
        return Res;
        
    }
};

上面的题目是一个类型,那么当我们需要不能排序的情况,或者整体大小情况未知的时候,该怎么办?

9. 回文数

https://leetcode-cn.com/problems/palindrome-number/

灵活机动,将数字变成字符串,这样就可以诸位进行访问,然后双指针操作,一个从左一个从右开始,向中间收缩,一旦不相等,break; 

class Solution {
public:
    bool isPalindrome(int x) {
        string num = to_string(x);
        int size = num.size();
        int right = size-1,left = 0;
        while(left<right)
        {
            if(num[left] != num[right]) return false;
            left++;
            right--;
        }
        return true;
    }
};

11. 盛最多水的容器

https://leetcode-cn.com/problems/container-with-most-water/

(参考解答:https://leetcode-cn.com/problems/container-with-most-water/solution/on-shuang-zhi-zhen-jie-fa-li-jie-zheng-que-xing-tu/

暴力解法:

class Solution {
public:
    int maxArea(vector<int>& height) {
        int size = height.size();
        if(size == 0) return 0;
        if(size == 2) 
        {
            return min(height[0],height[1]);
        }
        int pre = 0,right = size-1,left = pre+1,MAX = 0;
        while(pre<size-2)
        {
            while(left<right)
            {
                int sum1 = min(height[left],height[pre])*(left-pre);
                int sum2 = min(height[right],height[pre])*(right-pre);
                int tempMAX = max(sum1,sum2);//计算最大范围
                MAX = max(MAX,tempMAX);
                left++;
            }
            pre++,right = size-1,left = pre+1;
        }
        return MAX;   
    }
};

显然是超时,那么我们应该怎样解决这个问题呢?简化双指针

就两个指针,一左一右,计算面积,然后收缩,怎么收缩呢,收缩牵扯到长方形长边的缩短,所以要找出最大值,我们要固定right和left中的较大值,移动较小的值。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0, right = height.size() - 1;
        int Res = 0;
        while (left < right) {
            int sum = min(height[left], height[right]) * (right - left);
            Res = max(Res, sum);
            //移动小的边界
            if (height[left] <= height[right]) ++left;
            else right--;
        }
        return Res;
    }
};
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(官方题解:https://leetcode-cn.com/problems/container-with-most-water/solution/sheng-zui-duo-shui-de-rong-qi-by-leetcode-solution/

 

面试题 16.06. 最小差 

https://leetcode-cn.com/problems/smallest-difference-lcci/solution/

此题和上面的题目如出一辙,上面是没有办法排序,本次是没有办法知道两个数组彼此之间的大小情况

那么还是老办法,让他们自己排序,然后两个指针,各自指向各自的首地址,算完差值后,二者比较,大的肯定是动不了,大的动了二者差距越来越大,小的动,以此类推不断循环。

class Solution {
public:
    int smallestDifference(vector<int>& a, vector<int>& b) {
        int sizea = a.size(),sizeb = b.size();
        if(sizea == 0||sizeb == 0) return 0;
        sort(a.begin(),a.end());
        sort(b.begin(),b.end());
        long p1 = 0,p2 = 0,res = INT_MAX;
        while(p1<sizea&&p2<sizeb)
        {
            long temp = abs(a[p1]-b[p2]);
            res = min(res,temp);
            if(a[p1]<b[p2])//现在,大的增大,差距更大,让小的增大
            p1++;
            else p2++;
            cout<<"temp:"<<temp<<"res:"<<res<<endl;
        }
        return res;
    }
};

240. 搜索二维矩阵 II

 https://leetcode-cn.com/problems/search-a-2d-matrix-ii/

本题最大的技巧,是从右上角开始,因为如果从左上角开始,上面右面都是大值,这怎么移动?

反而是右上角a点为起始,比target大了,左移(因为a的值本行最大),比target小了,下移(因为a的值本行最小)

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty()||matrix[0].empty()) return false;
        int m = matrix.size(),n = matrix[0].size();
        ;
        int Hbegin = 0,Lbegin = n-1;
        while(Hbegin<m&&Lbegin>=0)
        {
            if(matrix[Hbegin][Lbegin] == target) return true;
            if(matrix[Hbegin][Lbegin] > target) Lbegin--;
            else Hbegin++;
        }
        return false;
        
    }
};

 

面试题21. 调整数组顺序使奇数位于偶数前面

https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/

优秀代码:

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        if(nums.empty()) return {};
        int size = nums.size();
        int left = 0,right = size - 1;
        while(left<right)
        {
            while(left<size&&nums[left]%2!=0) left++;//找第一个偶数
            cout<<"left"<<left<<endl;
            while(right>=0&&nums[right]%2==0) right--;//找最后一个奇数
            cout<<"right"<<right<<endl;
            if(right<0||left>=size||left>right) break;
            swap(nums[left],nums[right]);
            left++,right--;
        }
        return nums;

    }
};

垃圾(但是实在是太容易想到了):

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        if(nums.empty()) return {};
        int size = nums.size();
        vector<int>Res;
        unordered_map<int,int>M;
        for(int i = 0;i<nums.size();++i)
        {
            if(nums[i]%2 != 0)
            {Res.push_back(nums[i]);M[i]++;}
        }
        for(int i = 0;i<nums.size();++i)
        {
            if(M[i]!=1)
            Res.push_back(nums[i]);
        }
        return Res;

    }
};

 

1471. 数组中的 k 个最强值

https://leetcode-cn.com/problems/the-k-strongest-values-in-an-array/

周赛第二题:

本题叙述还是比较复杂的,我们整理一下内容:

  1. 排序
  2. 找中位数
  3. 寻找满足条件的数字

前两点非常好满足

第三点:我们看表达式一的样子,就是在求和中位数的差值(绝对值),越大越好,那么对于一个排序数组来说,首尾必然是差值最大的地方,但是具体差多少,是首差值大还是尾差值大,我们不知道

再看表达式二,当二者差值相等,本身值谁大选择谁,因为是排序数组,显然是序号越大,越靠近尾部的值大。

综上,直接双指针法,left = 0,right = size - 1

对比 abs(arr[right] - arr[mid]) 和 abs(arr[mid] - arr[left])的关系,大于等于,其实都是选择right值

只有当小于的时候,才会选择left,我们让while跳出条件为left = right即可。

class Solution {
public:
    vector<int> getStrongest(vector<int>& arr, int k) {
        if(arr.empty()) return {};
        sort(arr.begin(),arr.end());//排序
        //找中位数
        int size = arr.size();
        int mid = arr[size/2];
        //找到合适的内容
        vector<int>Res;
        int right = size-1,left  = 0;
        mid = left + (right - left)/2;
        while(left<=right)
        {
            if(k == 0) break;
            if(abs(arr[right] - arr[mid]) > abs(arr[mid] - arr[left])) //第一条规则
            {Res.push_back(arr[right]);right--;k--;}
            else if(abs(arr[right] - arr[mid]) == abs(arr[mid] - arr[left]))//第二条规则
            {Res.push_back(arr[right]);right--;k--;}//必然是后面的大
            else {Res.push_back(arr[left]);left++;k--;}
        }
        return Res;

    }
};

 

581. 最短无序连续子数组

https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/

本题我们要的是双指针,从头开始,扫描,找单调递增中的异常,也就是下降沿

找到下降沿后,需要移动左指针,找到下降沿的起点,同时移动右指针,找到下降沿的终点

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        int right = 1,left = 0,begin = 0;
        int NewB = size,NewE = 0;
        while(right<size)
        {
            if(nums[left]<=nums[right]) {right++;left++;}
            else if(nums[left]>nums[right]) 
            {
                int Ltemp = left;
                while(Ltemp>=0)//检查左侧,有没有高的
                {
                    if(nums[Ltemp]>nums[right]) Ltemp--;
                    else break;
                }
                int Rtemp = right;
                while(Rtemp<size)//检查右侧,有没有低的
                {
                    if(nums[left]>nums[Rtemp]) Rtemp++;
                    else break;
                }
                
                NewB = min(NewB,Ltemp);NewE = max(NewE,Rtemp);//记录起点和终点
                right++;left++;
            }
        }

        return NewE==0?0:NewE-NewB-1;
    }
};

 

283. 移动零

https://leetcode-cn.com/problems/move-zeroes/

双指针原地交换,并不难

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        if(nums.empty()) return;
        int right = 1,left = 0;
        while(right<nums.size())
        {
            if(nums[left] == 0&&nums[right] == 0) {right++;continue;}//都为0
            if(nums[left] != 0&&nums[right] != 0) {right += 2;left += 2;continue;}
            if(nums[left] == 0&&nums[right] != 0) 
            {
                swap(nums[left],nums[right]);
                if(right-left>1) left++;
                else {right++;left++;}
            }
            else {right++;left++;}
        }
        return;
    }
};

75. 颜色分类

https://leetcode-cn.com/problems/sort-colors/

三指针分类解题:

三个指针,C2探索2的左边界,C0探索0的右边界,PCur保存正常遍历

当PCur指向0时,将C0和PCur交换,二者同时增加步长

当PCur指向2时,将C2和PCur交换,此时收缩2的边界指针C2,也就是将C2--,但是此时PCur不能动,因为你不知道现在PCur指向的是谁,需要在下一轮循环中判断。

比如下面的情况

错误程序: 

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //原地排序
        int C2 = nums.size()-1,C0 = 0,PCur = 0;
        while(PCur<C2)//错误地点!!!!!!!!!!!!!!!!!!!
        {
            if(nums[PCur]==0)//等于0
            {
                swap(nums[PCur],nums[C0]);
                C0++,PCur++;
            }
            else if(nums[PCur]==2)//等于2
            {
                swap(nums[PCur],nums[C2]);
                C2--,PCur++;//错误地点!!!!!!!!!!!!!!!!!!!                
            }
            else PCur++;
        }
    }
};

本题要特别的注意细节问题:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //原地排序
        int C2 = nums.size()-1,C0 = 0,PCur = 0;
        while(PCur<=C2)
        {
            if(nums[PCur]==0)//等于0
            {
                swap(nums[PCur],nums[C0]);
                C0++,PCur++;//交换完后,都移动
            }
            else if(nums[PCur]==2)//等于2
            {
                swap(nums[PCur],nums[C2]);
                C2--;//只减少2的边界               
            }
            else PCur++;
        }
    }
};

更加巧妙的双指针(从后往前遍历)

88. 合并两个有序数组 https://leetcode-cn.com/problems/merge-sorted-array/

class Solution {
public:
    // m+n 方法1:新建数组,从头开始合并
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int size1 = nums1.size(),size2 = nums2.size();
        vector<int>Res;
        int i = 0,j = 0;
        while(i<m&&j<n){
            if(nums1[i]<nums2[j]) Res.push_back(nums1[i++]);
            else Res.push_back(nums2[j++]);
        }
        for(int k = i;k<m;++k) Res.push_back(nums1[k]);
        for(int k = j;k<n;++k) Res.push_back(nums2[k]);
        copy(Res.begin(),Res.end(),nums1.begin());
    }
    // size1logsize1 方法2:将nums2插入到nums1尾部,然后整体排序
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int size1 = nums1.size(),size2 = nums2.size();
        for(int i = m,j = 0;j<n&&i<size1;++i,++j) nums1[i] = nums2[j];
        sort(nums1.begin(),nums1.end());
    }
    //m+n 方法3:反着来,从后往前遍历
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int size1 = nums1.size(),size2 = nums2.size();
        int end1 = m - 1,end2 = n-1,insert = size1-1;
        while(end1>=0&&end2>=0){
            if(nums1[end1]>nums2[end2]) nums1[insert--] = nums1[end1--];
            else nums1[insert--] = nums2[end2--];
        }
        // cout<<end1<<" "<<end2<<endl;
        for(int k = end1;k>=0;--k) nums1[insert--] = nums1[k];
        for(int k = end2;k>=0;--k) nums1[insert--] = nums2[k];
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值