面试复习——数据结构(三):栈

栈和队列是操作受限的线性表,应用广泛。

栈的基本概念

栈(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=Maxsize1时栈满→栈大小不可变
#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:对于尾递归和单向递归的算法,可用循环结构的算法替代

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值