Skip to main content

第49章 栈、队列与循环队列

栈、队列和循环队列是计算机科学中最基础且应用广泛的线性数据结构,它们通过特定的元素插入和删除规则,实现对数据的有序管理

49.1 栈(Stack)

49.1.1 定义与特性

栈是一种后进先出(Last In First Out,LIFO)原则的线性数据结构,即后插入的元素最先被删除。栈的操作仅在一端进行,这一端被称为栈顶,另一端则被称为栈底。 形象比喻:栈类似于叠放的盘子,只能从最顶端放置或取走盘子,先放的盘子被压在底部,最后放的盘子最先被取走。

49.1.2 基本操作

栈的核心操作包括:

  • 入栈(Push):将元素添加到栈顶,栈的大小增加1。
  • 出栈(Pop):删除栈顶元素,栈的大小减少1(需先判断是否为空)。
  • 取元素(Top/Peek):返回栈顶元素的值,不改变栈的结构(需先判断是否为空)。
  • 判空(IsEmpty):判断栈是否为空,若为空返回true,否则返回false。
  • 获取大小(Size):返回栈中元素的个数。

49.1.3 实现方式

栈可通过数组或链表实现,数组实现更高效,链表实现更灵活。

数组实现

#include <iostream>
#include <cstring>
using namespace std;
const int MAX_SIZE=100;
class Stack {
private:
int data[MAX_SIZE];//存储栈元素的数组
int top;//栈顶指针(指向栈顶元素的下标,-1表示空栈)
public:
//构造函数:初始化空栈
Stack(){
top= -1;
}
//入栈
bool push (int val) {
if (top >= MAX_SIZE - 1){
return false;//栈满,入栈失败
}
data[++top]= val;//栈顶指针先加1,再存入元素
return true;
}
//出栈
bool pop(){
if (isEmpty()){
return false;//栈空,出栈失败
}
top--;//栈顶指针减1(逻辑上移除元素)
return true;
}
//取栈顶元素
int topVal(){
if (isEmpty()){
throw "Stack is empty";
}
return data[top];
}
//判空
bool isEmpty(){
return top == -1;
}
//获取大小
int size(){
return top + 1;
}
};

链表实现

#include <iostream>
using namespace std;
//链表节点结构
struct Node{
int val;
Node* next;
Node (int v):val(v),next (nullptr){}
};
class Stack {
private:
Node* top;//栈顶指针(指向栈顶节点)
int size;
public:
//构造函数:初始化空栈
Stack(){
top = nullptr;
size=0;
}
//析构函数:释放所有节点
~Stack(){
while (top!= nullptr){
Node* temp=top;
top= top->next;
delete temp;
}
}
//入栈
void push (int val){
Node* newNode = new Node (val);
newNode->next= top;//新节点指向原栈顶
top=newNode;//更新栈顶指针
size++;
}
//出栈
bool pop(){
if (isEmpty()){
return false;
}
Node* temp= top;
top=top->next;//栈顶指针后移
delete temp;
size--;
return true;
}
//取栈顶元素
int topVal(){
if (isEmpty()){
throw "Stack is empty";
}
return top->val;
}
//判空
bool isEmpty(){
return top == nullptr;
}
//获取大小
int getSize(){
return size;
}
};

49.1.4 应用场景

  1. 函数调用:程序执行时,函数调用的上下文(返回地址、局部变量等)通过栈存储,函数返回时从栈顶弹出。
  2. 表达式求值:如后缀表达式(逆波兰表达式)的计算,利用栈存储操作数,遇到运算符时弹出操作数计算。
  3. 括号匹配:检查代码中括号是否成对出现,遇到左括号入栈,遇到右括号时与栈顶左括号匹配并出栈。
  4. 撤销操作:文本编辑器中的"撤销"功能,通过记录操作历史,撤销时弹出最近的操作。

49.2 队列(Queue)

49.2.1 定义与特性

队列是一种先进先出(First In First Out,FIFO)原则的线性数据结构,元素的入队在一端(队尾)进行,元素的删除在另一端(队头)进行。 形象比喻:队列类似于排队购票,先排队的人先购票离开,后排队的人依次等待,符合"先来后到"的规则。

49.2.2 基本操作

队列的核心操作包括:

  • 入队(Enqueue):将元素添加到队尾,队列大小增加1。
  • 出队(Dequeue):删除队头元素,队列大小减少1(需先判断队列是否为空)。
  • 取队头元素(Front):返回队头元素的值,不改变队列结构(需先判断队列是否为空)。
  • 判空(IsEmpty):判断队列是否为空,若为空返回true,否则返回false。
  • 获取大小(Size):返回队列中元素的个数。

49.2.3 实现方式

队列可通过数组或链表实现,链表实现更适合动态大小场景,数组实现需注意队头和队尾的指针管理。

链表实现

#include<iostream>
using namespace std;
//链表节点结构
struct Node{
int val;
Node* next;
Node (int v): val(v), next (nullptr) {}
};
class Queue{
private:
Node* front;//队头指针(指向第一个元素)
Node* rear;//队尾指针(指向最后一个元素)
int size;
public:
//构造函数:初始化空队列
Queue(){
front = rear = nullptr;
size=0;
}
//析构函数:释放所有节点
~Queue(){
while (front!= nullptr){
Node* temp= front;
front = front->next;
delete temp;
}
}
//入队
void enqueue(int val){
Node* newNode = new Node (val);
if (isEmpty()){
front= rear=newNode;//空队列时,队头和队尾指向新节点
} else{
rear->next=newNode;//新节点链接到队尾
rear=newNode;//更新队尾指针
}
size++;
}
//出队
bool dequeue(){
if(isEmpty()){
return false;
}
Node* temp = front;
front = front->next;//队头指针后移
if (front == nullptr) {
rear=nullptr;//队列变空时,队尾指针也置空
}
delete temp;
size--;
return true;
}
//取队头元素
int getFront(){
if (isEmpty()){
throw"Queue is empty";
}
return front->val;
}
//判空
bool isEmpty(){
return front == nullptr;
}
//获取大小
int getSize(){
return size;
}
};

