阿翰 剑指offer 之 Day 27 栈与队列 困难

目录

栈与队列

1 滑动窗口的最大值

1. 暴力

2.  模拟-保存窗口最大值

3. 单调队列

4. 单调队列 优化 

5. 官方题解

5.1 优先队列

​延伸一题「大根堆的应用」

5.2 单调队列 

5.3 分块 + 预处理

2 滑动窗口的最大值

1. 暴力更新 

​2. 递减队列


栈与队列

1 滑动窗口的最大值

剑指 Offer 59 - I. 滑动窗口的最大值https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/

1. 暴力

模拟一下,按照每一次滑动的初始位置来求最大值即可。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0) return new int[0];
        if (1 == k) return nums; 
        int i = 0;
        int max = 0;
        int t = 0;
        int[] res = new int[nums.length - k + 1];
        int w = 0;
        while ((i+k)<= nums.length){
            // if(t > max) max = t;
            max = nums[i];
            for (int l = i; l < i+k; l++) {
                max = Math.max(max, nums[l]);
            }
            i++;
            res[w++] = max;
        }
        return res;
    }
}

2.  模拟-保存窗口最大值

如果上一个窗口里,最大值不是第一个值,则可以直接进行max(前一个窗口最大值,新滑进来元素)

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0) return new int[0];
        if (1 == k) return nums;
        int i = 0;
        int max = 0;
        int[] res = new int[nums.length - k + 1];
        int w = 0;
        for (int l = i; l < i+k; l++) {
            max = Math.max(max, nums[l]);
        }
        res[w++] = max;
        i++;
        while ((i+k)<= nums.length){
            if(res[w-1] == nums[i-1]){
                max = nums[i];
                for (int l = i; l < i+k; l++) {
                    max = Math.max(max, nums[l]);
                }
            }else{
                max = Math.max(res[w-1], nums[i+k-1]);
            }
            i++;
            res[w++] = max;
        }
        return res;
    }
}

3. 单调队列

  • 设数组 nums 的长度为 n ,则共有 (n-k+1) 个窗口;
  • 获取每个窗口最大值需线性遍历,时间复杂度为 O(k) 。

 本题难点: 如何在每次窗口滑动后,将 “获取窗口内最大值” 的时间复杂度从 O(k) 降低至 O(1) 。

回忆 剑指Offer 30. 包含 min 函数的栈 ,其使用 单调栈 实现了随意入栈、出栈情况下的 O(1) 时间获取 “栈内最小值” 。本题同理,不同点在于 “出栈操作” 删除的是 “列表尾部元素” ,而 “窗口滑动” 删除的是 “列表首部元素” 。

窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证单调队列 deque :

  1. deque 内 仅包含窗口内的元素 ⇒ 每轮窗口滑动移除了元素 nums[i−1] ,需将 deque 内的对应元素一起删除。
  2. deque 内的元素 非严格递减 ⇒ 每轮窗口滑动添加了元素nums[j+1] ,需将 deque内所有 < nums[j + 1]的元素删除。

算法流程:

个人感觉不存deque,只需要max进行比较存储 ,再根据不同情况,比较或遍历k个数字即可。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int[0];
        Deque<Integer> deque = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];
        for(int j = 0, i = 1 - k; j < nums.length; i++, j++) {
            // 删除 deque 中对应的 nums[i-1]
            if(i > 0 && deque.peekFirst() == nums[i - 1])
                deque.removeFirst();
            // 保持 deque 递减
            while(!deque.isEmpty() && deque.peekLast() < nums[j])
                deque.removeLast();
            deque.addLast(nums[j]);
            // 记录窗口最大值
            if(i >= 0)
                res[i] = deque.peekFirst();
        }
        return res;
    }
}

4. 单调队列 优化 

将 “未形成窗口” 和 “形成窗口后” 两个阶段拆分到两个循环里实现。代码虽变长,但减少了冗余的判断操作。

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

5. 官方题解

对于每个滑动窗口,可以使用 O(k) 的时间遍历其中的每一个元素,找出其中的最大值。对于长度为 n 的数组 nums 而言,窗口的数量为 n-k+1 ,因此该算法的时间复杂度为 O((n−k+1)k)=O(nk),会超出时间限制,因此我们需要进行一些优化。

可以想到,对于两个相邻(只差了一个位置)的滑动窗口,它们共用着 k-1 个元素,而只有 1 个元素是变化的。可以根据这个特点进行优化。(方法1、2我就是根据这个来做的~)

5.1 优先队列

对于「最大值」,可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

对于本题而言,初始时,将数组 nums 的前 k 个元素放入优先队列中。每当向右移动窗口时,就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,可以将其永久地从优先队列中移除。

不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组(num,index),表示元素 num 在数组中的下标为 index。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if(n == 0 || k == 0) return new int[0];
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] pair1, int[] pair2) {
                return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
            }
        });
        for (int i = 0; i < k; ++i) {
            pq.offer(new int[]{nums[i], i});
        }
        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];
        for (int i = k; i < n; ++i) {
            pq.offer(new int[]{nums[i], i});
            while (pq.peek()[1] <= i - k) {
                pq.poll();
            }
            ans[i - k + 1] = pq.peek()[0];
        }
        return ans;
    }
} 

延伸一题「大根堆的应用

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

1. 建立大顶堆

大顶堆执行poll()去掉的是大数,留下的是小数。构造方法传入(w1, w2) -> w2 - w1 // 第二个参数减第一个是大顶堆

