第3章 栈和队列
数据结构与算法基础(青岛大学-王卓)😂
课程link
3.1 栈和队列的定义和特点
1.普通线性表的插入和删除:可以任意位置插入和删除,插入/删除位置之后的元素需要后移/前移。
2.栈和队列是限定插入和删除只能在表的“端点”进行的线性表。栈:后进先出,队:先进先出

-
栈的常见应用:
1)数制转换
2)括号匹配的检验
3)行编辑程序
4)迷宫求解
5)表达式求值
6)八皇后问题
7)函数调用
8)递归调用的实现 -
队列的常见应用
![队列的常见应用]()
- 栈和队列的定义和特点
1)栈(stack)是特殊的线性表,限定仅在表尾进行插入和删除操作的线性表。后进先出,Last In First Out的线性表,简称LIFO结构。分类有顺序栈或链栈。
表尾(\(a_{n}\)端称为栈顶Top),表头(\(a_{1}\)端称为栈底Base)。
入栈(PUSH):插入元素到栈顶的操作
出栈(POP):从栈顶删除最后一个元素的操作
![]()
练习题1:三个元素a,b,c,入栈顺序a,b,c,由于并没有限制一定要所有元素都入栈后才能出栈,由于出栈时间的不同会有多种出栈顺序。
![题目1]()
2)队列(queue)是一种先进先出的线性表,在表尾(\(a_{n}\))插入,在表头(\(a_{1}\))删除。
分类:顺序队或链队,循环顺序队列更常见。
3.2 案例引入
1.进制转换(栈)
原理:十进制整数n向它的进制数d进行转换

用栈来解决的思路:取余

2.括号匹配的检验(栈)
思路:利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况。
三种不匹配的情况:1)右括号多于左括号:遇到右括号时,栈已空;2)括号交叉:栈中弹出的左括号与当前检验的右括号类型不同;3)左括号多于右括号:算术表达式输入完毕,栈中仍有未匹配的左括号。

示例代码:
#include <iostream>
#include <stack>
#include <unordered_map>
using namespace std;
bool isValid(string s) {
stack<char> st;
unordered_map<char, char> pairs = {{')', '('}, {']', '['}, {'}', '{'}};
for (char c : s) {
if (pairs.find(c) != pairs.end()) {
if (st.empty() || st.top() != pairs[c])
return false;
st.pop();
} else if (c == '(' || c == '[' || c == '{') {
st.push(c);
}
}
return st.empty();
}
int main() {
string test1 = "([])[]{}"; // 有效
string test2 = "([)]"; // 无效
cout << isValid(test1) << endl; // 输出1 (true)
cout << isValid(test2) << endl; // 输出0 (false)
return 0;
}
3.表达式求值(栈)
算符优先算法:由运算符优先级确定运算顺序。求值的过程是自左向右扫描表达式的每一个字符。
算符栈OPTR:寄存运算符operator。
操作数栈OPND:寄存运算数和运算结果。

4.舞伴问题(队列)
思路:先进先出,采用队列作为算法的数据结构。构造两个队列,依次将队头元素出队配成舞伴,若某队为空则另外一队等待着的人作为下一舞曲第一个获得舞伴的人。(OS资源调度,也是一种队列)

#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
void dancePartners(vector<string> men, vector<string> women) {
queue<string> maleQueue;
queue<string> femaleQueue;
// 初始化队列
for (const auto& name : men) maleQueue.push(name);
for (const auto& name : women) femaleQueue.push(name);
while (!maleQueue.empty() && !femaleQueue.empty()) {
// 配对舞伴
string male = maleQueue.front();
string female = femaleQueue.front();
maleQueue.pop();
femaleQueue.pop();
cout << "舞伴配对: " << male << " 和 " << female << endl;
}
// 处理剩余等待者
if (!maleQueue.empty()) {
cout << "下一舞曲第一个获得舞伴的是男性: " << maleQueue.front() << endl;
} else if (!femaleQueue.empty()) {
cout << "下一舞曲第一个获得舞伴的是女性: " << femaleQueue.front() << endl;
}
}
int main() {
vector<string> men = {"张三", "李四", "王五", "赵六"};
vector<string> women = {"小美", "小红", "小丽"};
dancePartners(men, women);
return 0;
}
3.3 栈的表示和操作实现
1.栈的抽象数据类型 Abstract Data Type

