文章目录
1.栈基础部分
四个关于栈的问题
-
C++中stack 是容器么?
stack
在 C++ 中被视为一种容器适配器,它不是 STL 中的基础容器类型(如vector
,list
,deque
,set
,map
等),但是它在现有的容器类型之上提供了特定的功能(即,后进先出,LIFO)。所以stack
是一种特殊的容器,被设计为仅支持限定的一组特定操作。 -
我们使用的stack是属于哪个版本的STL?
我们使用的
stack
是属于 C++ 标准模板库(STL)的一部分,STL 是自 C++98 开始的标准 C++ 的一部分。但是,stack
容器适配器在几个版本的 C++ 标准(C++03、C++11、C++14、C++17、C++20)中均存在,并且基本保持不变。你使用的特定版本取决于你的编译器设置和所使用的 C++ 标准版本。 -
我们使用的STL中stack是如何实现的?
stack
是通过其他基础容器实现的。在 STL 中,stack
默认使用deque
作为其底层容器,但你也可以使用其他容器(如vector
或list
)作为stack
的底层容器。这是通过模板参数来实现的。例如,你可以声明一个使用list
作为底层容器的stack
,如下所示:std::stack<int, std::list<int>> s;
但是请注意,不是所有的容器都可以用作
stack
的底层容器,只有支持back()
,push_back()
和pop_back()
这些操作的容器才能被用作stack
的底层容器。 -
stack 提供迭代器来遍历stack空间么?
stack
并不提供遍历其元素的迭代器。这是因为stack
被设计为只提供后进先出 (LIFO) 的访问模式,意味着你只能访问stack
的顶部元素。这符合stack
的设计原则,因为stack
在现实世界中常常被用来表示只允许在一端插入和删除的数据结构(比如一摞盘子)。如果你需要遍历stack
的所有元素,你可能需要考虑使用其他的容器类型,如deque
或vector
。
栈的定义
栈是 OI 中常用的一种线性数据结构,请注意,本文主要讲的是栈这种数据结构,而非程序运行时的系统栈/栈空间。
栈的修改是按照后进先出的原则进行的,因此栈通常被称为是后进先出(last in first out)表,简称 LIFO 表。
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
那么问题来了,STL 中栈是用什么容器实现的?
从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
warning
LIFO 表达的是 当前在容器 内最后进来的最先出去。
我们考虑这样一个栈:
push(1)
pop(1)
push(2)
pop(2)
这个栈的操作是完全正确的。栈是按照后入先出(LIFO)的原则进行操作的。在你给出的操作中:
- push(1):元素1被推入栈
- pop():元素1被弹出栈(不需要在括号中标明元素,因为栈只能移除顶部元素)
- push(2):元素2被推入栈
- pop():元素2被弹出栈
在每个操作后,总是最后进入栈的元素先被移除,这就是LIFO的性质。
然而,这里的理解可能会产生误会。虽然从整体看,1是最先入栈和最先出栈的元素,2是最后入栈和最后出栈的元素,这可能看起来像是先进先出(FIFO),但实际上,我们不能基于整个过程的顺序来判断数据结构的类型。
栈和队列的类型是基于在任意时刻,下一个被移除的元素是哪一个来决定的。在栈中,下一个被移除的元素总是最近添加的元素(LIFO);在队列中,下一个被移除的元素总是最早添加的元素(FIFO)。所以,本例子中的数据结构依然是一个栈,而非队列。
所以,在考虑数据结构是 LIFO 还是 FIFO 的时候,应当考虑在当前容器内的情况。
使用数组模拟栈
我们可以方便的使用数组来模拟一个栈,如下:
int st[N];
// 这里使用 st[0] (即 *st) 代表栈中元素数量,同时也是栈顶下标
// 压栈 :
st[++*st] = var1;
// 取栈顶 :
int u = st[*st];
// 弹栈 :注意越界问题, *st == 0 时不能继续弹出
if (*st) --*st;
// 清空栈
*st = 0;
cppSTL
中的栈
// clang-format off
template<
class T,
class Container = std::deque<T>
> class stack;
T
为 stack 中要存储的数据类型。
Container
为用于存储元素的底层容器类型。这个容器必须提供通常语义的下列函数:
back()
push_back()
pop_back()
STL
容器 std::vector
、std::deque
和 std::list
满足这些要求。如果不指定,则默认使用 std::deque
作为底层容器。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
stack成员函数
STL 中的 stack
容器提供了一众成员函数以供调用,其中较为常用的有:
- 元素访问
st.top()
返回栈顶
- 修改
st.push()
插入传入的参数到栈顶st.pop()
弹出栈顶
- 容量
st.empty()
返回是否为空st.size()
返回元素数量
stack赋值运算
此外,std::stack
还提供了一些运算符。较为常用的是使用赋值运算符 =
为 stack
赋值,示例:
// 新建两个栈 st1 和 st2
std::stack<int> st1, st2;
// 为 st1 装入 1
st1.push(1);
// 将 st1 赋值给 st2
st2 = st1;
// 输出 st2 的栈顶元素
cout << st2.top() << endl;
// 输出: 1
使用python中的list模拟栈
st = [5, 1, 4]
# 使用 append() 向栈顶添加元素
st.append(2)
st.append(3)
# >>> st
# [5, 1, 4, 2, 3]
# 使用 pop 取出栈顶元素
st.pop()
# >>> st
# [5, 1, 4, 2]
# 使用 clear 清空栈
st.clear()
2.队列基础部分
刚刚讲过栈的特性,对应的队列的情况是一样的。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
队列定义
队列(queue)是一种具有「先进入队列的元素一定先出队列」性质的表。由于该性质,队列通常也被称为先进先出(first in first out)表,简称 FIFO 表。
数组模拟队列
通常用一个数组模拟一个队列,用两个变量标记队列的首尾。
int q[SIZE], ql = 1, qr;
队列操作对应的代码如下:
- 插入元素:
q[++qr] = x;
- 删除元素:
ql++;
- 访问队首:
q[ql]
- 访问队尾:
q[qr]
- 清空队列:
ql = 1; qr = 0;
双栈模拟队列
还有一种冷门的方法是使用两个 栈 来模拟一个队列。
这种方法使用两个栈 F, S 模拟一个队列,其中 F 是队尾的栈,S 代表队首的栈,支持 push(在队尾插入),pop(在队首弹出)操作:
- push:插入到栈 F 中。
- pop:如果 S 非空,让 S 弹栈;否则把 F 的元素倒过来压到 S 中(其实就是一个一个弹出插入,做完后是首尾颠倒的),然后再让 S 弹栈。
容易证明,每个元素只会进入/转移/弹出一次,均摊复杂度 O(1)。
cppSTL中的队列
C++ 在 STL 中提供了一个容器 std::queue
,使用前需要先引入 <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
作为底层容器。
quene成员函数
STL 中的 queue
容器提供了一众成员函数以供调用。其中较为常用的有:
-
元素访问
q.front()
返回队首元素q.back()
返回队尾元素
-
修改
q.push()
在队尾插入元素q.pop()
弹出队首元素
-
容量
-
q.empty()
队列是否为空 -
q.size()
返回队列中元素的数量
-
quene赋值运算
std::queue<int> q1, q2;
// 向 q1 的队尾插入 1
q1.push(1);
// 将 q1 赋值给 q2
q2 = q1;
// 输出 q2 的队首元素
std::cout << q2.front() << std::endl;
// 输出: 1
特殊:双端队列deque
双端队列是指一个可以在队首/队尾插入或删除元素的队列。相当于是栈与队列功能的结合。具体地,双端队列支持的操作有 4 个:
- 在队首插入一个元素
- 在队尾插入一个元素
- 在队首删除一个元素
- 在队尾删除一个元素
数组模拟双端队列的方式与普通队列相同。
stl中的双端队列
C++ 在 STL 中也提供了一个容器 std::deque
,使用前需要先引入 <deque>
头文件。
STL中对deque的定义:
// clang-format off
template<
class T,
class Allocator = std::allocator<T>
> class deque;
T
为 deque 中要存储的数据类型。
Allocator
为分配器,此处不做过多说明,一般保持默认即可。
deque成员函数
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
还提供了一些运算符。其中较为常用的有:
- 使用赋值运算符
=
为deque
赋值,类似queue
。 - 使用
[]
访问元素,类似vector
。
<queue>
头文件中还提供了优先队列 std::priority_queue
,因其与 堆 更为相似,在此不作过多介绍。
python中的双端队列
在 Python 中,双端队列的容器由 collections.deque
提供。
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
)。这样就形成了循环队列。