执行heap.poll()这样会将大的数出队,剩下的是小的数据。

import java.util.*;


public class Solution {

    public static void main(String[] args) {
        int[] arr = new int[]{3,2,1,6,9};
        int[] res = getLeastNumbers(arr, 4);
        System.out.println(Arrays.toString(res));
    }
    public static int[] getLeastNumbers(int[] arr, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>(
                (w1, w2) -> w2 - w1    // 第二个参数减第一个是大顶堆
        );

        for (int value : arr) {
            heap.offer(value);
            if (heap.size() > k) heap.poll();
        }

        int[] res = new int[k];
        int j = 0;
        while (! heap.isEmpty()) {
            res[j++] =  heap.poll();
        }
        return res;
    }
}

2. 建立小顶堆

PriorityQueue默认建立小顶堆
执行poll()出队的是值小的数据,保留的是值大的数据

import java.util.*;


public class Solution {

    public static void main(String[] args) {
        int[] arr = new int[]{3,2,1,6,9};
        int[] res = getLeastNumbers(arr, 3);
        System.out.println(Arrays.toString(res));
    }
    public static int[] getLeastNumbers(int[] arr, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();

        for (int value : arr) {
            heap.offer(value);
            if (heap.size() > k) heap.poll();
        }

        int[] res = new int[k];
        int j = 0;
        while (! heap.isEmpty()) {
            res[j++] =  heap.poll();
        }
        return res;
    }
}

5.2 单调队列 

顺着方法一的思路继续进行优化。

也就是方法 4 。以上是官方题解的说法。

5.3 分块 + 预处理

可以将数组 nums从左到右按照 k 个一组进行分组,最后一组中元素的数量可能会不足 k 个。如果我们希望求出 nums[i] 到 nums[i+k−1] 的最大值,就会有两种情况:

 

这一手预处理 太秀了  

//    int[] nums = new int[]{1,3,-1,-3,5,3,6,7};
    public int[] maxSlidingWindow(int[] nums, int k) {
        System.out.println(Arrays.toString(nums));
        int n = nums.length;
        int[] prefixMax = new int[n];//表示下标 i 对应的分组中,以 i 结尾的前缀最大值
        int[] suffixMax = new int[n];//表示下标 i 对应的分组中,以 i 开始的后缀最大值
        for (int i = 0; i < n; ++i) {
            if (i % k == 0) {
                prefixMax[i] = nums[i];
            }
            else {
                prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
            }
        }
        System.out.println(Arrays.toString(prefixMax));
        for (int i = n - 1; i >= 0; --i) {
            if (i == n - 1 || (i + 1) % k == 0) {
                suffixMax[i] = nums[i];
            } else {
                suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
            }
        }

        System.out.println(Arrays.toString(suffixMax));
        int[] ans = new int[n - k + 1];
        for (int i = 0; i <= n - k; ++i) {
            ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);
        }
        return ans;
    }
[1, 3, -1, -3, 5, 3, 6, 7]
[1, 3, 3, -3, 5, 5, 6, 7]
[3, 3, -1, 5, 5, 3, 7, 7]
[3, 3, 5, 5, 6, 7]

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们需要 O(n) 的时间预处理出数组 prefixMax,suffixMax 以及计算答案。
  • 空间复杂度:O(n),即为存储 prefixMax 和 suffixMax 需要的空间。

2 滑动窗口的最大值

剑指 Offer 59 - II. 队列的最大值https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/

最直观的想法是 维护一个最大值变量 ,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值 ,因此不可行。

这里的想法是在pop_front()的时候,判断出的是不是当前最大值,如果是,需要进行最大值的一个更新。

1. 暴力更新 

class MaxQueue {
    int max = Integer.MIN_VALUE;
    Deque<Integer> deque = null;
    public MaxQueue() {
        deque = new LinkedList<>();
    }

    public int max_value() {
        if(deque.isEmpty()) return -1;
        return max;
    }

    public void push_back(int value) {
        if(max < value){
            max = value;
        }
        deque.push(value);
    }

    public int pop_front() {
        if(deque.isEmpty()) return -1;
        int res = deque.pollLast();
        if(res == max){
            max = Integer.MIN_VALUE;
            Deque<Integer> deque2 = new LinkedList<>();
            while(!deque.isEmpty()){
                int t = deque.pop();
                if(max < t) max = t;
                deque2.push(t);
            }
            while (!deque2.isEmpty()) {
                deque.push(deque2.pop());
            }

        }
        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();
 */

2. 递减队列

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

使用双向队列原因:维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。 

  •  入队时,递减队列和队列queue都push,但是递减队列要移除之前入队了的所有小于新入队的元素,保持递减。
  • 出队时,如果递减队列和队列队首相等,则一同出队,否则,只有队列出队。
  • 求max的时候,返回递减队列队首元素。
class MaxQueue {
    Queue<Integer> queue;
    Deque<Integer> deque;
    public MaxQueue() {
        queue = new LinkedList<>();
        deque = new LinkedList<>();
    }
    public int max_value() {
        return deque.isEmpty() ? -1 : deque.peekFirst();
    }
    public void push_back(int value) {
        queue.offer(value);
        while(!deque.isEmpty() && deque.peekLast() < value)
            deque.pollLast();
        deque.offerLast(value);
    }
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        if(queue.peek().equals(deque.peekFirst()))
            deque.pollFirst();
        return queue.poll();
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值