DAY9:栈和队列(一):栈和队列基础

1.栈基础部分

四个关于栈的问题

  1. C++中stack 是容器么?

    stack 在 C++ 中被视为一种容器适配器它不是 STL 中的基础容器类型(如 vector, list, deque, set, map等),但是它在现有的容器类型之上提供了特定的功能(即,后进先出,LIFO)。所以 stack 是一种特殊的容器,被设计为仅支持限定的一组特定操作。

  2. 我们使用的stack是属于哪个版本的STL?

    我们使用的 stack 是属于 C++ 标准模板库(STL)的一部分,STL 是自 C++98 开始的标准 C++ 的一部分。但是,stack 容器适配器在几个版本的 C++ 标准(C++03、C++11、C++14、C++17、C++20)中均存在,并且基本保持不变。你使用的特定版本取决于你的编译器设置和所使用的 C++ 标准版本。

  3. 我们使用的STL中stack是如何实现的?

    stack 是通过其他基础容器实现的。在 STL 中,stack 默认使用 deque 作为其底层容器,但你也可以使用其他容器(如 vectorlist)作为 stack 的底层容器。这是通过模板参数来实现的。例如,你可以声明一个使用 list 作为底层容器的 stack,如下所示:

    std::stack<int, std::list<int>> s;
    

    但是请注意,不是所有的容器都可以用作 stack 的底层容器只有支持 back(), push_back()pop_back() 这些操作的容器才能被用作 stack 的底层容器。

  4. stack 提供迭代器来遍历stack空间么?

    stack 并不提供遍历其元素的迭代器。这是因为 stack 被设计为只提供后进先出 (LIFO) 的访问模式,意味着你只能访问 stack 的顶部元素。这符合 stack 的设计原则,因为 stack 在现实世界中常常被用来表示只允许在一端插入和删除的数据结构(比如一摞盘子)。如果你需要遍历 stack 的所有元素,你可能需要考虑使用其他的容器类型,如 dequevector

栈的定义

参考:栈 - OI Wiki (oi-wiki.org)

栈是 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)的原则进行操作的。在你给出的操作中:

  1. push(1):元素1被推入栈
  2. pop():元素1被弹出栈(不需要在括号中标明元素,因为栈只能移除顶部元素)
  3. push(2):元素2被推入栈
  4. 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::vectorstd::dequestd::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::dequestd::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)。这样就形成了循环队列。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值