浅谈单调队列:死海不是海,单调队列不是队列

1.滑动窗口最值问题

给定一个长度为n的序列a1,a2,…ai,…,an,将一个长为k的滑动窗口自序列最左端向右边滑动。例如:初始时,窗口内的子序列为a1,a2,…,ak;当窗口向右滑动一位,此时窗口内的子序列变为a2,a3,…,ak+1。

我们要解决的问题是,给定长度为n的序列以及滑动窗口的大小k,求每一个滑动窗口内的最小值和最大值。

以长度为5的序列1, 3, 4, 5, 7滑动窗口k=3为例说明:

第1个滑动窗口(1, 3, 4)的最小值、最大值分别为1和4;

第2个滑动窗口(3, 4, 5)的最小值、最大值分别为3和5;

第3个滑动窗口(4, 5, 7)的最小值、最大值分别为4和7。

2.一些可行的解决思路

最直接的思路,可以枚举所有窗口(一共n-k+1个),扫描窗口内的每一个元素求其最值。
整个算法的时间复杂度是O(n*k),当数据规模较大的时候(如n=10^6,k=10^4),该算法耗时较长。

另一个容易想到的思路,将序列建线段树(或者树状数组等等),该过程的时间复杂度是O(n*logn),再通过n-k+1次查询区间最值求每个窗口对应区间的最大(小)值。整体时间复杂度是O(n*logn)。

3.单调队列:更优美的思路

一种更加优美的解决方法是单调队列。那么,我们就来一起揭开「单调队列」的神秘面纱吧。

首先,第一个问题来了:单调队列是队列——吗?
字面上去理解的话,单调队列肯定是队列,没毛病。不然为啥不叫单调栈呢。
但是,初中地理老师有言在先:死海不是海,是湖泊,还是世界上最低的湖泊。为毛不起个「死湖」的名字?!这个……就自行google/baidu吧。
扯远了,扯回来。
单调队列,从严格意义上讲还真不是「队列」
什么是队列呢?就是一中FIFO(First In First Out)的数据结构。所有要入队的元素,统一从队尾入队,再从队首出队。
但是,「单调队列」却不是一种FIFO的数据结构。在单调队列中,为了维护队列内元素的「单调」性,所有要入队的元素,统一从队尾入队,再从对首出队,也可以从对尾直接出队

4.单调队列的基本操作

听起来有点玄乎,先来看看单调队列(以递增队列为例)有哪些基本的操作。

1.入队(push_back):对于待入队的元素,为维护队列的递增性,如果队尾元素值大于待入队元素,则将对尾元素从队列中弹出,重复此操作,直到队列为空或者队尾元素小于待入队元素。然后,再把待入队元素添加到队列末尾。

2.出队(pop):分被动的出队(为维护队列单调性,将元素从队尾弹出)和主动的出队(和传统的队列一样,从队首出;但是有讲究,正是这个讲究让滑动窗口最值问题得以解决)。

5.利用单调队列求解滑动窗口最值问题

下面,一起来看看如何利用单调(递增)队列来解决滑动窗口的最(小)值问题。

以长度为6的序列1, 3, 4, 5, 7, 2和滑动窗口k=3为例:

1)1入队,入队后队列变为[1];

2)3入队,3大于队尾元素1,入队后队列变为[1, 3];

3)4入队,4大于队尾元素3,入队后队列变为[1, 3, 4];

从4开始,已经形成了第1个滑动窗口,窗口内最小值就是队首元素1。

4)5入队,5大于队尾元素4,入队后队列变为[1, 3, 4, 5];

这时,队内有4个元素,求第2个滑动窗口内最小值的策略是:

取出队首元素,如果该元素不在滑动窗口内,则将其从队列中弹出,继续取新的对首元素,直到队首元素出现在窗口内;此时,队首元素即为窗口最小值。

这也就是出队操作的「讲究」之处。

在求得第2个滑动窗口的最小值后,1由于不在滑动窗口内被弹出,队列变为[3, 4, 5];

5)7入队,7大于队尾元素5,入队后队列变为[3, 4, 5, 7];

求得第3个滑动窗口的最小值,3由于不在窗口内出队,4在窗口内,所以4为第3个窗口的最小值。队列变为[4, 5, 7]。

6)2入队,为维护队列的单调性,依次弹出7, 5, 4,完成入队后,队列变为[2]。

