单调队列(java)

单调队列,顾名思义,就是队列中的值要么单调递增要么单调递减。之前做题的时候对这个并没有太在意,但是今天的每日抑题用堆实现的时候发现太耗时了,借此机会好好学一下单调队列。

leetcode1438

满足条件的最长连续子数组的长度,一看就是滑动窗口的题,按着模板来就行,但是这里难的点在于要维护窗口内的最大值和最小值,只要保证在扩张右边界和收缩左边界的时候可以拿到窗口内的最大值和最小值,这个题就迎刃而解了。所以第一想法是用两个堆来分别维护最大值和最小值,代码如下:

public int longestSubarray(int[] nums, int limit) {
    int ans = 0;
    int left = 0, right = 0;
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
    int maxVal = 0, minVal = 0;
    while (right < nums.length) {
        minHeap.offer(nums[right]);
        maxHeap.offer(nums[right]);
        minVal = minHeap.peek();
        maxVal = maxHeap.peek();
        while (maxVal - minVal > limit) {
            minHeap.remove(nums[left]);
            maxHeap.remove(nums[left]);
            minVal = minHeap.peek();
            maxVal = maxHeap.peek();
            left++;
        }
        ans = Math.max(ans, right - left + 1);
        right++;
    }
    return ans;
}

除了数据处理和之前写的滑动窗口的题不一样,模板的大致结构还是一样的。(求的是最大值,其实可以按之前博客里写的那样用if判断条件就可以,不用while收缩左边界,感兴趣的可以自己改一下代码)

但是这个代码最大的问题在于remove这个函数,别的语言压根就不提供这个方法,因为堆最重要的只有堆顶,所以删别的数是很麻烦的,看一下源码就知道了:

public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}
private int indexOf(Object o) {
    if (o != null) {
        for (int i = 0; i < size; i++)
            if (o.equals(queue[i]))
                return i;
    }
    return -1;
}

和大家能想到的一样,只能遍历整个数组来删数据,然后还要调整堆,所以这个方法最后耗时200+ms,击败16%。

单调队列

那么更简单的维护最大值和最小值的方法就是用单调队列了,可以保证删数据的时候不用遍历整个数组。先看一个简单的模板题:剑指offer59。这个题用堆当然可以实现,并且也不麻烦,但是下面换单调队列来实现 一下,顺便理解单调队列的思想。

既然是单调队列,那么队列内的数肯定是要单调递增或递减的,然后队首就是我们需要的最大值/最小值。那么应该是单调递增还是单调递减呢?现在要求的是最大值,那么单调队列应该是单调递减的,保证队首永远是当前队列中最大的值。下面举个简单的例子来理解一下。

比如现在有一个普通的队列用来存数:[5, 2, 3, 4, 7, 6, 1],那么它的单调队列应该存什么东西呢?一步步分析:

  • 5入队,单调队列同时入队5。此时单调队列为 [5]。
  • 2入队,2小于单调队列的队尾5,单调队列入队2,满足单调递减。此时单调队列为 [5, 2]。
  • 3入队,3大于单调队列的队尾2,单调队列应该弹出队尾2,此时单调队列为 [5],3小于单调队列的队尾5,单调队列入队3,满足单调递减。此时单调队列为 [5, 3]。

先看到这里,会发现为了满足单调递减的条件,需要不断调整单调队列弹出队尾,所以用的肯定是双端队列,那么意义在哪里呢?为什么3入单调队列的时候,2就一定要弹出去呢?这就需要代入求最大值的场景了,现在存数据的队列为[5, 2, 3],单调队列为[5, 3],假如队列不改变,那么单调队列的队首永远是5,也就是最大值一直是5。现在如果需要弹出普通队列的队首5,那么单调队列也一样要弹出队首,因为单调队列的队首也是5,既然存数据的队列已经弹出5了,那么再求最大值的时候肯定不能是5了。此时存数据的队列为[2, 3],单调队列为[3],是不是发现此时单调队列的队首依然是存数据队列的最大值呢?

这时再想一下为什么3入队,2就要从队尾弹出去。因为3既然比2大了,并且3还比2晚入队列,那么怎么都轮不到2当最大值,因为存数据的是普通队列,是不可能从队尾弹出数据的,所以只要2还在队列中,3就一定在,所以轮不到2当最大值。下面接着分析:

  • 4入队,单调队列调整为[5, 4]。
  • 7入队,会发现[5, 4]都比7小,所以一块踢出去,然后单调队列调整为[7]。此时队列为[5, 2, 3, 4, 7],单调队列为[7],也就是说前面的数都轮不到当最大值,只能是7,因为7只可能是最后一个被移出队列的。
  • 6入队,单调队列入队6。此时单调队列为[7, 6]。
  • 1入队,单调队列入队1。此时单调队列为[7, 6, 1]。

最终队列为[5, 2, 3, 4, 7, 6, 1],单调队列为[7, 6, 1]。此时不停从队列中移出队首,会发现单调队列的队首始终是当前队列的最大值,不需要我们去遍历单调队列删除某个数据。可以这么理解:单调队列的队首是当前队列最大值,后面的都是“候选”的最大值。一旦当前最大值在存数据的队列中被移出了,那么“候选”的最大值马上上位。

理解这个例子之后,代码就好写的多了:

public class MaxQueue {

    // 单调队列,因为要维护队头的最大值,所以单调队列是单调递减的,存的都是“候选”最大值
    private Deque<Integer> max;
    // 队列,存所有的数
    private Queue<Integer> queue;