基本操作:初始化、入栈、出栈、取栈顶元素
//栈的基本操作:分顺序栈和链栈讨论
// InitStack(&S)构造一个空栈
void InitStack(LinkStack &S){
// 对于链栈而言,构造一个空栈,栈指针置为空
S = NULL;
return OK;
}
// DestroyStack(&S)销毁栈,释放内存空间,栈不再存在
Status DestroyStack(SqStack S){
if(S.base){
delete S.base;
S.stacksize = 0;
S.base = S.top = NULL;
}
return OK;
}
// ClearStack(&S)栈置空操作,栈存在且为空
Status ClearStack(SqStack S){
if(S.base)S.top = S.base;
return OK;
}
// StackEmpty(S)判断S是否为空栈,bool
Status StackEmpty(SqStack S){
////对于顺序栈而言,若栈为空,返回True,否则返回False
if (S.top == S.base)
return true;
else
return false;
}
Status StackEmpty(LinkStack S){
if(S==NULL)return true;
else return false;
}
// StackLength(S)求栈的长度
int StackLength(SqStack S)
{
return S.top-S.base;
}
// GetTop(S,&e)取栈顶元素e
SElemType GetTop(LinkStack S){
if(S!=NULL)
return S->data;
}
// Push(&S,e)插入元素e为新的栈顶元素
Status Push(SqStack &S, SElemType e){
if(S.top - S.base == S.stacksize) // 栈满
return ERROR;
*S.top = e; S.top++;
// *S.top++ = e; // e的优先级更高,所以先赋值,再自增1
return OK;
}
Status Push(LinkStack &S, SElemType &e){
p = new StackNode; // 生成新节点P
p->data = e; //将新节点数据域赋值e
p->next = S; //将新节点插入栈顶
S=p; //修改栈顶指针
return OK;
}
// Pop(&S,&e)删除S的栈顶元素an,通过e返回其值
Status Pop(SqStack &S, SElemType &e){
if(S.top == S.base) // if (StackEmpty(S))
return ERROR;
--S.top; e = *S.top; // 先指针下移,再取值,因top比实际栈顶高一个元素
// e = *--S.top; // 取栈顶元素
}
Status Pop(LinkStack &S, SElemType &e){
if(S==NULL)return ERROR;
e = S->data;
p = S; //指针指向需要删除的节点
S = S->next;
delete p; // 释放节点
return OK;
}
基础知识1:运算符优先级:*和++都是单目运算符,多个单目运算符在一起,结合方向从右到左 参考
*++ptr; // 先进行++ptr;后进行*ptr解引用,前置++会将操作数增加后用于表达式
*ptr++; //先进行ptr++;后进行*ptr解引用,后置++会将操作数的值先用于表达式再自增1
基础知识2:什么时候用“.”什么时候用“->”:对象放在heap上,就用指针(对象指针->函数);对象放在stack上,就用对象函数。stack编译器会自动分配内润,heap一般由程序员手动分配释放。简单来说,若ClassA a,则用a.function;若ClassA *a = new ClassA,则用a->function。
2.栈的分类、实现
1)顺序栈
利用一组地址连续的存储单元存放数据,top指针指示栈顶元素的位置(But, top通常指示真正的栈顶元素之上的下标地址,如一个栈{\(a_{1},a_{2},...,a_{n}\)},其下标对应的是0,1,...,n-1,而top的下标为),base指示栈底元素\(a_{1}\)的位置。
stacksize表示栈可使用的最大容量。

top和base可以使用int表示,即它们在栈中所表征的下标。也可以用指向对应元素的指针表示,两个指针相减的结果是两个指针之间相差多少个元素。
空栈:base == top,初始状态
栈满(上溢,overflow):top-base == stacksize。栈已满,又要压入元素,会报错并将错误返回操作系统,操作系统为其分配更大空间,并将原栈内容移入新栈。
下溢(underflow):栈已空,还要弹出元素。
注:上溢是一种错误,而下溢是一种结束条件。

2)链栈
运算受限的单链表,只能在链表头部操作。数据域 + 指针域,链表的头指针就是栈顶,指针从栈顶指向栈底。空栈相当于头指针指向空,基本不存在栈满的情况,插入和删除仅在栈顶处执行。

3.4 栈的递归
1.递归:一个对象部分包含自己,自己给自己定义(e.g. 单链表、树)。一个过程直接或间接地调用自己,称为递归的过程。
// 递归求n的阶乘
long Fact(int n){
if(n==0)return 1;
else return n*Fact(n-1);
}