求得第4个滑动窗口最小值为2,队列保持不变,依然为[2]。

6.时间复杂度

理解单调队列的核心之一在于,所有被动的出队(在队尾被弹出)的元素,都不可能是当前所求窗口的最值。

由于序列中的每个元素只可能入队1次,最多也可能出队1次,所以均摊下来,用单调队列求滑动窗口内最小值的算法时间复杂度是O(n)。

类似地,也可以利用单调递减队列来求得滑动窗口内的最大值问题。

单调队列的一个更加实用的用途,就是利用其滑动窗口最值优化动态规划问题的时间复杂度。

另外,关于这个问题,你可以在这里小试牛刀。

7.c++源码实现

 1 #include <iostream>
 2 #include <vector>
 3 #include <deque>
 4 #include <cstdio>
 5
 6 #define MAXN 10010
 7
 8 class Data {
 9 public:
10     int val;
11     int idx;
12     Data() { val = idx =  0; }
13     Data(int x, int y):val(x), idx(y){}
14 };
15
16 class OrderedQueue {
17 private:
18     Data que[MAXN]; //在部分机器上(如POJ的环境上,MAXN为10^6时,会出现Runtime Error,一种可行的方法是将其设置为全局变量(由于封装差,因此不提供这个版本的代码).
19     int front;
20     int back;
21     int window_size;
22     // true -> increasing(not strictly)
23     // false -> decreasing(not strictly)
24     bool order;
25 public:
26     OrderedQueue();
27     OrderedQueue(int window_size, bool order);
28     void push_back(Data d);
29     Data get_window_front(int pos);
30     void clear();
31     bool empty();
32 };
33
34 OrderedQueue::OrderedQueue() {
35     window_size = 3;
36     order = true;
37     clear();
38 }
39
40 OrderedQueue::OrderedQueue(int window_size, bool order) {
41     this->window_size = window_size;
42     this->order = order;
43     clear();
44 }
45
46 void OrderedQueue::clear() {
47     front = back = 0;
48 }
49
50 bool OrderedQueue::empty() {
51     return front == back;
52 }
53
54 void OrderedQueue::push_back(Data d) {
55     while (front < back) {
56         Data tail = que[back - 1];
57         bool tag = order ? d.val > tail.val : d.val < tail.val;
58         if (tag) {
59             break;
60         } else {
61             back--;
62         }
63     }
64     que[back++] = d;
65 }
66
67 Data OrderedQueue::get_window_front(int pos) {
68     while (front < back && que[front].idx < pos - window_size + 1) {
69         front++;
70     }
71     return que[front];
72 }
73
74
75 int main() {
76     int a[8] = {1, 3, -1, -3, 5, 3, 6, 7};
77     int wsize = 3;
78     OrderedQueue oq1 = OrderedQueue(wsize, true);
79     OrderedQueue oq2 = OrderedQueue(wsize, false);
80     for (int i = 0; i < 8; i++) {
81         oq1.push_back(Data(a[i], i));
82         oq2.push_back(Data(a[i], i));
83         if (i + 1 >= wsize) {
84             std::cout << "在区间[" << (i - wsize + 1) << "," << i << "]内的最小值为" <<  oq1.get_window_front(i).val << std::endl;
85             std::cout << "在区间[" << (i - wsize + 1) << "," << i << "]内的最大值为" <<  oq2.get_window_front(i).val << std::endl;
86         }
87     }
88     return 0;
89 }

8.如果可以……

本文为原创博文,如需转载请注明文章出处以及作者信息。

每一篇博文分享,我都倾尽满心诚意,如有任何问题,请在评论区留言沟通。

最后,如果可以……微信扫一扫下面二维码打赏点咖啡钱(当然,前提是博文对你受用)鼓励一下,我将打起精神努力码字。

「本文 完」

时间: 2024-07-30 10:17:58

浅谈单调队列:死海不是海,单调队列不是队列的相关文章

浅谈Java中的hashcode方法 - 海 子

浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 一.hashCode方法的作用 对于包含容器类型的程

浅谈单调队列、单调栈

       初谈这个话题,相信许多人会有一种似有所悟,但又不敢确定的感觉.没错,这正是因为其中"单调"一词的存在,所谓单调是什么,学过函数的people都知道单调函数或者函数的单调性,直白一点说单调就是一直增或一直减.例如:1,3,5,9就是一个单调增数列,数列中不存在后一个数比前一个数小的现象.那么同样,在这里谈到的话题也有类似特点.        先说一下单调队列吧!      单调队列,就是一个符合单调性质的队列,它同时具有单调的性质以及队列的性质.他在编程中使用频率不高,但却

