zoukankan      html  css  js  c++  java
  • LeetCode 双指针、滑动窗口、单调队列专题

    167. 两数之和 II - 输入有序数组

    暴力做法O(n^2):枚举两个数的组合,两层循环

    优化,因为具有单调性

    使i和j分别指向数组末尾和开头

    对于固定了的a[j],我们去可以找到一个a[i] 使得 a[j] + a[i] >= target;

    然后向右移动j,那么i肯定要向左移动,这就是双指针适用于单调性问题

    把两层循环优化到一层循环,复杂度(O(2n))

    class Solution {
    public:
        vector<int> twoSum(vector<int>& numbers, int target) {
            int n = numbers.size();
            //i向右移动number[i]增大
            //为使得和等于target 必须要将j向左移动 即numbers[j]减小
            for(int i=0,j = n-1;i<n,j>i;i++){
                while(j > i && 
                	numbers[j] + numbers[i] > target) j--;
                if(j > i && numbers[j] + numbers[i] == target) 
                	return 	{i+1,j+1};
            }
            return {-1,-1};
        }
    };
    

    方法二:

    也能用二分,由于数组中可能有重复元素,所以要判断一下i==j的情况

    如下样例:

    class Solution {
    public:
        vector<int> twoSum(vector<int>& numbers, int target) {
            int n = numbers.size();
            for(int i=0;i<n;i++){ // i + j >= 9  j >= 9-i
                if(numbers[i] > target) break;
                int j= lower_bound(numbers.begin(),numbers.end(),target-numbers[i]) - numbers.begin();
                if(i == j) j = j + 1;
                if(j < n && numbers[i] + numbers[j] == target) return {i+1,j+1};
            }
            return {-1,-1};
        }
    };
    

    88. 合并两个有序数组

    归并排序的思想,把两个有序序列合并为一个有序序列

    看出是双指针,初始时两个指针同时先指向末尾,第三个指针指向下一个存放位置

    红指针、蓝指针分别指向自己数组中的最大值,取二者指向的最大值

    把两个数组中的比较大的值先放到新答案数组中,然后移动这个指针

    为什么要从大到小来做,因为题目要去存入到第一个数组中
    如果从前开始绿色指针会覆盖蓝色数组没用过的数字,而第一个数组末尾的数是空位置,所以可以从大到小来做

    注意边界,数组可能为空!

    class Solution {
    public:
        void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
            int p = m-1, q = n-1, r = m + n - 1;
            //注意p和q边界控制
            for(r = m + n - 1; r>=0, p>=0 && q>=0; r--){
                if(nums1[p] >= nums2[q]){
                    nums1[r] = nums1[p--];
                }else{
                    nums1[r] = nums2[q--];
                }
            }
            while(q >= 0 ) nums1[r--] = nums2[q--];
        }
    };
    

    26. 删除排序数组中的重复项

    就是unique去重函数

    class Solution {
    public:
        int removeDuplicates(vector<int>& nums) {
            return unique(nums.begin(),nums.end()) - nums.begin();
        }
    };
    

    两个指针,一个指针指向下一个存放位置
    另一个提前判断是否与第一个指针前一个所指向的位置的值相同。

    class Solution {
    public:
        int removeDuplicates(vector<int>& nums) {
            int n = nums.size();
            if(n == 0) return 0;
            if(n == 1) return 1;
            int p = 0,q = 1;
            while(p < n && q < n){
                while(q < n && nums[q] == nums[p]) q++;
                if(q < n && nums[q] != nums[p]) nums[++p] = nums[q];
            }
            return p+1;
        }
    };
    

    for循环写法,需要注意p不一定在每一次for循环结束就增加,比如最后一次不能满足的时候不增加,把p++写在该增加的地方就可以了

    class Solution {
    public:
        int removeDuplicates(vector<int>& nums) {
            int n = nums.size();
            if(n == 0) return 0;
            if(n == 1) return 1;
            int p,q;
            //用for循环不太好控制最终p的值 for循环后p不一定增加 
            for(p = 0,q = 1;q < n && p + 1 < n;){
                while(q < n && nums[q] == nums[p]) q++;
                if(q < n && nums[p] != nums[q]) 
                	nums[++p] = nums[q++];//这个时候p再++
            }
            return p + 1;
        }
    };
    

    76. 最小覆盖子串

    1.先想暴力写法

    枚举所有子串,看这个子层是不是包含全部字母,更新最小符合的子串长度

    2.有无单调性,怎么去优化

    单调性;假设i~j包含了子串T,那么当i++,j也一定是增加的,这就是单调性

    从左往右枚举i,移动j直到满足包含子串T,更新答案;然后自变量i++

    i和j都最多走了一遍,i、j不会往后走;所以时间复杂度是 O(2n) 即 O(n)

    如何判断i~j的子串包含了串T?
    初始时,哈希表统计T里面次数;

    另一个哈希表,每次移动j更新当前i~j子串的次数,判断是否满足,两个哈希表比较会超时

    超时代码:

    class Solution {
    public:
        map<char,int> cnt1,cnt2;
        bool check(string &s,string &t){
            for(int i=0;i<t.length();i++){
                if(cnt1[t[i]] < cnt2[t[i]]) return false;
            }
            return true;
        }
    
        string minWindow(string s, string t) {
            string ans = "";
            int len = 0x3f3f3f3f; 
            int n = s.length();
            int m = t.length();
            if(m > n) return ans;
            if(m == n && s == t) return t;
            for(int i=0;i<m;i++) {
                cnt2[t[i]]++;
            }
            for(int i=0,j = 0;i<n;i++){
                while(j < n && !check(s,t)){
                    cnt1[s[j]]++;
                    j++;
                }
                if(j < n && check(s,t) && j-i < len){
                    ans = s.substr(i,j-i);
                    len = j - i;
                }
                if(j == n && check(s,t) && j-i < len){
                    ans = s.substr(i,j-i);
                    len = j - i;
                }
                cnt1[s[i]]--;
            }
            return ans;
        }
    };
    

    优化成只用一个hash表

    先统计T里面的次数增加need值;记录需要T中字符的类型和数量

    有足够T字符类型和数量后,后面移动左指针来减少need值,更新最小长度,

    直到不满足所有类型数量了退出循环

    参考题解

    class Solution {
    public:
        string minWindow(string s, string t) {
            if(s == t) return t;
            unordered_map<char,int> need;
            unordered_map<char,int> have;
            int n = s.length();
            int m = t.length();
            if(m > n) return "";
            int typeNum = 0;
            //统计t里 出现的字符 以及 出现的个数
            for(int i=0;i<m;i++){
                if(need[t[i]] == 0) {
                    typeNum++;
                    have[t[i]] = 1;
                }
                need[t[i]]++;
            }
            int minLen = n+1;
            int start = 0;
            bool flag = false;
            for(int right = 0,left = 0; right < n; right++){
                char c = s[right];
                if(have[c]) need[c]--;
                if(have[c] && need[c] == 0) typeNum--;
                //当typeNum==0 说明满足了子串包含的条件 
                //这个时候可以考虑逐渐右移动left左指针 更新最小满足长度
                while(typeNum == 0){
                    //更新
                    if(right - left + 1 < minLen){
                        start = left;
                        minLen = right - left + 1;
                        flag = true;
                    }
                    //移除left后的变化
                    //如果这个字符是t里面的字符就去更新变化
                    if(have[s[left]]){
                        //这个left要丢弃了 下次需要的个数就要+1
                        need[s[left]]++; 
                        //如果need字符的个数大于0了 
                        //说明字符typeNum不是全都满足
                        if(need[s[left]] > 0) typeNum++;
                    }
                    left++;
                }
            }
            if(!flag) return "";
            return s.substr(start,minLen);
        }
    };
    

    另一种思路:

    string minWindow(string s, string t) {
    	unordered_map<char,int> need;
    	for(auto c : t) need[c]++;
    	int cnt = need.size();
    	string res;
    	for(int i=0,j=0,c=0;i<s.size();i++){
    		if(need[s[i]] == 1) c++;
    		need[s[i]]--;
    		while(need[s[j]] < 0) 
    			need[s[j++]]++;
    		if(c == cnt){
    			if(res.empty() || res.size() > i - j + 1) 
    				res = s.substr(j,i-j+1);
    		}
    	}
    	return res;
    }
    

    32. 最长有效括号

    有关括号序列的性质:

    1.括号对应的括号一定是对应的,不变的

    把左括号看成1,右括号看成-1;

    2.一个括号序列合法,等价于所有前缀和均>=0,并且总和 = 0

    即 从左到右前缀和小于0时,这段肯定不合法;

    只能由总和>=0 变成 = 0;才是合法序列;而由总和<=0变成=0就不是合法的序列

    正着判断一遍,还需要反着判断一遍

    避免正多负少时的 ((((()) 这种情况下,cnt一直大于0的情况时,cnt没有等于0无法统计合法序列

    反着一遍相当于)))(((((( ,这样就能统计负的少,合法序列了

    class Solution {
    public:
        int work(string s){
            int n = s.length();
            int i = 0, start = 0;
            int cnt = 0;
            int ans = 0;
            while(start < n && i < n){
                if(s[i] == '(') cnt++;
                else cnt--;
                if(cnt == 0){
                    ans = max(ans,i - start + 1);
                }
                if(cnt < 0) start = i + 1, cnt = 0;
                i++;
            }
            return ans;
        }
        int longestValidParentheses(string s) {
            int ans = work(s);
            reverse(s.begin(),s.end());
            //(:ASCII=40   ):ASCII = 41 异或一下交换
            for(auto &c :s) c^=1;
            return max(work(s),ans);
        }
    };
    

    思路二:开一map记录和当前i位置的前缀相同的最远一次的下标;下标相同且中间没有前缀和小于0的非法情况,就是一个合法答案了

    如:((()))

    前缀和:123210 下标从1开始 则 第0+1与倒1;倒2与第1+1匹配....

    再如:(())))()

    前缀和:1210 -1 清零记录0的位置 1 0

    双指针写法题解

    也可以用动态规划做

    239. 滑动窗口最大值

    长度k的窗口中的最大值

    思路一:O(n)枚举窗口起点,O(k)遍历一遍窗口,计算窗口的最小值;总时间复杂度:O(n×k)超时

    思路二:单调队列思想:删除冗余元素

    单调队列的思想:

    总结起来就是:位于窗口左侧,并且还偏小的数字一定不会成为答案;所以可以把这些数移除

    用单调队列,对头删除,队尾添加和删除;用双端队列deque

    class Solution {
    public:
        vector<int> maxSlidingWindow(vector<int>& nums, int k) {
            vector<int> result;
            deque<int> q;
            for(int i=0;i<nums.size();i++){
                //1.先判断队头是否已经在窗口外了 
                //已经在窗口外过期的要出队
                if(q.size() && i - q.front() + 1 > k) 
                	q.pop_front();
                //2.判断队尾是不是冗余 比当前新进来的num[i]值还小就出队
                while(q.size() && 
                	nums[q.back()] < nums[i]) q.pop_back();
                	//3. i入队
                	q.push_back(i); 
                //4. 记录这一轮答案
                if(i >= k - 1) 
                	result.push_back(nums[q.front()]); 
            }
            return result;
        }
    };
    

    918. 环形子数组的最大和

    拆环成链:环展开成链,两倍长度,限制长度为n

    题意就变为,找区间大小长度为1~n的数组连续子段和

    固定终点i,找距离1~n的j(窗口左端点,下标比i小),使得前缀和s[j] - s[i]和最小

    问题就转成了"查大小为n的滑动窗口的最小值"问题,查窗口中的最小值s[j]

    时间复杂度O(n)

    滑动窗口中的值是下标

    然后找下标对应的的前缀和最小值j

    class Solution {
    public:
        int maxSubarraySumCircular(vector<int>& A) {
            int n = A.size();
            for(int i=0;i<n;i++) A.push_back(A[i]);
            deque<int> q;
            vector<int> sum(2*n+1);
            for(int i=1;i<=n*2;i++) sum[i] = sum[i-1] + A[i-1];
            int ans = INT_MIN;
            q.push_back(0);
            for(int i=1;i<=2*n;i++){
                //1.队头是否在窗口
                if(q.size() && i - (q.front()+1) + 1 > n) q.pop_front();
                //2.更新答案 注意要在删除队尾前更新答案
                if(q.size()) ans = max(ans,sum[i] - sum[q.front()]);
                //3.删除队尾冗余
                while(q.size() && sum[q.back()] >= sum[i]) q.pop_back();
                //4.i入队
                q.push_back(i);
            }
            return ans;
        }
    };
    

    补充

    滑动窗口维护两个边界,可以看成两个指针

    可以把滑动窗口看成特殊的双指针算法;

    滑动窗口一定是双指针算法,双指针不一定是滑动窗口算法

  • 相关阅读:
    VSCode拓展插件推荐(HTML、Node、Vue、React开发均适用)
    算法_栈的Java的通用数组实现
    算法_计算输入的算术表达式的值.
    设计模式整理_组合模式
    JavaSE复习_9 集合框架复习
    一个小题目的三种不同的解法
    设计模式整理_状态模式
    设计模式整理_迭代器模式
    设计模式整理_模板模式
    JavaSE复习_8 泛型程序设计
  • 原文地址:https://www.cnblogs.com/fisherss/p/12988972.html
Copyright © 2011-2022 走看看