栈和队列是操作受限的线性表,应用广泛。
栈
栈的基本概念
栈(Stack) 是限制在线性表的一端进行插入和删除操作的线性表,也称为 后进先出(LIFO, Last In First Out)或先进后出(FILO, First In Last Out) 线性表
- 栈顶(Top):允许进行插入、删除操作的一端,也称为表尾。用栈顶指针来指示栈顶元素
- 栈底(Bottom/Base):固定端,也称为表头
空栈:当表中没有元素时称为空栈
栈的设计
定义:
- 栈顶 Top,可以用一个指针指向或找到栈顶元素
- 栈底 Bottom,可以用一个指针指向或找到栈底元素
- 栈的大小 Size
操作:
- 初始化栈(构造一个空栈)
- 销毁栈
- 判断栈是否为空
- 返回栈的长度(即元素个数)
- (Push)元素进栈,成为栈顶元素
- (Pop)栈顶元素出栈,并返回其值
- 取栈顶元素, 但并不在栈里删除该元素
- 从栈底到栈顶依次对栈的每个元素调用visit()
顺序栈
顺序栈,也就是栈的顺序存储结构。和线性表相类似,用一维数组来存储栈
根据数组是否可以根据需要增大,又可分为:动态顺序栈 和 静态顺序栈
动态顺序栈
采用动态一维数组来存储栈,所谓动态指的是栈的大小可以根据需要增加
- 用 b a s e base base表示栈底指针,栈底固定不变的
- 栈顶则随着进栈和退栈操作而变化,用 t o p top top(称为栈顶指针)指向数组中的下一个空闲存储位置
- 用 t o p = = b a s e top==base top==base作为栈空的标记
- 进栈 P u s h Push Push:首先将数据元素保存到栈顶(top所指的当前位置),然后,执行top加1,使top指向栈顶的下一个存储位置
- 出栈
P
o
p
Pop
Pop:首先执行top减1,使top指向栈顶元素的存储位置,然后将栈顶元素取出
#define INITSIZE 100 //栈空间初始分配量
#define INCREMENTSIZE 10 //栈空间分配增量
typedef int ElemType ;
typedef struct {
int top; // 栈顶指针
ElemType *base; // 存栈的起始地址,栈不存在时值为NULL
int stacksize; // 当前已分配空间
}SqStack ;
//构造一个空栈s
bool InitStack(SqStack *s){
s->base=(ElemType *)malloc(INITSIZE * sizeof(ElemType));
if (!s) return false;
s->stacksize=INITSIZE;
s->top=0;
return true;
}
//取栈的长度
int GetLen(SqStack *s){
return (s->top);
}
//查看栈顶元素,不修改栈
bool GetTop(SqStack *s, ElemType *e){
if(s->top==0) return false; // 栈空,返回出错标志
*e=s->base[s->top-1];
return true;
}
//入栈(动态顺序栈的体现)
bool Push(SqStack *s, ElemType e){
if(s->top >= s->stacksize) {
//若栈已满,则增加INCREMENTSIZE个存储单元
s->base=(ElemType *)realloc(s->base,(s->stacksize + INCREMENTSIZE)*sizeof(ElemType));
if(!s->base) return false;
s->stacksize += INCREMENTSIZE;
}
s->base[s->top++] = e; //e成为新的栈顶元素,栈顶指针加1
return true;
}
//出栈
bool Pop(SqStack *s, ElemType *e){
if(s->top==0) return false; //栈空,返回出错标志
*e=s->base[--s->top];
return true;
}
//判断栈是否为空
int IsStackEmpty(SqStack *s){
if(s->top == 0) return 1;
else return 0;
}
//遍历栈,从栈顶到栈底依次对每个元素调用visit()
bool StackTraverse(SqStack *s, void visit(ElemType *e)){
int i;
if(s->top==0) return false;
for(i=s->top-1;i>=0;i--)
visit(&s->base[i]);//visit()访问函数
return true;
}
顺序栈的应用举例:
- 数制转换:
void Conversion(int n,int d) {
//将十进制整数n转换为d进制数
ElemType e;
SqStack s; if(!InitStack(&s)) return;
while(n!=0) {
//将余数逐一进栈
Push(&s, n%d);
n=n/d;
}
while(!IsStackEmpty(&s)) {
Pop(&s,&e);
printf("%d ",e);
}
}
- 括号匹配问题:
① 设置一个栈,当读到左括号时,将它进栈
② 当读到右括号时,
→ 若栈空,则该括号多余,返回ERROR
→ 否则,与栈顶元素(左括号)进行匹配(若匹配成功,从栈顶删除该左括号,继续读入;否则,匹配失败,返回ERROR)
③ 算法的终止条件
→ 输入结束的时候,栈为空,则终止,返回OK
→ 输入结束的时候,栈不为空,则终止,返回ERROR
→ 读到的右括号与栈顶的左括号不匹配或栈空,则终止,返回ERROR
//exps为待检查括号匹配的字符串数组
bool MatchingBrackets(char *exps) {
int i=0;
bool state=true;
ElemType e; SqStack s; InitStack(&s);
while(state && exps[i]!=‘\0’ ) {
//输入未结束
switch(exps[i]){
case '(':
case '[':
case '{':Push(&s,exps[i]); break;
//读到以上三种左括号就入栈
case ')':
case ']':
case '}':
if(!GetTop(&s,&e)){
state=false;
break;
}
//栈不为空进行括号匹配
if((e=='(' && exps[i]==')') || (e=='[' && exps[i]==']') || (e=='{' && exps[i]=='}'))
Pop(&s,&e);
else state=false;
break;
}
i++;
}
if(IsStackEmpty(&s) && state) return true;
else return false;
}
静态顺序栈
采用静态一维数组来存储栈,栈底固定不变的,而栈顶则随着进栈和退栈操作变化的
- 用一个整型变量 t o p top top(称为栈顶指针)指向当前栈顶位置,用 t o p = 0 top=0 top=0表示栈空的初始状态,每次top指向栈顶元素在数组中的存储位置
- 若栈的数组有 M a x s i z e Maxsize Maxsize个元素,则 t o p = M a x s i z e − 1 top=Maxsize-1 top=Maxsize−1时栈满→栈大小不可变
#define MAX_STACK_SIZE 100 //栈的大小
typedef int ElemType ; //举一个数据元素的例子
typedef struct {
ElemType stack_array[MAX_STACK_SIZE];
int top;
} SqStack;
链式栈
用链表的形式进行栈结构的构建
typedef struct Node{
ElemType data;
struct Node *next;
} LinkedStack; //结点定义
// 栈顶元素为链表头的next所指结点
//创建一个带头结点的空栈
LinkedStack *InitStack(void){
LinkedStack *s;
//s为链表头,不存数据
s=(LinkedStack *)malloc(sizeof(LinkedStack));
s->next=NULL;
return s;
}
//获取栈长度
int GetLen(LinkedStack *s)){
int i=0;
LinkedStack *p;
p=s->next;
while(p){
i++;
p =p->next;
}
return i;
}
//获取栈顶元素,但不修改栈
bool GetTop(LinkedStack *s,ElemType *e){
if(s->next==NULL) return false;//栈为空
*e=s->next->data;
return true;
}
bool Push(LinkedStack *s,ElemType e){
LinkedStack *p;
p=(LinkedStack *)malloc(sizeof(LinkedStack));
if(!p) return false;
p->data = e;
p->next=s->next; //新结点插入到头结点之后
s->next=p;
return true;
}
bool Pop(LinkedStack *s,ElemType *e){
LinkedStack *p;
if(!s->next) return false;
p=s->next;
*e=p->data; //取栈顶元素
s->next=p->next; // 修改栈顶指针
free(p);
return true;
}
int IsStackEmpty(LinkedStack *s){
if(s->next==NULL) return 1;
else return 0;
}
链式栈的应用举例
行编辑程序问题:
在用户输入一行的过程中,允许用户输入出差错,并在发现有误时可以及时更正。如何实现?
假设“#”为退格符,“@”为退行符,那么从终端接收了这样两行字符:
whli##ilr#e (s#*s)
outcha@putchar(*s=#++);
设立一个栈结构的输入缓冲区,用以接收用户输入的一行字符
int rowEdit(void){
LinkedStack *s;
s=InitStack();
ElemType c; char ch;
ch=getchar();
while(ch!=EOF) {
while(ch!=EOF && ch !='\n'){
switch(ch){
case '#':if(!Pop(s,&c)) return 1; break;
case '@': ClearStack(s);break;
default: if(!Push(s,ch)) return 1; break;
}
ch=getchar();
}
//打印出从栈底到栈顶的字符,并置s为空栈
PrintStack(s);
//遇到换行符,继续读下一行信息进行处理
if(ch!=EOF) ch=getchar();
}
return 0;
}
void PrintStack(LinkedStack *s){
LinkedStack *t, *p,*q;
ElemType e;
int i,j;
i=GetLen(s);
j=1;
t=InitStack();
while(j<=i){
Pop(s,&e);
Push(t,e);
j++;
}
p=t->next;
while(p!=NULL) {
printf("%s",p->data);
q=p;
p=p->next;
free(q);
}
printf("\n");
free(t);
}
栈的应用举例
算术表达式求值/中缀表达式求值
设置两个工作栈
-OPTR栈,用于存放算符,栈底元素设置为#
-OPND栈,用于存放操作数和运算结果,初始为空
依次读入表达式的每个字符
-若是操作数,则进OPND栈
-若是算符,则与OPTR栈的栈顶算符进行优先级比较,然后进行相应操作
直到表达式求值完毕(即OPTR栈顶元素和当前读入字符均为#)
// 引入算符优先级矩阵
#define OPSETSIZE 7
char OPSET[OPSETSIZE]={'+' , '-' , '*' , '/' ,'(' , ')' , '#'};
unsigned char Prior[OPSETSIZE][OPSETSIZE] = {'>','>','<','<','<','>','>',
'>','>','<','<','<','>','>',
'>','>','>','>','<','>','>',
'>','>','>','>','<','>','>',
'<','<','<','<','<','=',' ',
'>','>','>','>',' ','>','>',
'<','<','<','<','<',' ','=' };
typedef union{float x; char op;} ElemType;
typedef struct {
int top; //栈顶
ElemType *base;
int stacksize;
}SqStack; // 动态顺序栈
ElemType tmp;
char theta;float a,b;
Pop(&OptrStack, &tmp); theta=tmp.op;
Pop(&OpndStack, &tmp);a=tmp.x;
Pop(&OpndStack, &tmp);b=tmp.x;
tmp.x=Operate(a, theta, b);
Push(&OpndStack,tmp);
//测试Test是否是算符
bool In(char Test, char* TestOp){
for (int i=0; i< OPSETSIZE; i++)
if (Test == TestOp[i]) return true;
return false;
}
//执行四则运算:a theta b
float Operate(float a, unsigned char theta, float b) {
switch(theta) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
default : return 0;
}
}
int ReturnOpOrd(char op, char* TestOp) {
for(int i=0; i< OPSETSIZE; i++)
if (op == TestOp[i]) return i;
return 0;
}
//返回两算符之间的优先关系
char precede(char Aop, char Bop){
return Prior[ReturnOpOrd(Aop,OPSET)][ReturnOpOrd(Bop,OPSET)];
}
float EvaluateExpression(char* MyExpression) {
StackChar OPTR; // 算符栈,字符元素
StackFloat OPND; // 运算数栈,实数元素
char TempData[20];
strcpy(TempData,"\0");
float Data,a,b;
char theta,*c, x, Dr[2];
InitStack (OPTR); Push (OPTR, '#');
InitStack (OPND);
c = MyExpression;
while (*c!= '#' || GetTop(OPTR)!= '#') {
if (!In(*c, OPSET)) {
// *c不是运算符则进运算数栈
Dr[0]=*c; Dr[1]='\0'; strcat(TempData,Dr); c++;
if(In(*c, OPSET)){
Data=(float)atof(TempData); Push(OPND, Data);
strcpy(TempData,"\0");
}
}else { // 根据它与算符栈 顶的优先关系,做相应的动作
switch (precede(GetTop(OPTR), *c)) {
case ‘<’: //栈顶元素优先级低,则将读到的算符进栈
Push(OPTR, *c); c++; break;
case '=': // 脱括号并接收下一字符
Pop(OPTR, x); c++; break;
case ‘>’: // 栈顶算符出栈并将运算结果入操作数栈
Pop(OPTR, theta); Pop(OPND, b);
Pop(OPND, a); Push(OPND, Operate(a, theta, b));
break;
} // switch
}
} // while
return GetTop(OPND);
}
迷宫寻路
通常用的是“穷举求解路径”的方法:尝试不成功,沿原路退回,用栈来保存从入口到当前位置的路径
算法思想:
- 若当前位置“可通”,则纳入路径,继续前进
- 若当前位置“不可通”,则后退,换方向继续探索
- 若四周“均无通路”,则将当前位置从路径中删除出去
实现:
Q:如何表示迷宫?
-二维数组:下标(迷宫的格子),元素值(迷宫的状态)
Q:如何记录已经走过的路径?
-二维数组的下标+方向,栈一个数据元素记录三个信息(行,列,方向)
Q:如何记录已探索过的路径?
-方向
设定当前位置的初值为入口位置;
do{
若当前位置可通,
则{在迷宫标记该块被走过,将当前位置插入栈顶;
若该位置是出口位置,则算法结束;
否则切换当前位置的东邻方块为新的当前位置;
}
否则 {//当前位置不通
若栈不空但栈顶位置的四周均不可通,
则{删去栈顶位置,并在迷宫标记该块“不通”;
若栈不空,则重新测试新的栈顶位置,
直至找到一个可通的相邻块或出栈至栈空;
}
若栈不空且栈顶位置尚有其他方向未被探索,则设定新的
当前位置为沿顺时针方向旋转找到的栈顶位置的下一相邻块;
}
}while (栈不空);
若栈空,则表明迷宫没有通路
/***************************************/
//代码实现
//表示迷宫
typedef struct Maze {
char array[10][10];
//迷宫的墙:X;没有走过的通道块:空格;
//走过标记:*;走不通:!
} MazeType;
//迷宫的坐标
typedef struct {
int r, c; //r 表示行,c表示列
}PosType;
//表示路径中的一通道块
Typedef struct {
int ord; //表示该通道块在路径上的序号
PosType seat; //通道块在迷宫中的坐标位置
int di; //从此通道块走向下一个通道块的方向
} ElemType; //保存在栈中的元素类型
SqStack s; //记录从入口到当前位置的路径
// 判定迷宫的当前位置是否可通过(即未曾走到过的通道块)
bool Pass(MazeType MyMaze, PosType CurPos){
if (MyMaze.arr[CurPos.r][CurPos.c]==' ')
return true; // 如果当前位置是空格,则可以通过
else return false; // 可能是墙,可能已经走过:包括当前的路径和被标记为走不通的通道块
}
//在迷宫的当前位置留下走过标记(*)
void FootPrint(MazeType &MyMaze,PosType CurPos){
MyMaze.arr[CurPos.r][CurPos.c]='*';
}
//在迷宫的当前位置留下走不通标记(!)
void MarkPrint(MazeType &MyMaze,PosType CurPos){
MyMaze.arr[CurPos.r][CurPos.c]='!';
}
//返回当前位置的Dir方向所指示的位置
PosType NextPos(PosType CurPos, int Dir){
PosType ReturnPos;
switch (Dir){
case 1:
ReturnPos.r=CurPos.r;
ReturnPos.c=CurPos.c+1;
break;
case 2:
ReturnPos.r=CurPos.r+1;
ReturnPos.c=CurPos.c;
break;
case 3:
ReturnPos.r=CurPos.r;
ReturnPos.c=CurPos.c-1;
break;
case 4:
ReturnPos.r=CurPos.r-1;
ReturnPos.c=CurPos.c;
break;
}
return ReturnPos;
}
// 若迷宫maze中存在从入口 start到出口 end的通道,则求得一条通路存放在栈S中
bool MazePath(MazeType &maze, PosType start, PosType end, Stack &S) {
PosType curpos;
int curstep;
ElemType e;
curpos = start; // 设定"当前位置"为"入口位置"
curstep = 1; // 探索第一步
do {
if (Pass(maze, curpos)) {
// 当前位置可通过
FootPrint(maze,curpos); // 留下足迹
e.ord = curstep; e.seat= curpos; e.di =1;//从di=1开始
Push(S, e); // 将当前通道块加入路径
if (curpos.r == end.r && curpos.c==end.c)
return true; // 到达出口
//准备探索下一通道块
//设置当前通道块为当前通道块的东邻
curpos = NextPos(curpos, 1);
curstep++;
}else {
// 当前位置不能通过
if (!StackEmpty(S)) {
Pop(S,e);
while (e.di==4 && !StackEmpty(S)) {
// 留下不能通过的标记,并退回一步
MarkPrint(maze,e.seat);
Pop(S,e);
} // while
if (e.di<4) {
e.di++;
Push(S, e); // 换下一个方向探索
// 当前位置设为新方向的相邻块
curpos = NextPos(e.seat, e.di);
}
}
}
} while (!StackEmpty(S) );
return false;
}
递归的实现
编译器依靠“栈”来管理递归函数的调用
递归调用:一个函数(或过程)直接或间接地调用自己本身,简称递归(Recursive)
为了使递归调用不至于无终止地进行下去,有效的递归调用函数(或过程)应包括两部分:
- 递推规则(方法)
- 终止条件
优点:
- 结构清晰,程序易读
- 编写容易,因为编译程序代替用户完成了栈的管理
- 正确性容易得到证明
缺点:
- 往往比较耗时间耗空间。这是由于递归函数会不断进行函数的调用操作,而函数的调用是比较消耗资源的
- 可将递归函数转化为非递归函数
举例:
- 求迷宫的路径
bool SeekPath(PosType curPos,PosType endPoint) {
//从迷宫中坐标点curPos的位置寻找通向终点endPoint的路径
//若找到则返回true,否则返回false
if ((curPos.r==endPoint.r) && (curPos.c==endPoint.c)){
printf("(%d %d)\n", endPoint.r, endPoint.c);
return true;
}
for(int i=0; i<4; i++){
PosType pos = NextPos(curPos, i+1);
if(Pass(pos)){
FootPrint(pos); // 留下足迹
if(SeekPath(pos, endPoint)){
printf("(%d %d),", pos.r, pos.c);
return true;
}
}
} //for
return false;
}
- 尾递归:一个递归函数(或过程)中,递归调用语句是最后一条执行语句
//求阶乘函数
int Factorial ( int n ) {
if ( n == 0 ) return 1;
else return n*Factorial (n-1);
}
- 汉诺塔
int Count=0; //计数
void move(char x, int n, char z) {
printf(" %2d. Move disk %d from %c to %c\n", ++Count, n, x, z);
}
//将n个圆盘从x移动到z,y作为辅助塔
void hanoi (int n, char x, char y, char z) {
if (n==1)
move(x, 1, z); //将编号为1的圆盘从x移到z
else {
hanoi(n-1,x,z,y);
move(x, n, z); //将编号为n的圆盘从x移到z
//将y上编号为1至n-1的圆盘移到z, x作辅助塔
hanoi(n-1, y, x, z); }
}
PS:关于递归算法的时间复杂度计算可以看看这篇https://zhuanlan.zhihu.com/p/129887381
PS:对于尾递归和单向递归的算法,可用循环结构的算法替代