浅谈算法和数据结构(1):栈和队列

浅谈算法和数据结构(1):栈和队列 2014/11/03 ·  IT技术                                         · 2 评论                                      ·  数据结构, 栈, 算法, 队列 分享到: 60 SegmentFault D-Day 2015 北京:iOS 站 JDBC之“对岸的女孩走过来” CSS深入理解之relative HTML5+CSS3实现春节贺卡 原文出处: 寒江独钓   欢迎分享原创

浅谈算法和数据结构: 五 优先级队列与堆排序

转载自:http://www.cnblogs.com/yangecnu/p/Introduce-Priority-Queue-And-Heap-Sort.html 浅谈算法和数据结构: 五 优先级队列与堆排序 在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象.最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话. 在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是

【转载】浅谈算法和数据结构: 一 栈和队列

作者:yangecnu(yangecnu's Blog on 博客园) 出处:http://www.cnblogs.com/yangecnu/ 最近晚上在家里看Algorithms,4th Edition,我买的英文版,觉得这本书写的比较浅显易懂,而且“图码并茂”,趁着这次机会打算好好学习做做笔记,这样也会印象深刻,这也是写这一系列文章的原因.另外普林斯顿大学在Coursera 上也有这本书同步的公开课,还有另外一门算法分析课,这门课程的作者也是这本书的作者,两门课都挺不错的. 计算机程序离不开

浅谈算法和数据结构: 一 栈和队列

最近晚上在家里看Algorithems,4th Edition,我买的英文版,觉得这本书写的比较浅显易懂,而且"图码并茂",趁着这次机会打算好好学习做做笔记,这样也会印象深刻,这也是写这一系列文章的原因.另外普林斯顿大学在Coursera 上也有这本书同步的公开课,还有另外一门算法分析课,这门课程的作者也是这本书的作者,两门课都挺不错的. 计算机程序离不开算法和数据结构,本文简单介绍栈(Stack)和队列(Queue)的实现,.NET中与之相关的数据结构,典型应用等,希望能加深自己对这

浅谈数据结构之链队列(六)

前面我们讲了队列的顺序存储结构,现在我们来看看队列的链式存储结构.队列的链式存储其实就是线性表的单链表结构,只不过它是尾进头出而已,通常我们把它简称为链队列.为了操作上的方便,我们将队头指针front指向链队列的头结点,而队尾指针rear则指向终端结点.注意:当队列为空时,指针front和rear都指向头结点. 在这里,我们再介绍一下循环队列.循环队列是为了避免数组插入与删除数据时需要移动数据而引入的,我们一般把队列的这种头尾相接的顺序存储结构称为循环队列.对于循环队列和链队列相比较来说,循环队

浅谈数据结构系列 栈和队列

计算机程序离不开算法和数据结构,在数据结构算法应用中,栈和队列应用你比较广泛,因为两者在数据存放和读取方面效率比较高,本章节重点讲解两者的基本概念和实现. 基本概念 栈:是一种先进后出,后进先出的数据结构,本质上是线性表,只是限制仅允许在表的一段进行插入和删除工作.此端为栈顶,这是在栈中应用很关键的概念.所有数据的处理都是在栈顶进行的,进栈时,栈中元素增加,栈顶上移一位,出栈时栈顶下移一位.应用中比如:洗碗,每次洗干净的碗放在上面-进栈,取碗,从顶上取出一个-出栈:装子弹-进栈,开枪-出栈. 队

浅谈栈、队列

所谓栈和队列其本质都是一种存储信息的方法,最主要的差别就是两者的存取方式不同,栈相当于是一个一端开口一端封闭的空心玻璃柱,每存入一个数据就是扔进一个与管口等粗的球,取出数据时只能取最上头的,也就是最后一个放进去的,并且当管中无球时,无法取数据:相对而言,队列相当于是一个两端开口的空心玻璃柱,每存入一个数据,就从后端插入一个与管口等粗的球,取数据时只能从前端取不能从后段取,同样当其中无球时无法取. 从上述介绍中可以发现两者各有优势,但队列有一个显著问题,但插入取出次数多时,可能仅有几个数据但存储却