    public MaxQueue() {
        max = new LinkedList<>();
        queue = new LinkedList<>();
    }

    public int max_value() {
        if (queue.isEmpty()) {
            return -1;
        } else {
            return max.peekFirst();
        }
    }

    public void push_back(int value) {
        queue.offer(value);
        // 维护单调队列的单调性
        while (!max.isEmpty() && max.peekLast() < value)
            max.pollLast();
        max.offer(value);
    }

    public int pop_front() {
        if (queue.isEmpty()) {
            return -1;
        } else {
            int poll = queue.poll();
            if (poll == max.peekFirst())
                max.pollFirst();
            return poll;
        }
    }
}

每日抑题的改进解法

会了单调队列后,就可以对上面的代码进行改进了:

public int longestSubarray(int[] nums, int limit) {
        Deque<Integer> max = new LinkedList<>();
        Deque<Integer> min = new LinkedList<>();
        int left = 0, right = 0, ans = 0;
        while (right < nums.length) {
            int offer = nums[right];
            // 右边界如果比max队列的队尾还大,那么只要这个数还在窗口内,比这个数还小的就不可能是最大值,可以踢出去
            while (!max.isEmpty() && offer > max.peekLast()) {
                max.pollLast();
            }
            // 右边界如果比min队列的队尾还小,那么只要这个数还在窗口内,比这个数还大的数就不可能是最小值,可以踢出去
            while (!min.isEmpty() && offer < min.peekLast()) {
                min.pollLast();
            }
            max.offer(offer);
            min.offer(offer);
            while (max.peekFirst() - min.peekFirst() > limit) {
                int poll = nums[left];
                // 踢出后max和min的队首还分别是最大值和最小值,比堆效率高
                if (min.peekFirst() == poll) min.pollFirst();
                if (max.peekFirst() == poll) max.pollFirst();
                left++;
            }
            ans = Math.max(ans, right - left);
            right++;
        }
        return ans;
    }

耗时30+ms,击败90%,比堆的效率提高太多。

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Deque<Integer> deque = new LinkedList<>(); for(int i=0; i<n; i++){ int num = scanner.nextInt(); while(!deque.isEmpty() && deque.getLast() > num){ deque.removeLast(); } deque.addLast(num); } System.out.println(deque.getFirst()); ### 回答2: 单调队列(Monotonic Queue)是一种特殊的队列数据结构,它具有以下特点: 1. 队列中的元素满足一定的单调性,例如单调递增或单调递减; 2. 每次插入一个新的元素时,会将队列中比新元素小的元素删除; 3. 队列中的元素按插入的顺序排列。 以下是用Java编写的单调队列的代码: ```java import java.util.*; public class MonotonicQueue { Deque<Integer> deque; // 可以用双端队列来实现单调队列 public MonotonicQueue() { deque = new ArrayDeque<>(); } // 向队尾插入元素,删除比新元素小的元素 public void push(int num) { while (!deque.isEmpty() && deque.getLast() < num) { deque.removeLast(); // 删除队尾元素 } deque.addLast(num); // 插入新元素到队尾 } // 返回队列中的最大元素 public int getMax() { return deque.getFirst(); // 队头元素始终是最大值 } // 删除队头元素 public void pop() { deque.removeFirst(); } } ``` 上述代码中,我们使用了Java的双端队列(Deque)来实现单调队列。在插入新元素时,我们不断地将队列中比新元素小的元素删除,以保持队列的单调性。同时,我们可以通过调用`getMax()`方法获取队列中的最大元素,通过调用`pop()`方法删除队头元素。 使用单调队列的时间复杂度为O(n),其中n为元素个数。 ### 回答3: 单调队列是一种特殊的队列,可以高效地找到队列中的最大或最小值。下面是一个使用Java实现的单调队列的代码示例: ```java import java.util.ArrayDeque; import java.util.Deque; public class MonotonicQueue { private Deque<Integer> queue; // 存储队列中的元素 private Deque<Integer> maxElements; // 存储单调队列中的最大值 public MonotonicQueue() { queue = new ArrayDeque<>(); maxElements = new ArrayDeque<>(); } public void push(int val) { while (!maxElements.isEmpty() && maxElements.peekLast() < val) { maxElements.pollLast(); // 如果当前值比最大值大,则移除最大值 } maxElements.offerLast(val); // 将当前值加入最大元素队列 queue.offerLast(val); // 将当前值加入原始队列 } public void pop() { // 如果要移除的元素是最大元素,也要从最大元素队列中移除 if (queue.peekFirst().equals(maxElements.peekFirst())) { maxElements.pollFirst(); } queue.pollFirst(); // 移除原始队列中的第一个元素 } public int getMax() { return maxElements.peekFirst(); // 返回最大元素队列中的第一个元素 } public static void main(String[] args) { MonotonicQueue queue = new MonotonicQueue(); queue.push(3); queue.push(5); queue.push(2); System.out.println(queue.getMax()); // 输出 5 queue.pop(); System.out.println(queue.getMax()); // 输出 5 queue.pop(); System.out.println(queue.getMax()); // 输出 2 } } ``` 在上述代码中,我们使用`ArrayDeque`作为队列的实现,分别维护了`queue`和`maxElements`两个队列。`push`方法用于往队列中添加元素,其中使用了一个`while`循环来将队列中所有小于当前值的元素移除,保持队列的单调性。`pop`方法用于移除队列中的元素,如果要移除的元素是最大元素,则需要同时从最大元素队列中移除。`getMax`方法用于返回当前队列中的最大值。在`main`方法中,我们展示了单调队列的基本使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值