滑动窗口+队列--面试题48、57-I、-II、59-I、-II

  • 注:滑动窗口可看成一个双端队列

面试题48 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

方法一、滑动窗口(以双指针实现,两个指针控制窗口滑动)

参考 LeetCode3 无重复字符的最长子串

滑动窗口的相关题目

方法二 动态规划

面试题57-I 和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]

示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]

方法:双指针

题解:
在这里插入图片描述

代码:

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

时间复杂度 O(N): N 为数组 nums的长度;双指针共同线性遍历整个数组。
空间复杂度 O(1): 变量 i, j 使用常数大小的额外空间。

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

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

方法:(滑动窗口)

设连续正整数序列的左边界 left 和右边界 right,则可构建滑动窗口从左向右滑动。循环中,每轮判断滑动窗口内元素和与目标值 target的大小关系,若大于 target 则移动左边界 left (以减小窗口内的元素和);若小于 target 则移动右边界 right (以增大窗口内的元素和);若相等则记录结果,并移动左边界(或右边界)。

代码:

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> res;
        int left=1, right=2, sum=3;
        while(left<right){
            if(sum<target){
                right++;
                sum += right;
            }
            else if(sum>target){
                sum -= left;
                left++;
            }
            else{
                vector<int> tmp;
                for(int i=left; i<=right; i++){
                    tmp.push_back(i);
                }
                res.push_back(tmp);
                sum -= left;
                left++;
            }
        }
        return res;
    }
};

Java

class Solution {
    public int[][] findContinuousSequence(int target) {
        if(target < 3) return new int[0][0];
        LinkedList<int[]> res = new LinkedList<>();
        int i = 1 , j =2 , sum = 3;
        while(i < j){
            if(sum == target){
                int[] temp = new int[j - i + 1];
                for(int k = i ; k <= j ; k++){
                    temp[k - i] = k;
                }
                res.add(temp);
            }
            if(sum >= target){
                sum -= i;
                i++;
            }else{
                j++;
                sum += j;
            }
        }

        return res.toArray(new int[res.size()][]);

    }
}

在这里插入图片描述

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

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
在这里插入图片描述
题解:

  • 我们可以动态维护一个双端队列,让队列的头部(front())始终是当前滑动窗口的最大值的索引。
  • 这样我们每次只需要取该索引对应的值,就是当前滑动窗口的最大值了。
  • 注意,我们的双端队列中储存的是元素的索引,而不是元素的值。因为当当前滑动窗口已经离开队列的头部索引所指向的值时,我们需要弹出当前队列的头部索引。而只有让队列储存的是索引,通过索引运算才能办到这一点。
  • 之所以用双端队列,是因为容器的头部和尾部都会弹出元素,所以需要一个两端开口的容器。

具体思路参考《剑指offer》289-291解析:

代码:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> res;
        deque<int> index;
        if(k<=0 || k>nums.size() || nums.size()==0) return res;
        //先移动生成第一个完整的滑动窗口,并计算最大值
        for(int i=0; i<k; i++){
            while(!index.empty() && nums[i]>=nums[index.back()])
                index.pop_back();
            index.push_back(i);
        }
        for(int i=k; i<nums.size(); i++){
            //这是先返回上一个滑动窗口的最大值,所以最后一个滑动窗口的最大值,并没有在for循环里返回,循环外需加上。
                res.push_back(nums[index.front()]);

            //nums[i]>=nums[index.back()]则nums[index.back()]肯定不是当前滑动窗口的最大值,弹出;否则nums[i]可能是一个滑动窗口的最大值;
            while(!index.empty() && nums[i]>=nums[index.back()])
                index.pop_back();
            //将不属于当前滑动窗口的弹出,剩下的队列头元素则是当前滑动窗口的最大值;
            if(!index.empty() && (i-index.front()) >= k)
                index.pop_front();
            index.push_back(i);
        }
         // 循环外加上的最后一个滑动窗口最大值
        res.push_back(nums[index.front()]);
        return res;
    }
};

Java

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k > nums.length || k <= 0) return new int[0];
        Deque<Integer> q = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];
        for(int i = 0 ; i < k ; i++){
            while(!q.isEmpty() && nums[i] >= nums[q.peekLast()]) q.removeLast();
            q.addLast(i);
        }
        res[0] = nums[q.peekFirst()];
        for(int i = k ; i < nums.length ; i++){
            while(!q.isEmpty() && nums[i] >= nums[q.peekLast()]) q.removeLast();
            q.addLast(i);
            if(!q.isEmpty() && i - q.peekFirst() >= k) q.removeFirst();
            res[i - k + 1] = nums[q.peekFirst()];
        }
        return res;
    }
}

可以将生成第一个滑动窗口的过程合并到第二个for循环中:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> res;
        deque<int> index;
        if(k<=0 || k>nums.size() || nums.size()==0) return res;
        //此时i从0开始
        for(int i=0; i<nums.size(); i++){
            //控制先移动生成第一个完整的滑动窗口,并计算最大值
            if(i>=k)
                //这是先返回上一个滑动窗口的最大值,所以最后一个滑动窗口的最大值,并没有在for循环里返回,循环外需加上。
                //之所以用if(i>=k),是因为首先是一个完整的滑动窗口,才能计算最大值。
                res.push_back(nums[index.front()]);

            //nums[i]>=nums[index.back()]则nums[index.back()]肯定不是当前滑动窗口的最大值,弹出;否则nums[i]可能是一个滑动窗口的最大值;
            while(!index.empty() && nums[i]>=nums[index.back()])
                index.pop_back();
            //将不属于当前滑动窗口的弹出,剩下的队列头元素则是当前滑动窗口的最大值;
            if(!index.empty() && (i-index.front()) >= k)
                index.pop_front();
            index.push_back(i);
        }
         // 循环外加上的最后一个滑动窗口最大值
        res.push_back(nums[index.front()]);
        return res;
    }
};

时间复杂度:O(N)
空间复杂度:O(N)

面试题 59 - II. 队列的最大值(*)

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

方法:deque + queue(双端队列) 的组合

思路:
考虑构建一个单调不增列表来保存队列 递减的元素 ,单调不增链表随着入队和出队操作实时更新,这样队列最大元素就始终对应单调不增列表的首元素,实现了获取最大值 O(1)时间复杂度。

注:

  • 单调不增列表用双端队列实现;
  • 设计双向队列为 单调不增 的原因:若队列 queue 中存在两个值相同的最大元素 ,此时 queue 和 deque 同时弹出一个最大元素,而 queue 中还有一个此最大元素;即采用单调递减将导致两队列中的元素不一致。

具体步骤:

  • 我们先建立一个普通队列 allData,用来储存所有的元素。我们再动态维护一个双端队列 maxData,它的头部元素永远是当前 allData 里所有元素的最大值,用来满足 max_value() 的调用。接下来我们来逐个分析题目要我们实现的三个函数:max_value()、push_back() 以及 pop_front() 的思路。
  • max_value():很自然,这个函数是要我们返回当前队列元素的最大值,那我们直接返回 maxData.front() 就好了;当 maxData 为空时,返回 -1。
  • push_back():首先我们要将新元素插入 allData 中,因为 allData 是储存所有元素的。然后我们应该更新 maxData,假如新插入的元素比 maxData 中已有的一些元素大,那么这些比新插入元素小的元素在任何时期都不可能是最大的。因为新插入的元素肯定会比这些元素要晚弹出 allData 队列,而且新元素比这些元素都大,所以只要新插入元素没有弹出队列,最大值就是这个元素,所以,比前面比新插入元素小的都要弹出;
    假如新插入的元素比 maxData 中已有的一些元素小,那么这个新插入的元素可能是在弹出“前面比它大的数”后,的最大值,所以此时不需弹出任何数据。
  • pop_front():首先我们需要返回的是 allData 队列中的头部元素。但是同时我们需要对 maxData 的头部元素做一个判断。假如 allData 弹出的元素恰好就是当前队列中最大的元素(也就是 maxData 的头部元素),我们需要将 maxData 的头部元素也同步弹出;但如果 allData 弹出的元素并不是 maxData 的头部元素,则不需要动 maxData。

代码:

class MaxQueue {
public:
    queue<int> allDdata;
    deque<int> maxData;
    MaxQueue() {

    }
    
    int max_value() {
        if(maxData.empty()) return -1;
        int res = maxData.front();
        return res;
    }
    
    void push_back(int value) {
        allDdata.push(value);
        while(!maxData.empty() && value>maxData.back()){
        	// 均摊时间复杂度满足 O(1)。因为最坏情况是某一次插入操作有 n 次出队操作,且每个数字只出队一次,
            // 那么把这 n 次出队平摊到前面的 n 次插入操作的话,均摊到每一次的出队操作就是 O(1) 了。
            maxData.pop_back();
        }
        maxData.push_back(value);
    }
    
    int pop_front() {
        if(allDdata.empty()) return -1;
        int res = allDdata.front();
        allDdata.pop();
        if(res==maxData.front()) maxData.pop_front();
        return res;

    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

Java

class MaxQueue {

    Queue<Integer> q;
    Deque<Integer> dq;
    public MaxQueue() {
    q = new LinkedList<>();
    dq = new LinkedList<>();
    }
    
    public int max_value() {
        if(q.isEmpty()) return -1;
        int res = dq.peekFirst();
        return res;
    }
    
    public void push_back(int value) {
        q.add(value);
        while(!dq.isEmpty() && value > dq.peekLast()) dq.removeLast();
        dq.addLast(value);
    }
    
    public int pop_front() {
        if(q.isEmpty()) return -1;
        int res = q.remove();
        if(res == dq.peekFirst()) dq.removeFirst();
        return res;
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

时间复杂度 O(1) : max_value(), push_back(), pop_front() 方法的均摊时间复杂度均为 O(1);
空间复杂度 O(N): 当元素个数为 N 时,最差情况下deque 中保存 N 个元素,使用 O(N) 的额外空间;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值