数组实现(简单版,存在空间浪费问题)

#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class Queue{
private:
int data[MAX_SIZE];
int front;//队头指针(指向队头元素)
int rear;//队尾指针(指向队尾元素的下一个位置)
int size;
public:
Queue(){
front=0;
rear=0;
size=0;
}
//入队
bool enqueue (int val) {
if (size == MAX_SIZE){
return false;//队列满
}
data[rear]= val;
rear=(rear +1) % MAX_SIZE;//循环移动(为后续循环队列铺垫)
size++;
return true;
}
//出队
bool dequeue(){
if (isEmpty()){
return false;
}
front =(front +1) % MAX_SIZE;
size--;
return true;
}
//取队头元素
int getFront(){
if (isEmpty()){
throw "Queue is empty";
}
return data[front];
}
bool isEmpty(){
return size==0;
}
int getSize(){
return size;
}
};

49.2.4 应用场景

  1. 任务调度:操作系统中的进程调度、打印机任务队列,按请求顺序处理任务。
  2. 广度优先搜索(BFS):遍历图或树时,使用队列存储待访问节点,确保按层次顺序访问。
  3. 缓冲处理:如键盘输入缓冲、网络数据接收缓冲,按到达顺序处理数据。
  4. 消息队列:分布式系统中,不同组件间通过消息队列传递消息,保证消息的有序处理。

49.3 循环队列(Circular Queue)

49.3.1 定义与特性

循环队列是对普通数组队列的优化,通过将数组的首尾相连(逻辑上形成环形),解决普通数组队列因队头移动导致的空间浪费问题。循环队列的队头和队尾指针在达到数组末尾时,会绕回数组的起始位置,从而高效利用存储空间。 核心解决的问题:普通数组队列中,即使队列元素个数小于数组容量,若队尾已到达数组末尾,也无法继续入队(假溢出);循环队列通过环形逻辑,允许队尾绕回数组头部,充分利用空间。

49.3.2 基本操作

循环队列的操作与普通队列一致,核心区别在于队头和队尾指针的移动方式(采用模运算实现循环)。

49.3.3 实现方式

循环队列通常通过数组实现,关键是如何判断队列满和队列空: 判空条件:front==rear且元素个数为0。 判满条件:通常通过预留一个空位置实现,即(rear+1)%MAX_SIZE == front,此时队列中实际可存储MAX_SIZE-1个元素。

#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class CircularQueue {
private:
int data[MAX_SIZE];
int front;//队头指针(指向队头元素)
int rear;//队尾指针(指向队尾元素的下一个位置)
public:
//构造函数:初始化空队列
CircularQueue(){
front=0;
rear=0;
}
//入队
bool enqueue (int val){
if (isFull()){
return false;//队列满
}
data[rear] = val;
rear=(rear + 1) % MAX_SIZE;//队尾指针循环后移
return true;
}
//出队
bool dequeue(){
if (isEmpty()){
return false;//队列空
}
front =(front+1) % MAX_SIZE; //队头指针循环后移
return true;
}
//取队头元素
int getFront(){
if (isEmpty()){
throw "CircularQueue is empty";
}
return data[front];
}
//判空
bool isEmpty(){
return front == rear;
}
//判满(预留一个空位置)
bool isFull(){
return (rear + 1) % MAX_SIZE == front;
}
//获取大小
int getSize(){
return (rear - front + MAX_SIZE) % MAX_SIZE;
}
};

49.3.4 优势与应用场景

优势

  1. 空间利用率高:避免普通数组队列的"假溢出"问题,充分利用数组空间。
  2. 操作高效:入队和出队操作的时间复杂度均为\(O(1)\),与普通队列一致。

应用场景

  1. 固定大小的缓冲池:如嵌入式系统中的数据缓冲区,内存资源有限,需高效利用空间。
  2. 生产者-消费者模型:当生产者和消费者速度不匹配时,循环队列可作为中间缓冲,平衡两者的处理速度。
  3. 实时数据处理:如传感器数据采集,固定容量的循环队列可存储最新的N条数据,旧数据自动被新数据覆盖。

49.4 三种数据结构的对比

数据结构核心原则操作端典型实现时间复杂度 (基本操作)主要应用
后进先出 (LIFO)仅栈顶数组、链表O(1)O(1)函数调用、 括号匹配、 表达式求值
队列先进先出 (FIFO)队头(出)、 队尾(入)链表、数组O(1)O(1)任务调度、 BFS、缓冲处理
循环队列先进先出 (FIFO)队头(出)、 队尾(入)数组O(1)O(1)固定大小缓冲、生产者-消费者模型

49.5 注意事项

  1. 边界条件处理:栈和队列的出队、取元素操作前必须判空;入队操作前判满,避免越界错误。
  2. 内存管理:链表实现的栈和队列需在析构函数中释放所有节点,防止内存泄漏。
  3. 循环队列的容量:采用预留空位置判满的循环队列,实际可存储的元素个数为MAX_SIZE-1,需根据需求调整数组大小。
  4. 选择合适的实现方式:
    • 若元素数量固定且已知,优先使用数组实现(效率高)。
    • 若元素数量动态变化,优先使用链表实现(灵活性好)。
    • 若需高效利用数组空间且大小固定,选择循环队列。