stack的详细介绍,queue的详细介绍
stack前言
在上一篇文章中,我们首先提出了三个问题:
- C++中stack 是容器么?
- stack 提供迭代器来遍历stack空间么?
- 我们使用的STL中stack是如何实现的?
我们的回答是:
- stack不是容器,而是容器适配器。
- satck不提供迭代器来遍历stack空间。
- c++中,stack的底层实现默认使用deque。
在上一篇文章中有对这三个问题更详细地回答,并且有对deque底层实现的详细介绍。相信了解了deque之后对于学习stack会更有帮助!
deque(双端队列)底层实现和实际运用-CSDN博客
stack 在 deque 的基础上主要做了以下几件事情:
- 限制了功能接口: stack 并没有实现自己的数据结构,而是适配了底层的容器(默认是 deque)。它只对外暴露了栈这种数据结构应该有的基本操作,强制执行了栈的后进先出 (LIFO) 的原则,而隐藏了底层容器的许多其他功能。
- 提供了更清晰的语义: 使用 stack 可以更清晰地表达代码的意图。当你看到代码中使用 stack 时,你会立即知道这里需要的是一个栈这种数据结构,而不是一个更通用的双端队列。这提高了代码的可读性和可维护性。
stack的函数接口
好消息:由于stack功能比较简单,所以stack提供的函数接口很少!!!
一定要注意:对于栈来说,是不向我们提供任何迭代器的,我们也就不能使用迭代器来遍历栈空间。
stack向我们提供的函数接口也比较少,常见的就是下面的几个:
- top(): 返回栈顶元素的引用。
- push(): 将一个新元素添加到栈顶,返回值类型: void。
- pop(): 移除位于栈顶的元素,返回值类型: void。
- empty(): 检查栈是否为空,空的时候返回true。
- size(): 返回栈中当前元素的数量。
栈(stack)是容器适配器,容器适配器(stack/queue)中,添加元素函数名都是push,移除元素都是pop,在vector以及deque中函数名都是push_back,pop_back。
#include<iostream>//c++标准头文件,可以使用cout,cin等标准库函数
#include<stack>//使用stack时需要的头文件
using namespace std;//命名空间,防止重名给程序带来各种隐患,使用cin,cout,stack,map,set,vector,queue时都要使用
int main(){
stack<int> s;//定义一个int类型的stack
s.push(1);//往栈里放入一个元素1
s.push(2);//往栈里放入一个元素2
s.push(3); //往栈里放入一个元素3
cout<<"按顺序放入元素1、2、3后,目前栈里的元素:1 2 3" <<endl;
cout<<"s.size()="<<s.size()<<endl;//s.size()返回栈内元素的个数
cout<<"s.empty()="<<s.empty()<<endl; //判断栈是否为空,值为1代表空,0代表非空,用s.size()同样可以判断 ,s.size()的值为0就代表空的
cout<<"s.top()="<<s.top()<<endl;//查看栈顶的元素
cout<<endl;
s.pop();//弹出栈顶元素
cout<<"s.pop()后,目前栈里的元素:1 2"<<endl;
cout<<"s.size()="<<s.size()<<endl;
cout<<"s.empty()="<<s.empty()<<endl;
cout<<"s.top()="<<s.top()<<endl;
cout<<endl;
s.pop();
cout<<"s.pop()后,目前栈里的元素:1"<<endl;
cout<<"s.size()="<<s.size()<<endl;
cout<<"s.empty()="<<s.empty()<<endl;
cout<<"s.top()="<<s.top()<<endl;
cout<<endl;
s.pop();
cout<<"s.pop()后,目前的栈是空的"<<endl;
cout<<"s.size()="<<s.size()<<endl;
cout<<"栈是空的就不能用s.top()访问栈顶元素了" <<endl;
cout<<"s.empty()="<<s.empty()<<endl;
}
运行结果:
按顺序放入元素1、2、3后,目前栈里的元素:1 2 3
s.size()=3
s.empty()=0
s.top()=3
s.pop()后,目前栈里的元素:1 2
s.size()=2
s.empty()=0
s.top()=2
s.pop()后,目前栈里的元素:1
s.size()=1
s.empty()=0
s.top()=1
s.pop()后,目前的栈是空的
s.size()=0
栈是空的就不能用s.top()访问栈顶元素了
s.empty()=1
stack实际运用
栈因其简单而强大的后进先出(LIFO) 特性,在计算机科学中扮演着重要的角色。无论是解决算法难题,还是作为构建更复杂数据结构的基础,栈都是一个非常有用的工具。下面介绍栈的常见的几种使用情景:
表达式求值 (Expression Evaluation):
- 中缀表达式转后缀表达式 (Infix to Postfix Conversion): 栈可以用来存储操作符,并根据运算符优先级和括号来生成后缀表达式(也称为逆波兰表示法)。
- 后缀表达式求值 (Postfix Evaluation): 栈可以用来存储操作数。当遇到操作符时,从栈中弹出所需数量的操作数进行计算,并将结果压回栈中。
括号匹配 (Parentheses Matching):
- 判断一个字符串中的括号(例如
()
,[]
,{}
) 是否正确匹配。遍历字符串,遇到左括号则压入栈中,遇到右括号则检查栈顶是否是对应的左括号。如果匹配则弹出栈顶元素,否则或栈为空时遇到右括号则表示不匹配。最后栈为空则表示所有括号都匹配。
深度优先搜索 (Depth-First Search, DFS):
- 在图或树的遍历中,DFS 通常使用栈(可以是显式的
std::stack
,也可以是递归调用的隐式栈)来跟踪访问过的节点和待访问的邻居节点。比如对于二叉树的遍历中就是可以使用栈来实现二叉树的前序,中序,后序遍历。
回溯算法 (Backtracking):
- 许多回溯算法(例如解决迷宫问题、N 皇后问题、子集生成等)都使用栈来保存当前的状态。当探索到一条死路时,可以从栈中弹出最近的状态进行回溯,尝试其他路径。
实现递归 (Implicitly):
- 递归函数的执行本质上依赖于系统维护的函数调用栈。理解栈的工作方式有助于理解递归的原理。虽然你通常不会显式地用
std::stack
来“实现”递归,但你可以使用栈来将某些递归算法转换为迭代算法,以避免递归深度过大的问题。
稍深入探讨一下使用栈来实现递归
理论上来说,所有用递归能解决的问题都可以用栈(通常是通过迭代的方式模拟)来解决。
这是因为递归的本质就是通过系统维护的调用栈 (Call Stack) 来实现的。每次进行递归调用时,当前函数的局部变量、参数、返回地址等信息会被压入调用栈中。当递归调用返回时,这些信息会从栈中弹出,程序会回到上一次函数调用的位置继续执行。
实际上来说,递归方式的函数栈可以自动帮助我们来保存信息,保存的信息很有条理。
我们自己想用迭代+栈来实现的话,要考虑的问题就比较多,需要考虑
- 何时入栈出栈,
- 多次入栈出栈,
- 还要考率栈为空时对应的逻辑,
- 有的时候需要多个栈来实现简单递归就能实现的问题,
- 不容易实现。
虽然用迭代加栈来理解递归的本质很有帮助,但在实际开发中,我们通常会优先选择更自然、更易于理解和维护的实现方式。对于那些容易产生栈溢出的递归,或者对性能有极致要求的场景,才会考虑使用迭代加栈的方式进行优化或替代。
对于栈的一个问题解答
我们在了解完栈之后,可以显著发现栈的所有功能其实很简单,成员函数也不多,deque,vector的功能其实完全可以覆盖栈的功能,那为什么我们很多时候还是会选择使用栈呢?
从功能上讲,deque
和 vector
的确可以用来模拟栈的行为。你可以只使用它们的尾部进行添加和删除操作,从而实现栈的 push
和 pop
功能。
然而,我们很多时候仍然选择使用 std::stack
,这主要是出于以下几个重要的原因:
代码的意图和可读性 (Intent and Readability):
std::stack
这个名字本身就清晰地表明了代码的意图:这里需要一个后进先出 (LIFO) 的数据结构。当其他开发者(或者未来的你)阅读这段代码时,一眼就能明白这里使用的是栈的语义。- 如果你直接使用
deque
或vector
并只操作尾部,虽然功能上实现了栈,但代码的意图并不那么明确。可能会让人疑惑为什么选择deque
或vector
,是否还有其他操作会用到它们的其他功能。使用std::stack
则消除了这种歧义。
制执行栈的语义 (Enforcing Stack Semantics):
std::stack
的接口被有意地限制为只包含栈的基本操作 (push
,pop
,top
,empty
,size
,emplace
,swap
). 这种限制避免了开发者在不经意间使用了底层容器的其他功能(比如deque
的push_front
,pop_front
, 随机访问等,或者vector
的随机访问),从而破坏了栈的 LIFO 原则。
综合来看:使用 stack 更多的是出于代码清晰性、语义明确性、强制执行栈原则以及提高代码可维护性的考虑。它是一种更符合逻辑和更安全的选择,能够更好地表达程序的意图。
queue前言
在了解完 stack 之后,我们现在来看一下 queue。首先提出三个类似的问题:
- C++ 中 queue 是容器么?
- queue 提供迭代器来遍历 queue 空间么?
- 我们使用的 STL 中 queue 是如何实现的?
我们的回答是:
- queue 不是容器,而是容器适配器。
- queue 不提供迭代器来遍历 queue 空间。
- C++ 中,queue 的底层实现默认使用 deque。
在上一篇文章中有对这三个问题更详细地回答,并且有对deque底层实现的详细介绍。相信了解了deque之后对于学习queue会更有帮助!
deque(双端队列)底层实现和实际运用-CSDN博客
queue 在 deque 的基础上主要做了以下几件事情:
- 限制了功能接口: queue 并没有实现自己的数据结构,而是适配了底层的容器(默认是 deque)。它只对外暴露了队列这种数据结构应该有的基本操作,强制执行了队列的先进先出 (FIFO) 的原则,而隐藏了底层容器的许多其他功能。
- 提供了更清晰的语义: 使用 queue 可以更清晰地表达代码的意图。当你看到代码中使用 queue 时,你会立即知道这里需要的是一个队列这种数据结构,而不是一个更通用的双端队列。这提高了代码的可读性和可维护性。
queue的函数接口
和stack(栈)一样,queue也是一个容器适配器,所以queue也是不向我们提供任何迭代器的,我们也就不能使用迭代器来遍历队列空间。
queue向我们提供的函数接口也比较少,常见的就是下面的几个:
- front(): 返回队列头部元素的引用。
- back(): 返回队列尾部元素的引用。
- push(): 将一个新元素添加到队列的尾部,返回值类型: void。
- pop(): 移除位于队列头部的元素,返回值类型: void。
- empty(): 检查队列是否为空,空的时候返回true。
- size(): 返回队列中当前元素的数量。
队列(queue)是容器适配器,容器适配器(stack/queue)中,添加元素函数名都是push,移除元素都是pop,在vector以及deque中函数名都是push_back,pop_back。
queue实际运用
queue 在实际算法编程中有着广泛的应用,其核心特性是先进先出 (FIFO),这使得它非常适合处理需要按顺序处理元素的场景。以下是一些常见的运用:
1. 广度优先搜索 (Breadth-First Search, BFS):(最常见也是最重要的应用)
- 典型场景: 在无权图中查找两个节点之间的最短路径。
- 原理: BFS 从起始节点开始,首先访问其所有直接邻居,然后访问这些邻居的邻居,依此类推,逐层扩展。
std::queue
非常适合用来存储待访问的节点,保证了按层级顺序进行探索。
2. 树的层序遍历 (Level Order Traversal):
- 典型场景: 按照树的层级顺序访问所有节点。
- 原理: 将根节点入队,然后当队列不为空时,取出队首节点并访问它,接着将其所有子节点按从左到右的顺序入队。重复这个过程,直到队列为空。
3. 任务调度 (Task Scheduling):
- 典型场景: 模拟任务按照到达顺序被处理的情况。
- 原理: 将待处理的任务放入队列中,然后按照入队顺序依次取出任务进行处理。这保证了先到达的任务先被执行。
4. 消息队列 (Message Queues):
- 典型场景: 在不同的程序或线程之间传递消息。
- 原理: 发送者将消息放入队列,接收者从队列中取出消息进行处理。队列保证了消息的顺序性和可靠性。虽然实际的消息队列系统可能更复杂,但
std::queue
提供了一个基本的模型。
对比一下stack和queue
函数接口方面:
queue中的函数接口和stack中的差不多,stack常用的函数接口有5个,queue常用的函数接口有6个。
- stack中返回栈顶元素是top();
- queue中返回队列头部元素的引用是front(),返回队列尾部元素的引用时back()。
- stack将一个新元素添加到栈顶函数接口为push(),将一个元素从栈顶移除的接口为pop();
- queue将一个新元素添加到队列尾部函数接口为push(),将一个元素从队列头部移除的接口为pop()。
对于empty() 和 size()这两个函数接口来说,stack和queue使用方法一样。
容器适配器功能方面:
这两个容器适配器默认的底层实现使用的都是deque(双端队列)。
STL标准库对于stack和queue来说,都没有提供迭代器来对其进行访问。
stack是对同一个口进行push和pop操作,queue是对两个口中的尾部进行push操作,对头部进行pop操作。