Hàng đợi (Queue)
本页面介绍和队列有关的数据结构及其应用.
引入
队列(queue)是一种具有「先进入队列的元素一定先出队列」性质的表.由于该性质,队列通常也被称为先进先出(first in first out)表,简称 FIFO 表.
实现
数组模拟队列
通常用一个数组模拟一个队列,用两个变量标记队列的首尾.
队列操作对应的代码如下:
插入元素:q[++qr] = x;
删除元素:ql++;
访问队首:q[ql]
访问队尾:q[qr]
清空队列:ql = 1; qr = 0;
Luogu B3616【模板】队列 数组模拟参考实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 #include <cstdio>
using namespace std ;
const int SIZE = 10000 + 5 ;
struct Queue {
int q [ SIZE ], ql , qr ;
Queue () : ql ( 1 ), qr ( 0 ) {}
bool empty () { return ql > qr ; }
void push ( int x ) { q [ ++ qr ] = x ; }
void pop () { ++ ql ; }
int front () { return q [ ql ]; }
int back () { return q [ qr ]; }
int size () { return qr - ql + 1 ; }
int clear () {
ql = 1 ;
qr = 0 ;
}
};
int main () {
Queue q ;
int n ;
scanf ( "%d" , & n );
while ( n -- ) {
int opt ;
scanf ( "%d" , & opt );
if ( opt == 1 ) {
int x ;
scanf ( "%d" , & x );
q . push ( x );
} else if ( opt == 2 ) {
if ( q . empty ())
printf ( "ERR_CANNOT_POP \n " );
else
q . pop ();
} else if ( opt == 3 ) {
if ( q . empty ())
printf ( "ERR_CANNOT_QUERY \n " );
else
printf ( "%d \n " , q . front ());
} else
printf ( "%d \n " , q . size ());
}
return 0 ;
}
双栈模拟队列
还有一种冷门的方法是使用两个 栈 来模拟一个队列.
这种方法使用两个栈 \(F\) 和 \(S\) 模拟一个队列,其中 \(F\) 是队尾的栈,\(S\) 代表队首的栈,支持 push(在队尾插入),pop(在队首弹出)操作:
push:插入到栈 \(F\) 中.
pop:如果 \(S\) 非空,让 \(S\) 弹栈;否则把 \(F\) 的元素倒过来压到 \(S\) 中(其实就是一个一个弹出插入,做完后是首尾颠倒的),然后再让 \(S\) 弹栈.
容易证明,每个元素只会进入/转移/弹出一次,均摊复杂度 \(O(1)\) .
Luogu B3616【模板】队列 双栈模拟参考实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 #include <cstdio>
#include <stack>
using namespace std ;
struct Queue {
stack < int > f , s ;
bool empty () { return f . empty () && s . empty (); }
void push ( int x ) { f . push ( x ); }
void pop () {
if ( s . empty ())
for (; ! f . empty (); f . pop ()) s . push ( f . top ());
s . pop ();
}
int front () {
if ( s . empty ())
for (; ! f . empty (); f . pop ()) s . push ( f . top ());
return s . top ();
}
int size () { return f . size () + s . size (); }
};
int main () {
Queue q ;
int n ;
scanf ( "%d" , & n );
while ( n -- ) {
int opt ;
scanf ( "%d" , & opt );
if ( opt == 1 ) {
int x ;
scanf ( "%d" , & x );
q . push ( x );
} else if ( opt == 2 ) {
if ( q . empty ())
printf ( "ERR_CANNOT_POP \n " );
else
q . pop ();
} else if ( opt == 3 ) {
if ( q . empty ())
printf ( "ERR_CANNOT_QUERY \n " );
else
printf ( "%d \n " , q . front ());
} else
printf ( "%d \n " , q . size ());
}
return 0 ;
}
C++ STL 中的队列
C++ 在 STL 中提供了一个容器 std::queue,使用前需要先引入 <queue> 头文件.
STL 中对 queue 的定义
// clang-format off
template <
class T ,
class Container = std :: deque < T >
> class queue ;
T 为 queue 中要存储的数据类型.
Container 为用于存储元素的底层容器类型.这个容器必须提供通常语义的下列函数:
back()
front()
push_back()
pop_front()
STL 容器 std::deque 和 std::list 满足这些要求.如果不指定,则默认使用 std::deque 作为底层容器.
STL 中的 queue 容器提供了一众成员函数以供调用.其中较为常用的有:
元素访问
q.front() 返回队首元素
q.back() 返回队尾元素
修改
q.push() 在队尾插入元素
q.pop() 弹出队首元素
容量
q.empty() 队列是否为空
q.size() 返回队列中元素的数量
此外,queue 还提供了一些运算符.较为常用的是使用赋值运算符 = 为 queue 赋值,示例:
std :: queue < int > q1 , q2 ;
// 向 q1 的队尾插入 1
q1 . push ( 1 );
// 将 q1 赋值给 q2
q2 = q1 ;
// 输出 q2 的队首元素
std :: cout << q2 . front () << std :: endl ;
// 输出: 1
特殊队列
双端队列
双端队列是指一个可以在队首/队尾插入或删除元素的队列.相当于是栈与队列功能的结合.具体地,双端队列支持的操作有 4 个:
在队首插入一个元素
在队尾插入一个元素
在队首删除一个元素
在队尾删除一个元素
数组模拟双端队列的方式与普通队列相同.
同样地,也可以使用双栈模拟队列的思想来维护双端队列,但需注意当某个栈为空时,交替查询队首和队尾将导致均摊分析失效.考虑在移动时,只将非空栈的一半元素移动到空栈中,并保持队首与队尾栈的性质,这样处理后仍可以做到均摊常数时间的插入和删除.
简要证明
由于插入操作只贡献常数复杂度,现在考虑弹出操作.假设初始时队列中有 \(m\) 个元素,下面我们计算将所有元素全部弹出(无论首尾)的时间复杂度.则第一次平衡的复杂度是 \(O(m)\) 的.然后两个栈就各有 \(\frac{m}{2}\) 个元素.这时就需要 \(O(\frac{m}{2})\) 的时间清空其中一个栈,然后就又可以触发一次复杂度为 \(O(\frac{m}{2})\) 的平衡操作,以此类推,直到所有元素被弹出.因此,这样做的总复杂度是
\[
T(m)=T\left(\frac{m}{2}\right)+O(m)
\]
根据主定理,解得 \(T(m)=O(m)\) .于是,这种维护方式的总复杂度仍是均摊常数的.
Luogu B3656【模板】双端队列 1 参考实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 #include <iostream>
#include <stack>
#include <vector>
using namespace std ;
const int M = 1000000 + 5 ;
struct Deque {
// 将 stack 的底层容器从 deque 换为 vector 以减少空间常数
stack < int , vector < int >> f , s ;
bool empty () { return f . empty () && s . empty (); }
void push_back ( int x ) { f . push ( x ); }
void push_front ( int x ) { s . push ( x ); }
void balance () {
// 平衡中需要辅助栈实现栈内元素倒置
stack < int , vector < int >> t ;
if ( s . empty ()) {
int n = f . size () / 2 ;
for (; f . size () > n ; f . pop ()) t . push ( f . top ());
for (; ! t . empty (); t . pop ()) s . push ( t . top ());
for (; ! f . empty (); f . pop ()) t . push ( f . top ());
f . swap ( t );
if ( ! f . empty ()) s . swap ( f );
} else if ( f . empty ()) {
int n = s . size () / 2 ;
for (; s . size () > n ; s . pop ()) t . push ( s . top ());
for (; ! t . empty (); t . pop ()) f . push ( t . top ());
for (; ! s . empty (); s . pop ()) t . push ( s . top ());
s . swap ( t );
if ( ! s . empty ()) f . swap ( s );
}
}
void pop_front () {
if ( s . empty ()) balance ();
s . pop ();
}
void pop_back () {
if ( f . empty ()) balance ();
f . pop ();
}
int front () {
if ( s . empty ()) balance ();
return s . top ();
}
int back () {
if ( f . empty ()) balance ();
return f . top ();
}
int size () { return f . size () + s . size (); }
};
int main () {
ios :: sync_with_stdio ( false );
cin . tie ( nullptr );
vector < Deque > q ( M );
int n ;
cin >> n ;
while ( n -- ) {
string opt ;
int a ;
cin >> opt >> a ;
if ( opt == "push_back" ) {
int x ;
cin >> x ;
q [ a ]. push_back ( x );
} else if ( opt == "pop_back" ) {
if ( ! q [ a ]. empty ()) q [ a ]. pop_back ();
} else if ( opt == "push_front" ) {
int x ;
cin >> x ;
q [ a ]. push_front ( x );
} else if ( opt == "pop_front" ) {
if ( ! q [ a ]. empty ()) q [ a ]. pop_front ();
} else if ( opt == "size" )
cout << q [ a ]. size () << " \n " ;
else if ( opt == "front" ) {
if ( ! q [ a ]. empty ()) cout << q [ a ]. front () << " \n " ;
} else {
if ( ! q [ a ]. empty ()) cout << q [ a ]. back () << " \n " ;
}
}
return 0 ;
}
C++ STL 中的双端队列
C++ 在 STL 中也提供了一个容器 std::deque,使用前需要先引入 <deque> 头文件.
STL 中对 deque 的定义
// clang-format off
template <
class T ,
class Allocator = std :: allocator < T >
> class deque ;
T 为 deque 中要存储的数据类型.
Allocator 为分配器,此处不做过多说明,一般保持默认即可.
STL 中的 deque 容器提供了一众成员函数以供调用.其中较为常用的有:
元素访问
q.front() 返回队首元素
q.back() 返回队尾元素
修改
q.push_back() 在队尾插入元素
q.pop_back() 弹出队尾元素
q.push_front() 在队首插入元素
q.pop_front() 弹出队首元素
q.insert() 在指定位置前插入元素(传入迭代器和元素)
q.erase() 删除指定位置的元素(传入迭代器)
容量
q.empty() 队列是否为空
q.size() 返回队列中元素的数量
此外,deque 还提供了一些运算符.其中较为常用的有:
使用赋值运算符 = 为 deque 赋值,类似 queue.
使用 [] 访问元素,类似 vector.
<queue> 头文件中还提供了优先队列 std::priority_queue,因其与 堆 更为相似,在此不作过多介绍.
Python 中的双端队列
在 Python 中,双端队列的容器由 collections.deque 提供.
示例如下:
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14 from collections import deque
# 新建一个 deque,并初始化内容为 [1, 2, 3]
queue = deque ([ 1 , 2 , 3 ])
# 在队尾插入元素 4
queue . append ( 4 )
# 在队首插入元素 0
queue . appendleft ( 0 )
# 访问队列
# >>> queue
# deque([0, 1, 2, 3, 4])
循环队列
使用数组模拟队列会导致一个问题:随着时间的推移,整个队列会向数组的尾部移动,一旦到达数组的最末端,即使数组的前端还有空闲位置,再进行入队操作也会导致溢出(这种数组里实际有空闲位置而发生了上溢的现象被称为「假溢出」).
解决假溢出的办法是采用循环的方式来组织存放队列元素的数组,即将数组下标为 0 的位置看做是最后一个位置的后继.(数组下标为 x 的元素,它的后继为 (x + 1) % SIZE).这样就形成了循环队列.
参考资料
std::queue - zh.cppreference.com
std::deque - zh.cppreference.com
Last updated on this page: , Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:OI-wiki
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply