栈和队列
详细教程可以观看
https://www.bilibili.com/video/BV1nJ411V7bd?spm_id_from=333.788.videopod.episodes&vd_source=daed5b8a51d3ab7eb209efa9d0ff9a34&p=48
栈和队列概念
栈和队列是限定插入和删除只能在表的端点进行的线性表
在装电池、装弹夹、拿放盘子时都会出现栈的思想,先放入的元素最先出来
队列时先排队的最先出来——先进先出
栈的定义与特点:
比较维度 | 一般线性表 | 栈 |
---|---|---|
运算规则 | 支持随机存取,可在任意位置插入、删除、读取元素 | 遵循 “后进先出(LIFO)”,操作仅限栈顶(入栈、出栈) |
逻辑结构 | 一对一关系 | 一对一关系 |
存储结构 | 顺序存储(如数组)、链式存储(如链表)等 | 顺序栈、链栈等 |
适用场景 | 适用于需要灵活操作元素的场景 | 适用于 “后进先出” 逻辑的场景,如函数调用栈、表达式求值 |
典型操作 | 通过下标直接访问任意位置元素 | 入栈(push)、出栈(pop)、获取栈顶元素(peek) |
单向队列的定义与特点:
比较维度 | 单向队列 |
---|---|
定义 | 只能在表的一端进行插入运算(尾插),在表的另一端进行删除运算(头删)的线性表 |
逻辑结构 | 一对一关系 |
存储结构 | 顺序队或链队,以循环顺序队列更常见 |
运算规则 | 只能在队首和队尾运算,遵循先进先出(FIFO)原则 |
实现方式 | 关键掌握入队和出队操作,依顺序队或链队不同而有差异 |
栈的顺序实现
设置base指针在栈底元素的位置,top表示栈顶元素之上的位置,优点新插入的元素直接在top指针操作即可,计算数量时直接相减,不需要加1。
清空操作:
让top与base指针相等即可。
// 清空栈void clear() {top = base; // 重置栈顶指针}
入栈操作:
直接将值复给top指针,然后top后移
// 入栈操作void push(int value) {*top = value; // 将值存入栈顶位置top++; // 栈顶指针上移}
出栈操作:
将top指针前移,然后取值
// 出栈操作int pop() {top--; // 栈顶指针下移return *top; // 返回栈顶元素}
完整代码:
在上边代码加上抛出异常,以及其他一些子功能。
#include <iostream>
#include <stdexcept>
using namespace std;class ArrayStack {
private:int *base; // 栈底指针(指向数组起始位置)int *top; // 栈顶指针(指向栈顶元素的下一个位置)int capacity; // 栈的总容量public:// 构造函数:创建指定容量的栈ArrayStack(int cap) {capacity = cap;base = new int[capacity];top = base; // 初始时栈顶指针等于栈底指针}// 析构函数:释放动态分配的内存~ArrayStack() {delete[] base;}// 获取栈的容量int getCapacity() {return capacity;}// 获取当前栈中元素数量(通过指针差计算)int size() {return top - base; // 指针差即为元素数量}// 检查栈是否为空bool isEmpty() {return top == base;}// 检查栈是否已满bool isFull() {return (top - base) == capacity;}// 入栈操作void push(int value) {if (isFull()) {throw overflow_error("栈已满,无法添加元素");}*top = value; // 将值存入栈顶位置top++; // 栈顶指针上移}// 出栈操作int pop() {if (isEmpty()) {throw underflow_error("栈为空,无法弹出元素");}top--; // 栈顶指针下移return *top; // 返回栈顶元素}// 查看栈顶元素(不弹出)int peek() {if (isEmpty()) {throw underflow_error("栈为空,无法查看栈顶元素");}return *(top - 1); // 返回栈顶元素的值}// 打印栈内容(从栈顶到栈底)void printStack() {if (isEmpty()) {cout << "栈为空" << endl;return;}cout << "栈内容(从顶到底): ";// 从栈顶向栈底遍历for (int *ptr = top - 1; ptr >= base; ptr--) {cout << *ptr << " ";}cout << endl;}// 清空栈void clear() {top = base; // 重置栈顶指针}
栈的链表实现
要知道,栈有着后进先出(LIFO)的特性,这就要求新元素入栈(push)和旧元素出栈(pop)都得在栈顶进行操作。
回顾链表的两种插入方法,头插与尾插,在只有一个指针的情况下,采用头插的方法较为简单。原因在于,链表头部进行插入和删除操作的时间复杂度都是 O (1)。要是使用尾插法,在删除尾部节点时,得先遍历到倒数第二个节点,时间复杂度就变成了 O (n),这显然不符合栈的操作要求。所以这里采用头插法,直接维护一个栈顶头。
完整代码:
// 定义链表节点结构
struct ListNode {int data;ListNode* next;ListNode(int num) : data(num), next(nullptr) {}
};class LinkedListStack {private:ListNode *stackTop; // 将头节点作为栈顶int stkSize; // 栈的长度public:LinkedListStack() {stackTop = nullptr;stkSize = 0;}~LinkedListStack() {while(stackTop != nullptr) {ListNode* temp = stackTop;stackTop = stackTop->next;delete temp;} stkSize = 0;}/* 获取栈的长度 */int size() {return stkSize;}/* 判断栈是否为空 */bool isEmpty() {return size() == 0;}/* 入栈 */void push(int num) {ListNode *node = new ListNode(num);node->next = stackTop;stackTop = node;//新结点作为栈顶stkSize++;}/* 出栈 */int pop() {int num = top();ListNode *tmp = stackTop;//创建临时指针指向栈顶stackTop = stackTop->next;//将栈顶指针下移// 释放内存delete tmp;//释放掉要出栈的指针stkSize--;return num;}/* 访问栈顶元素 */int top() {if (isEmpty()){cout<<"栈为空"<<endl;// 考虑返回一个特殊值或抛出异常return -1; }else{return stackTop->data; // 修改:使用正确的成员名}}/* 将 List 转化为 Array 并返回 */vector<int> toVector() {ListNode *node = stackTop;vector<int> res(size());for (int i = res.size() - 1; i >= 0; i--) {res[i] = node->val;node = node->next;}return res;}
};
队列顺序实现
队列表示
回顾单向队列的定义与特点
单向队列的定义与特点:
比较维度 | 单向队列 |
---|---|
定义 | 只能在表的一端进行插入运算(尾插),在表的另一端进行删除运算(头删)的线性表 |
逻辑结构 | 一对一关系 |
存储结构 | 顺序队或链队,以循环顺序队列更常见 |
运算规则 | 只能在队首和队尾运算,遵循先进先出(FIFO)原则 |
实现方式 | 关键掌握入队和出队操作,依顺序队或链队不同而有差异 |
溢出问题
在入队时随着元素的增加,当尾部指针进行达到最大容量时,发生溢出现象,分为真溢出和假溢出现象:
真溢出: front=0,即首指针从0开始,这时在添加没有位置,对应最左边图
假溢出: 由于front!=0,这是由于进行了出队操作,只能在对头进行删除导致front后移,之前的位置会空下来对应中间和最右边图,这时其实front之前是有空闲位置的。
解决方法:
可以使用首尾相接的环形数组来解决这个问题,让front与rear在越过数组尾部时,直接会搭配数组头部继续遍历,周期性的规律可以使用取余操作来完成,与后边的获取长度,判断是否已经满一起讲解。
以最右边假溢出为例:此时尾指针指向尾元素的后一个位置rear=5==capacity,发生溢出,在添加新的元素时,想让其存储在0所在位置,即rear=1,方法(rear+1)%capacity,(5+1)%5=1
同理删除元素时,front会后移,当front=4,要想在移动到队首0的位置,取余操作(4+1)%5=0,回到最初的位置
求解长度
求解长度的挑战:
rear与front是变化的,与rear、front指针的相对位置有关
公式(rear - front + queCapacity) % queCapacity
能够统一处理上述两种情况:
-
尾指针rear>首指针front,rear指向队尾元素下一个,用rear-front即可,以左边图片为例
mod((4-0+5)/5)=4
-
尾指针rear<首指针front,说明发生了循环队列现象,以中间图片为例,此时front=3,rear=0,用
mod((0-3+5)/5)=2
,等于队列长度 -
最右边图片
mod((1-3+5)/5)=3
,等于队列长度;
方法:当假溢出时出现的现象是rear索引以前的元素填充完毕但rear与front之间的元素是空着的 先计算front与rear之间的元素个数,由于rear指向尾元素下一个位置,直接用大数减小数即可,再用总容量减去空着的元素即可得到真实长度。
/* 获取队列的长度 */
int size() {return (rear - front + queCapacity) % queCapacity;
}
判断是否已满
问题:
在判断队空时方法是判断首尾指针是否相等,因为一般情况即元素并未满的时候,指针不相等,但随着元素的入队会出现首尾指针相等的情况,比如说图中front指向Q4,随着rear指针的移动,rear指向尾元素(图中Q8)的后一个元素,此时就会和front指针相撞,出现首尾指针相等的情况,这种情况下与队空情况相同。
解决方法-少用一个元素空间
如最右边的图所示,rear指向尾元素的下一个位置,此时rear指向空着的元素,如果令rear+1则rear指向和头指针重合,就和队空的情况区别开。同样取余运算是为了解决溢出问题。
/* 判断队列是否已满 */
bool isFull() {return (rear + 1) % queCapacity == front;
}
返回数组
假设环形队列如下:
queCapacity = 6
(数组下标 0~5)。front = 4
,rear = 2
(队列有效元素为 4→5→0→1)。- 数组
nums
存储:[7, 8, ?, ?, 5, 6]
(?
表示无效位置)。
调用toVector()
后的结果:
- 创建
vector
,大小为(2-4+6)%6 = 4
。 - 遍历过程:
j=4
:arr[0] = nums[4] = 5
。j=5
:arr[1] = nums[5] = 6
。j=0
:arr[2] = nums[0] = 7
。j=1
:arr[3] = nums[1] = 8
。
- 返回
vector
:[5, 6, 7, 8]
。
/* 将数组转化为 Vector 并返回 */
vector<int> toVector() {//创建数组容器vector<int> arr(size());//遍历环形队列,将非连续转换为连续的for (int i = 0, j = front; j != rear; i++, j = (j + 1) % queCapacity) {arr[i] = nums[j];}return arr;
}
完整代码
class ArrayQueue {private:int *nums; // 用于存储队列元素的数组int front; // 队首指针,指向队首元素int rear; // 队尾指针,指向队尾元素的下一个位置int queCapacity; // 队列容量,含多浪费的空间public:ArrayQueue(int cap) {queCapacity = cap + 1; // 为用户期望容量添加额外空间nums = new int[queCapacity];front = rear = 0;}~ArrayQueue() {delete[] nums;}/* 获取队列的容量 */int capacity() {return queCapacity-1;}/* 获取队列的长度 */int size() {return (rear - front + queCapacity) % queCapacity;}/* 判断队列是否为空 */bool isEmpty() {return front == rear;}/* 判断队列是否已满 */bool isFull() {return (rear + 1) % queCapacity == front;}/* 入队 */void push(int num) {if (isFull()) {cout << "队列已满" << endl;return;}// 将 num 添加至队尾nums[rear] = num;// 队尾指针向后移动一位,若越过尾部,则返回到数组头部rear = (rear + 1) % queCapacity;}/* 出队 */int pop() {int num = peek();// 队首指针向后移动一位,若越过尾部,则返回到数组头部front = (front + 1) % queCapacity;return num;}/* 访问队首元素 */int peek() {if (isEmpty())throw out_of_range("队列为空");return nums[front];}/* 将数组转化为 Vector 并返回 */vector<int> toVector() {//创建数组容器vector<int> arr(size());//遍历环形队列,将非连续转换为连续的for (int i = 0, j = front; j != rear; i++, j = (j + 1) % queCapacity) {arr[i] = nums[j];}return arr;}
};
队列链表实现
入队
思路:创建一个新节点,链接到尾指针的后继,然后将尾指针指向新节点。
由于未设置头指针,所以在入队时需要判断一个尾指针是否为空,空的话插入的第一个元素既是头结点也是尾节点,否则的话再尾节点添加值即可
/* 入队 */void push(int num) {ListNode* node = new ListNode(num);if (rear == nullptr) {// 队列为空时,新节点同时作为头尾节点front = node;rear = node;} else {// 队列非空,追加到尾节点后rear->next = node;rear = node; // 更新尾指针}}
出队
思路:出队只能从头节点开始进行删除,先将头结点进行保存,然后将头指针后移,将头指针删除。
只不过要注意将元素删除的只剩下一个时,头指针与尾指针全都指向最后一个元素。 执行完pop操作,此时头指针后移成为空指针,这时要将尾指针也变为空指针 。
/* 出队 */int pop() {if (isEmpty()) {throw std::out_of_range("队列为空");}ListNode* temp = front; // 保存待删除节点int num = temp->val; // 保存返回值front = front->next; // 头指针后移if (front == nullptr) {rear = nullptr; // 如果队列变空,重置尾指针}delete temp; // 删除原头节点return num;}
返回数组
设置一个容器接受这些数据
/* 将链表转化为 Vector 并返回 */vector<int> toVector() const {vector<int> res;ListNode* current = front;while (current != nullptr) {res.push_back(current->val);current = current->next;}return res;}
完整代码
#include <stdexcept>
#include <vector>// 链表节点定义
struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};/* 基于链表实现的队列(无哨兵节点) */
class LinkedListQueue {
private:ListNode* front; // 头指针(直接指向首元素)ListNode* rear; // 尾指针(直接指向尾元素)public:LinkedListQueue() : front(nullptr), rear(nullptr) {} // 初始化空指针~LinkedListQueue() {clear();}// 清空队列void clear() {while (front != nullptr) {ListNode* temp = front; // 临时节点指向当前头节点front = front->next; // 头指针后移delete temp; // 删除原头节点}rear = nullptr; // 重置尾指针}/* 获取队列的长度 */int size() const {int count = 0;ListNode* current = front;while (current != nullptr) {count++;current = current->next;}return count;}/* 判断队列是否为空 */bool isEmpty() const {return front == nullptr; // 头指针为空表示队列空}/* 入队 */void push(int num) {ListNode* node = new ListNode(num);if (rear == nullptr) {// 队列为空时,新节点同时作为头尾节点front = node;rear = node;} else {// 队列非空,追加到尾节点后rear->next = node;rear = node; // 更新尾指针}}/* 出队 */int pop() {if (isEmpty()) {throw std::out_of_range("队列为空");}ListNode* temp = front; // 保存待删除节点int num = temp->val; // 保存返回值front = front->next; // 头指针后移if (front == nullptr) {rear = nullptr; // 如果队列变空,重置尾指针}delete temp; // 删除原头节点return num;}/* 访问队首元素 */int peek() const {if (isEmpty()) {throw std::out_of_range("队列为空");}return front->val; // 直接返回头节点的值}/* 将链表转化为 Vector 并返回 */vector<int> toVector() const {vector<int> res;ListNode* current = front;while (current != nullptr) {res.push_back(current->val);current = current->next;}return res;}
};
双端队列顺序实现
在队列中,我们仅能删除头部元素或在尾部添加元素。双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
队首入队
让新进来的元素排在先进来之前称为队首入队,先移动后插入
/* 队首入队 */
void pushFront(int num) {if (isFull()) {throw std::overflow_error("Deque is full");}front = (front - 1 + capacity) % capacity;nums[front] = num;
}
可视化示例(容量=5)
初始状态(空队列):
索引: 0 1 2 3 4[ ] [ ] [ ] [ ] [ ]↑front=0, rear=0
执行 pushFront(10):
- 移动 front:
(0 - 1 + 5) % 5 = 4
- 在索引4插入10
索引: 0 1 2 3 4[ ] [ ] [ ] [ ] [10]↑front=4 (新队首)
再执行 pushFront(20):
- 移动 front:
(4 - 1 + 5) % 5 = 3
- 在索引3插入20
索引: 0 1 2 3 4[ ] [ ] [ ] [20] [10]↑ ↑ rear=0 front=3
再执行 pushFront(30):
- 移动 front:
(3 - 1 + 5) % 5 = 2
- 在索引2插入30
索引: 0 1 2 3 4[ ] [ ] [30] [20] [10]↑ front=2
这样就实现了front始终指向新添加的元素,便于下一次从队首插入。
队尾入队
先进的元素在前,后进的在后,先插入后移动:
/* 队尾入队 */
void pushBack(int num) {if (isFull()) {throw std::overflow_error("Deque is full");}nums[rear] = num;rear = (rear + 1) % capacity;
}
初始状态(空队列)
索引: 0 1 2 3[ ] [ ] [ ] [ ]↑(front=0)↑(rear=0)
操作1: pushBack(10)
- 在索引0插入10
- rear后移:(0 + 1) % 4 = 1
索引: 0 1 2 3[10] [ ] [ ] [ ]↑(front=0)↑(rear=1)
操作2: pushBack(20)
- 在索引1插入20
- rear后移:(1 + 1) % 4 = 2
索引: 0 1 2 3[10] [20] [ ] [ ]↑(front=0)↑(rear=2)
操作3: pushBack(30)
- 在索引2插入30
- rear后移:(2 + 1) % 4 = 3
索引: 0 1 2 3[10] [20] [30] [ ]↑(front=0)↑(rear=3)
队首出队
保留当前队首的值,作为输出,将front后移
/* 队首出队 */int popFront() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}int val = nums[front];front = (front + 1) % capacity;return val;}
队尾出队
先将尾指针前移,返回要出队的元素
/* 队尾出队 */int popBack() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}rear = (rear - 1 + capacity) % capacity;return nums[rear];}
完整代码
#include <stdexcept>
#include <vector>class ArrayDeque {
private:int* nums; // 存储队列元素的数组int front; // 队首指针,指向队首元素int rear; // 队尾指针,指向队尾元素的下一个位置int capacity; // 队列总容量(包含额外空间)public:ArrayDeque(int cap) {capacity = cap + 1; // 多分配一个空间用于区分空和满nums = new int[capacity];front = rear = 0;}~ArrayDeque() {delete[] nums;}/* 获取队列的有效容量(最大元素个数) */int getCapacity() {return capacity - 1;}/* 获取队列的当前长度 */int size() {return (rear - front + capacity) % capacity;}/* 判断队列是否为空 */bool isEmpty() {return front == rear;}/* 判断队列是否已满 */bool isFull() {return (rear + 1) % capacity == front;}/* 队首入队 */void pushFront(int num) {if (isFull()) {throw std::overflow_error("Deque is full");}front = (front - 1 + capacity) % capacity;nums[front] = num;}/* 队尾入队 */void pushBack(int num) {if (isFull()) {throw std::overflow_error("Deque is full");}nums[rear] = num;rear = (rear + 1) % capacity;}/* 队首出队 */int popFront() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}int val = nums[front];front = (front + 1) % capacity;return val;}/* 队尾出队 */int popBack() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}rear = (rear - 1 + capacity) % capacity;return nums[rear];}/* 访问队首元素 */int peekFront() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}return nums[front];}/* 访问队尾元素 */int peekBack() {if (isEmpty()) {throw std::out_of_range("Deque is empty");}int last = (rear - 1 + capacity) % capacity;return nums[last];}/* 将队列转化为 Vector 并返回 */std::vector<int> toVector() {std::vector<int> res(size());for (int i = 0, j = front; j != rear; i++, j = (j + 1) % capacity) {res[i] = nums[j];}return res;}
};
总结
操作 | 指针移动方向 | 元素插入/移除顺序 | 指针移动时机 | 边界处理 |
---|---|---|---|---|
队首入队 | 向前 | 先移动指针再插入 | 插入前 | (front-1+cap)%cap |
队尾入队 | 向后 | 先插入再移动指针 | 插入后 | (rear+1)%cap |
队首出队 | 向后 | 先取值再移动指针 | 取值后 | (front+1)%cap |
队尾出队 | 向前 | 先移动指针再取值 | 取值前 | (rear-1+cap)%cap |
双端队列(deque)的实现可以通过不同的方法组合,支持多种元素处理顺序。以下是几种常见的顺序组合方式及其说明:
- 先进先出(FIFO) - 普通队列模式
组合方式:
入队:使用 pushBack
(从队尾添加元素)
出队:使用 popFront
(从队首移除元素)
特点:
最先进入队列的元素最先被处理,严格遵循 先进先出 原则,与普通队列(Queue)的行为一致。
- 后进先出(LIFO) - 栈模式
组合方式:
入栈:使用 pushBack
(从队尾添加元素)
出栈:使用 popBack
(从队尾移除元素)
特点:
最后进入队列的元素最先被处理,遵循 后进先出 原则,与栈(Stack)的行为一致。
- 双向操作 - 双端队列模式
组合方式:
队首操作:使用 pushFront
(队首插入) + popFront
(队首移除)
队尾操作:使用 pushBack
(队尾插入) + popBack
(队尾移除)
特点:
可以灵活地在队列两端进行插入和删除操作,元素顺序取决于具体调用方式。
区别:
以下两种组合方式都能实现逻辑上的栈:区别在于元素物理存储顺序
操作组合 | 元素物理存储顺序 | 逻辑顺序 | 适用场景 |
---|---|---|---|
pushBack + popBack | 最旧元素 → … → 最新元素 | LIFO | 传统栈(如函数调用栈) |
pushFront + popFront | 最新元素 → … → 最旧元素 | LIFO | 需要队首操作的特殊场景 |
双端队列链表实现
采用无哨兵结点的方法:
与队列链表实现大致思路相同,只需要补充链接前驱即可。
在队首与队尾出队时其前驱与后继指向空指针即可。
/* 双向链表节点 */
struct DoublyListNode {int val; // 节点值DoublyListNode *next; // 后继节点指针DoublyListNode *prev; // 前驱节点指针DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {}
};/* 基于双向链表实现的双向队列 */
class LinkedListDeque {private:DoublyListNode *front, *rear; // 头节点 front ,尾节点 rearint queSize = 0; // 双向队列的长度public:/* 构造方法 */LinkedListDeque() : front(nullptr), rear(nullptr) {}/* 析构方法 */~LinkedListDeque() {// 遍历链表删除节点,释放内存DoublyListNode *pre, *cur = front;while (cur != nullptr) {pre = cur;cur = cur->next;delete pre;}}/* 获取双向队列的长度 */int size() {return queSize;}/* 判断双向队列是否为空 */bool isEmpty() {return size() == 0;}/* 队首入队 */void pushFirst(int num) {DoublyListNode* node = new DoublyListNode(num);if (isEmpty()) {front = rear = node;} else {node->next = front;front->prev = node;front = node;}queSize++;}/* 队尾入队 */void pushLast(int num) {DoublyListNode* node = new DoublyListNode(num);if (isEmpty()) {front = rear = node;} else {rear->next = node;node->prev = rear;rear = node;}queSize++;}/* 队首出队 */int popFirst() {if (isEmpty()) throw out_of_range("队列为空");int val = front->val;DoublyListNode* temp = front;front = front->next;if (front != nullptr) {front->prev = nullptr;} else {rear = nullptr; // 若队列已空,更新 rear}delete temp;queSize--;return val;}/* 队尾出队 */int popLast() {if (isEmpty()) throw out_of_range("队列为空");int val = rear->val;DoublyListNode* temp = rear;rear = rear->prev;if (rear != nullptr) {rear->next = nullptr;} else {front = nullptr; // 若队列已空,更新 front}delete temp;queSize--;return val;}/* 返回数组用于打印 */vector<int> toVector() {DoublyListNode *node = front;vector<int> res(size());for (int i = 0; i < res.size(); i++) {res[i] = node->val;node = node->next;}return res;}
};