递归函数调用的实现:“层次”,主函数第0层,第1次调用为第1层...第i次调用为第i层。重点参数:实参、返回地址(后调用的先返回,地址入栈)
| --- | --- |
|---|---|
| 优点 | 结构清晰,程序易读 |
| 缺点 | 每次调用要保存状态信息入栈,返回时要出栈恢复状态。 |
2.分治法求解递归问题
分治法:对于一个较为复杂的问题,分解成几个相对简单的且相同 or 类似的子问题求解。重点:递归的出口/边界

3.5 队列的表示、实现
队列(Queue):仅在表尾进行插入,在表头进行删除的线性表。
队头(Front):表头,\(a_{1}\)端
队尾(Rear):表尾,\(a_{n}\)端
插入元素称为入队,删除元素称为出队。

1.顺序队
线性表中每个元素包括L.element(数组)和L.length(整数)两个成员。队列每个元素包含3个成员,*base(数组) + front(整数)+ rear(整数)

初始状态:front=rear=0

1)溢出
- 真溢出:front=0,rear=MAXQSIZE
- 假溢出:front≠0,rear=MAXQSIZE
☆ 解决假上溢:将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear=MAXQSIZE时,若向量的起始端空闲,那么从“头”使用空着的空间。
2)循环队列

注:这里SqQueue不是指针,而是普通的结构体,所以使用“.”而不是“->”
// 引入循环队列,使用mod模运算
base[0]接在base[MAXQSIZE-1]之后,即rear+1=M时令rear=0
// 插入元素
Q.base[Q.rear] = x;
Q.rear = (Q.rear + 1)% MAXQSIZE;
// 删除元素
x = Q.base[Q.front]
Q.front = (Q.front+1)% MAXQSIZE;
3)如何区分栈空和栈满:rear=front
①设一个标志区别空、队满
②设另外一个变量来记录元素个数
③少用一个元素空间:rear+1就和front重合。队空:front=rear,队满:(rear+1)% MAXQSIZE==front

队满有两种情况:a)从队头按顺序填满 b)假溢出循环填满

- 循环队列操作
// 队列的初始化
Status InitQueue(SqQueue &Q){
Q.base = new QElemType[MAXQSIZE]; // 分配数组空间,Q.base是一个指针指向数组的首地址
if(!Q.base) exit(OVERFLOW); //存储分配失败
Q.base = Q.rear =0; // 头尾指针置为0,队列为空
return OK;
}
// 求队列的长度:循环队列时,尾指针的位置可能在低地址位,不能简单Q.rear-Q.front
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXQSIZE)% MAXQSIZE;
}
// 循环队列入队
Status EnQueue(SqQueue &Q, QElemType e){
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR; //队满
Q.base[Q.rear] = e;
Q.rear = (Q.rear+1)%MAXQSIZE; // 队尾指针+1
return OK;
}
// 循环队列出列
Status DeQueue(SqQueue &Q, QElemType e){
if(Q.front==Q.rear)return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front+1) % MAXQSIZE; // 队头指针+1
return OK;
}
// 取队头元素,没有用&Q——是对Q的副本进行操作
QElemType GetHead(SqQueue Q){
if(Q.front != Q.rear)
return Q.base[Q.front]; //返回队头指针元素的值,队头指针不变
}
2.链队列
当所用队列长度无法估计时,采用链队列。链表节点定义、队列定义

入队:Q.rear变化
出队:Q.front变化

// 链队列初始化
Status InitQueue(LinkQueue &Q){
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
Q.front->next = NULL;
return OK;
// 链队列销毁:从队头节点开始,依次释放所有节点
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p=Q.front->next; // 使用p暂存下一个位置
free(Q.front); // 如果直接free掉Q.front,那就转不到下一个了
Q.front=p;
// Q.rear=Q.front->next; free(Q.front); Q.front=Q.rear;
}
return OK;
}
// 链队列入队
Status EnQueue(LinkQueue &Q, QElemType e){
p = (QueuePtr)malloc(sizeof(QNode));
if(!p) exit(OVERFLOW);
p->data = e; p->next = NULL;
Q.rear->next = p; //链接上一个尾节点和新节点
Q.rear = p; //修改尾指针的位置
}
// 链队列出队
Status DeQueue(LinkQueue &Q, QElemType e){
if(Q.front==Q.rear) return ERROR;
p=Q.front->next; // 使用p暂存下一个位置
e=p->data;
Q.front->next=p->next;
// 如果删除的节点恰好是尾节点
if(Q.rear==p)Q.rear=Q.front;
delete p; // 释放指针空间
return OK;
}
// 获取链队列头元素
Status GetHead(LinkQueue Q, QElemType &e){
if(Q.front==Q.rear) return ERROR;
e = Q.front->next->data;
}




浙公网安备 33010602011771号