数据结构——第三章 栈和队列
第三章 栈和队列
3.1 栈
3.1.1 栈的基本概念
-
栈(stack)是一个特殊的线性表,是限定仅在一端(通常在表尾)进行插入和删除操作的线性表;又称为后进先出(Last In First Out)的线性表,简称LIFO结构
例如:栈s=(a1,a2,…):a1称为栈底元素,an称为栈顶元素
-
栈顶:允许插入和删除的一端,表尾(an端)称为栈顶Top
栈底:不允许插入和删除的一端,表头(a1端)称为栈底Base
-
逻辑结构:一对一关系,与普通线性表相同;存储结构:顺序栈(更常见)和链栈
数据的运算:插入元素——称为入栈进栈压栈 PUSH(x);删除元素——称为出栈弹栈 pop(y)
3.1.2 栈的顺序存储实现
*顺序栈的定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize] //静态指针存放栈中元素
int top; //栈顶指针(一般指的是数组下表)
}SqStack; //Sq:sequence顺序
void testStack(){
SqStack S; //声明一个顺序栈(分配空间,其大小为MaxSize*sizeof(ElemType))
}
*栈的初始化
void InitStaack(SqStack &S){
S.top=-1; //初始化栈顶指针
}
//判断栈空
bool StackEmpty(SqStack S){
if(S.top==-1)
return true;
else
return false;
}
*进栈操作
bool Push(SqStack &S,EleemType x){
if(S.top==MaxSize-1)
return false;
S.top=S.top+1; //此处两句等价于:S.data[++S.top]=x——先加再运算
S.data[S.top]=x; //先加1指向下一个,再使用top值入栈
return ture;
}
*出栈操作
bool Pop(SqStack &S,ElemType &x){
if(S.top==-1)
return false;
x=S.data[S.top]; //等价于:S.data[S.top--]=x——先运算再减
S.top=S.top-1;
return ture;
}
- 数据还残留在内存中,只是逻辑上被删除了
*读栈顶元素
bool GetTop(SqStack &S,EleemType &x){
if(S.top==-1)
return false;
x=S.data[S.top];
return ture;
}
*另一种方式
- 初始时,栈顶指针指向0:
S.top==0;
//进栈
S.data[S.top]=x; //等价于:S.data[S.top++]=x
S.top=S.top+1;
//出栈
S.top=S.top-1; //等价于:S.data[--S.top]=x
x=S.data[S.top];
- 栈满的条件:
top==MaxSize
*共享栈
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top0; //0号栈顶指针,栈底-1开始
int top1; //1号栈顶指针,栈顶MaxSize开始
}ShStack;
//共享栈的初始化
void InitStack(ShStack &S){
S.top0=-1;
S.top1=MaxSize;
}
- 两个栈共享一片空间,逻辑上实现了两个栈,物理上只使用了一片存储空间
- 栈满的条件:
top0+1==top1
3.1.3 栈的链式存储方式
- 链栈的基本操作:类似于单链表的头插法和对头结点的删除操作
*链栈的定义
- 和单链表类似,但一般推荐不带头结点的链栈
typedef struct Linknde{
ElemType data;
struct Linknode *next;
}*LiStack;
3.2 队列
3.2.1 队列的基本概念
-
队列(Queue)是只允许在一端进行插入,在另一端删除的线性表;又称为先进先出(FIFO)的线性表
-
队头:允许删除的一端;队尾:允许插入的一端
3.2.2 队列的顺序实现
*顺序队列的定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front; //队头指针:指向队头元素
int rear; //队尾指针:指向队尾元素的后一个位置(下一个应该插入的位置)
}SqQueue;
*顺序队列的初始化
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front;
int rear;
}SqQueue;
void InitQueue(SqQueue &Q){
Q.front=Q.rear=0; //0既是队头位置,也是下一个插入的位置
return OK;
}
//判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.front==Q.rear)
return true;
else
return false;
}
*循环队列——入队操作
-
队列元素个数——
(rear-front+MaxSize)%MaxSize;
-
队空——队尾和队头指针指向同一位置
front==rear;
队满——队尾指针的下一位置是队头
(rear+1)%MAXQSIZE==front;
bool EnQueue(SqQueue &Q,ElemType x){
if((Q.rear+1)%MAXQSIZE==Q.front) //判断队满
return false;
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxQSize; //队尾指针后移,指针加1取模
return true;
}
- 模运算:即取余运算,a%b==a除以b的余数,也表示为a MOD b
*循环队列——出队操作
bool DeQueue(SqQueue &Q,ElemType &x){
if(Q.rear==Q.front) //判断队空
return false;
x=Q.data[Q.front];
Q.front=(Q.f+1)%MaxQSize; //队头指针后移
return true;
}
//获得队头元素(只删除后移的代码)
bool GetQueue(SqQueue &Q,ElemType &x){
if(Q.rear==Q.front)
return false;
x=Q.data[Q.front];
return true;
}
*判断队列已满/已空
-
方案一:牺牲一个存储空间:
队列元素个数——
(rear-front+MaxSize)%MaxSize;
队空——
front==rear;
队满——(rear+1)%MAXQSIZE==front;
-
方案二:不牺牲存储空间(设置size)
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front;
int rear;
int size; //设置一个计数size
}SqQueue;
-
初始化:
rear=front=0; size=0;
插入成功:size++;
删除成功:size--;
队满条件:
size==MaxSize;
队空条件size=0;
-
方案三:不牺牲存储空间(设置tag)
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front;
int rear;
int tag; //设置一个计数tag
}SqQueue;
-
初始化:
rear=front=0; tag=0;
插入成功:tag=1(入队);
删除成功:tag=0(出队);
队满条件:
front==rear&&tag==1;
队空条件tag=0
*其他出题方法(队尾)
- 队尾指针指向队尾元素的位置:初始时:
Q.rear=-1;
//入队操作(颠倒顺序)
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxQSize;
//出队操作
x=Q.data[Q.front];
Q.front=(Q.f+1)%MaxQSize;
//判空
(Q.rear+1)%MAXQSIZE==Q.front; //头指针在尾指针的后一个位置(类似循环)
//判满(方案一:牺牲一格存储单位)
(Q.rear+2)%MAXQSIZE==Q.front; //头指针在尾指针的后两个位置
//判满方案二:增加辅助变量
3.2.3 队列的链式实现
*链式队列的定义
typedef struct LinkNode{ //定义链式队列的结点
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{ //定义链式队列
LinkNode *front,*rear;
}LinkQueue;
- 链队列——链式存储实现的队列
*链式队列的初始化(带头结点)
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
//初始化队列(带头结点)
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode)); //初始时,头尾指针都指向头结点
Q.front->next=NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue){
if(Q.front==Q.rear)
return true;
else
return false;
}
void testLinkQueue(){
LinkQueue(); //声明一个队列
InitQueue(Q) //初始化队列
//...后续操作...
}
*链式队列的初始化(不带头结点)
void InitQueue(LinkQueue &Q){
Q.front=NULL; //初始时,头尾指针都指向NULL
Q.rear=NULL;
}
bool IsEmpty(LinkQueue){
if(Q.front==NULL)
return true;
else
return false;
}
*链式队列——入队(带头结点)
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
s->data=x;
s->next->NULL;
Q.rear->next=s;
Q.rear=s;
}
*链式队列——入队(不带头结点)
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
s->data=x;
s->next->NULL;
if(Q.front==NULL){ //在空队列中插入第一个元素
Q.front=s;
Q.rear=s;
}else{
Q.rear->next=s;
Q.rear=s;
}
}
*链式队列——出队(带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front==Q.rear)
return false;
LinkNode *p=Q.front->next; //出队为头指针的下一个结点
x=p->data;
Q.front->next=p->next;
if(Q.rear==p) //此次是最后一个结点出队
Q.rear=Q.front; //头尾指针均指向头结点,为空状态
free(p);
return true;
}
*链式队列——出队(不带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front==NULL)
return false;
LinkNode *p=Q.front; //出队为头指针结点
x=p->data;
Q.front=p->next;
if(Q.rear==p){ //此次是最后一个结点出队
Q.rear=NULL;
Q.front=NULL;
}
free(p);
return true;
}
- 队列满的情况:顺序存储——预分配的空间耗尽时队满;链式存储——一般不会满队,除非内存不足
3.2.4 双端队列
-
双端队列:只允许从两端插入、两端删除的线性表
输入受限的双端队列:只允许从一端插入、两端删除的线性表;输出受限的双端队列:只允许从两端插入、一端删除的线性表
-
考点:判断输出序列的000合法性——考特兰数:C n(上) 2 n(下) /(n+1)
3.3 栈的运用
3.3.1 栈在括号匹配中的应用
-
IDE :可视化编程环境
-
栈的特性——最后出现的左括号最先被匹配
遇到左括号:入栈;遇到右括号:出栈“消耗”一个左括号
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top; //栈顶指针
}
bool bracketCheck(char str[],int length){
SqStack S; //定义一个栈
InitStack(S); //初始化一个栈
for(i=0;i<length;i++){ //下标从0开始
if(str[i]=='('||str[i]=='{'||str[i]=='['){
Push(S,str[i]); //入栈左括号
}else{
if(EmptyStack(S))
return false;
char topElem;
Pop(S,topElem); //出栈左括号;依次判断不匹配的可能
if(str[i]==')'&&topElem!='(')
return false;
if(str[i]=='}'&&topElem!='{')
return false;
if(str[i]==']'&&topElem!='[')
return false;
}
}
return StackEmpty(S); //检索完后,栈空才说明成功
}
- 匹配失败的情况:左右括号不匹配;左右括号单身
3.3.2 栈在表达式求值中的应用
-
算数表达式的三个组成部分:操作数、运算符、界限
例子:a+b-c*d(先算a加b,再算c乘d,再算减法)
-
中缀表达式:a+b-c*d;后缀表达式:ab+cd *-;前缀表达式:- +ab * cd
后缀表达式(逆波兰表达式)——运算符在两个操作数后面
前缀表达式(波兰表达式)——运算符在两个操作数前面
*中缀表达式转前缀表达式
-
手算:运算符和两个操作数组合成为一个新的操作数,再参与下次运算
“右优先”原则:只要右边的运算符能先计算,就优先算右边的;让前缀表达式的运算符从右到左依次生效
*前缀表达式的计算
- 手算:从右开始,第一个运算符及其右边两个操作数开始运算
- 机算:
- 从右往左依次扫描元素
- 遇到操作数:直接压入栈
- 遇到运算符:弹出两个栈顶元素,执行相应运算,结果压回栈顶
- 注意:先弹出栈的是“左操作数”;除法和减法部分需要注意
*中缀表达式转后缀表达式
-
手算:两个操作数和运算符组合成为一个新的操作数,再参与下次运算
“左优先”原则:只要左边的运算符能先计算,就优先算左边的,可保证运算顺序一致
-
机算:
初始化一个栈,用于保存还不能确定运算顺序的运算符
从左到右依次扫描各个元素,直到末尾(多种方法)
- 遇到操作数:直接加入后缀表达式
- 遇到界限符:遇到“(”直接入栈;遇到“)”依次弹出栈内运算符,并加入后缀表达式,直到遇到“(”
- 遇到运算符:依次弹出栈中优先级高于等于当前运算符的所有运算符,并加入后缀表达式,若遇到“(”或栈空则停止。之后再把当前运算符入栈
- 上述处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式
-
注意:后缀表达式中没有左右括号
*后缀表达式的计算
- 手算:从左开始,第一个运算符及其左边两个操作数开始运算
- 机算:
- 从左往右依次扫描元素
- 遇到操作数:直接压入栈
- 遇到运算符:弹出两个栈顶元素,执行相应运算,结果压回栈顶
- 注意:先弹出栈的是“右操作数”
*中缀表达式的计算
-
中缀表达式计算:中缀转后缀+后缀表达式求值
-
初始化两个栈——操作数栈和运算符栈
- 遇到操作数:压入操作栈
- 遇到运算符或界限符:按照“中缀转后缀”压入运算符栈;期间每弹出运算符,就需要再弹出两个数运算,再压回操作数栈
- 最后的运算结果在操作数栈顶
3.3.3 栈在递归中的应用
-
函数调用的特点:最后被调用的函数最先执行(LIFO)
函数调用时,需要用一个栈(函数调用栈)存储——调用返回地址、实参、局部变量
-
适合用“递归”算法解决的特点:可以把问题转为属性相同,但规模较小的问题
递归调用时,函数调用栈可称为“递归工作栈”;每进入一层递归,就将递归调用所需信息压入栈顶;每退出一层递归,就从栈顶弹出相应信息;
(进入:return到下一个要计算的函数;退出:return到上一个函数的结果)
*递归算法求阶乘
int factorial(int i){
if(n==0||n==1)
return 1; //当n减到1时,返回1到factorial(1+1)函数中算(1+1)*fac(1)
else
return n*factorial(n-1); //先递减进入函数,再递加计算每一层函数
}
int main(){
//...其他代码
int x=factorial(10);
}
-
缺点:太多层递归会导致栈溢出,空间复杂度太高;可能包含很多重复运算
可以自定义栈将递归算法改造成非递归算法
3.3.4 队列的应用
-
队列应用——树的层次遍历、图的广度优先遍历
-
队列在操作系统中的应用——CPU:先来先服务(FCFS)
3.4 特殊矩阵的压缩存储
-
数组的存储结构:一维数组、二维数组
特殊矩阵:对称矩阵、三角矩阵、三对角矩阵、稀疏矩阵
-
注意:除非特殊说明,否则数组下标默认从0开始
- 二维数组:b[2] [4]表示2行4列的数组
- 普通矩阵:可用二维数组存储;注意描述矩阵元素时通常从1开始,数组从0开始
- 对称矩阵:存储位置行数和列数相等,关于对角线对称;下三角区(i>j)\上三角区(i<j),计算时可用只存储主对角线+上(下)对角线;按行优先原则
- 三角矩阵:存储在主对角线和上(下)三角区,其余元素都为常量c;计算时按行优先原则与对称矩阵相似,并在最后一个位置存储常量c
- 三对角矩阵:行号和列号绝对值差大于1,其余为0;按行优先原则,只有头尾存储2个元素,其余为3个元素;计算时先求行号,列号最多与行号差1
- 稀疏矩阵:顺序存储——三元组<行,列,值>;链式存储——十字链表法<行,列,值><指向同列下一元素,指向同行下一元素>;包含向右域right和向下域down分别指向对应行列的第一个元素