数据结构学习--栈
对于栈这个数据结构来说,其实它本身数据存储形式与线性表相同,但是从数据结构来看,他们又有较大不同,而区分他们的就是栈的“后进先出”的特点。
而实现这种特点的原因就是栈的插入与删除元素都是在栈的一边进行的,而那个边的元素称为栈顶。当a1,a2...an插入时,从栈顶到栈底分别是an...a1。
① 栈一般有两种存储表示方法,我们一一来介绍:
栈的顺序存储结构:
#define STACK_INIT_SIZE 100 //栈的初始大小 #define STACKINCREMENT 10 //栈每次申请空间增加的大小 #define OK 1 #define ERROR 0 #define OVERFLOW -2 typedef struct { char *base; //栈底指针 char *top; //栈顶指针 int stacksize; //现在栈的空间大小 }Cstack; int InitStack(Cstack &S){ //建立顺序栈 S.base = S.top = (char *)malloc(sizeof(char) * STACK_INIT_SIZE); //为栈申请空间 if( !S.base ) exit(OVERFLOW); S.stacksize = STACK_INIT_SIZE; //保存现在栈的长度大小 return OK; } int StackEmpty(Cstack S){ //栈的判空 if(S.base == S.top) //当栈顶指针仍和栈底指针指向同一位置的时候,说明这是一个空栈 return OK; else return ERROR; } int Push(Cstack &S, char e){ //入栈 if(S.top - S.base >= S.stacksize){ //如果栈满,则为栈增加申请空间 S.base = (char *)realloc(S.base, sizeof(char ) * (S.stacksize + STACKINCREMENT)); if( !S.base ) exit(OVERFLOW); S.top = S.base + S.stacksize; S.stacksize += STACKINCREMENT; } *S.top ++ = e; //入栈的同时,让栈顶指针指向下一个空间 return OK; } int Pop(Cstack &S, char &e){ //出栈 if( StackEmpty(S) ) //栈空则报错 return ERROR; e = * -- S.top; //出栈的同时,让栈顶指针指向上一个空间 return OK; }
栈的链式存储结构:
栈的链式存储结构与线性表的线性存储结构相似,就不写备注了。
typedef struct stack{ int data; struct stack *next; }CSTACK, *Linkstack; int InitlistStack(Linkstack top){ top -> next = NULL; return OK; } int Emptyliststack(Linkstack top){ if(top -> next == NULL) return OK; else return ERROR; } int Pushlist(Linkstack top, int e){ CSTACK *temp; temp = (Linkstack)malloc(sizeof(CSTACK)); if( !temp ) exit(OVERFLOW); temp -> data = e; temp -> next = top -> next; top -> next = temp; return OK; } int Poplist(Linkstack top, int &e){ CSTACK *temp; if(Emptyliststack(top)) return ERROR; temp = top -> next; e = temp -> data; top -> next = temp -> next; free(temp); return OK; }
②栈的应用
栈的应用很广泛,而这紧扣它的特点,也就是后进先出,所有有类似特点的问题,都可以用栈这种数据结构来解决。下面举几个简单的例子
(1)进制转换
这里拿十进制转化为八进制为例,我们都知道十进制转化为八进制是不断用8去除十进制数,得到余数,最后将余数从最新得到的开始依次输出得到的就是八进制的数。这显然是一个后进先出的问题,所以可以采取的策略是将每次读出的余数存入一个栈中,最后出栈打印出的一串数字即是我们需要求的数字,算法思想比较简单:
int Hex_converter(){ Cstack S; int n, e; InitStack(S); scanf("%d", &n); while(n){ Push(S, n % 8); //存储余数入栈 n /= 8; //对十进制数除以8 } while(!StackEmpty(S)){ Pop(S, e); //出栈后打印 printf("%d", e); } return OK; }
(2)括号配对
括号配对这里指的是多种括号的配对问题,也就是有”()“,”{}“和”[]“三对括号的配对,括号配对的算法思想就是将左括号入栈,然后当读入的是右括号的时候,在栈不空的情况下出栈,判断出栈的左括号和读入的右括号是不是配对,如果不是配对,那就出现了不配对的情况,报错。如果栈空的话,那就说明右括号多了,同样是不配对的,所以报错
int Matching_brackets(char *str){ Cstack S; char e; InitStack(S); for(int i = 0; i < strlen(str); i ++){ switch(str[i]){ case '(' : Push(S, str[i]); continue; case '{' : Push(S, str[i]); continue; case '[' : Push(S, str[i]); continue; case ')' : Pop(S, e); if(e == '(') continue; else printf("not matching !\n"); return ERROR; case '}' : Pop(S, e); if(e == '{') continue; else printf("not matching !\n"); return ERROR; case ']' : Pop(S, e); if(e == '[') continue; else printf("not matching !\n"); return ERROR; } } }
(3)表达式的求值
表达式的求值这里只考虑有十以内的只含"()"类型括号的表达式的求值。对于这个的算法思想是我们需要两个栈,一个栈来存储数字,另一个栈用来存储运算符。同样,我们需要提前定义运算符的运算先后顺序,此后是依次对表达式每个值进行处理。先对元素进行分类,如果数字直接进数字的栈,对将要处理的符号元素与栈顶元素进行比较,如果优先级低的话那就入符号栈,如果相等的话,那就出栈,因为这样可以消去一对括号。如果大的话,那就直接出栈一个符号元素与两个数字元素进行运算工作,然后继续比较,直到当前元素的优先级不比栈顶元素高。下面贴上代码:
int In(char c) //判断是否是数字 { if(c != '+' && c != '-' && c != '*' && c != '/' && c != '(' && c != ')' && c != '#' ) return 1; else return 0; } char CompareOPTR(char ch1, char ch2){ //元素符优先级 if(ch1 == '+' && ch2 == '+') return '>'; if(ch1 == '+' && ch2 == '-') return '>'; if(ch1 == '+' && ch2 == '*') return '<'; if(ch1 == '+' && ch2 == '/') return '<'; if(ch1 == '+' && ch2 == '(') return '<'; if(ch1 == '+' && ch2 == ')') return '>'; if(ch1 == '+' && ch2 == '#') return '>'; if(ch1 == '-' && ch2 == '+') return '>'; if(ch1 == '-' && ch2 == '-') return '>'; if(ch1 == '-' && ch2 == '*') return '<'; if(ch1 == '-' && ch2 == '/') return '<'; if(ch1 == '-' && ch2 == '(') return '<'; if(ch1 == '-' && ch2 == ')') return '>'; if(ch1 == '-' && ch2 == '#') return '>'; if(ch1 == '*' && ch2 == '+') return '>'; if(ch1 == '*' && ch2 == '-') return '>'; if(ch1 == '*' && ch2 == '*') return '>'; if(ch1 == '*' && ch2 == '/') return '>'; if(ch1 == '*' && ch2 == '(') return '<'; if(ch1 == '*' && ch2 == ')') return '>'; if(ch1 == '*' && ch2 == '#') return '>'; if(ch1 == '/' && ch2 == '+') return '>'; if(ch1 == '/' && ch2 == '-') return '>'; if(ch1 == '/' && ch2 == '*') return '>'; if(ch1 == '/' && ch2 == '/') return '>'; if(ch1 == '/' && ch2 == '(') return '<'; if(ch1 == '/' && ch2 == ')') return '>'; if(ch1 == '/' && ch2 == '#') return '>'; if(ch1 == '(' && ch2 == '+') return '<'; if(ch1 == '(' && ch2 == '-') return '<'; if(ch1 == '(' && ch2 == '*') return '<'; if(ch1 == '(' && ch2 == '/') return '<'; if(ch1 == '(' && ch2 == '(') return '<'; if(ch1 == '(' && ch2 == ')') return '='; if(ch1 == ')' && ch2 == '+') return '>'; if(ch1 == ')' && ch2 == '-') return '>'; if(ch1 == ')' && ch2 == '*') return '>'; if(ch1 == ')' && ch2 == '/') return '>'; if(ch1 == ')' && ch2 == ')') return '>'; if(ch1 == ')' && ch2 == '#') return '>'; if(ch1 == '#' && ch2 == '+') return '<'; if(ch1 == '#' && ch2 == '-') return '<'; if(ch1 == '#' && ch2 == '*') return '<'; if(ch1 == '#' && ch2 == '/') return '<'; if(ch1 == '#' && ch2 == '(') return '<'; if(ch1 == '#' && ch2 == '#') return '='; } char Caculate(char a, char theta, char b){ //数值运算函数 if(theta == '+') return a + b; if(theta == '-') return a - b; if(theta == '*') return a * b; if(theta == '/') return a / b; } int EvaluateExpression(){ SqStack OPTR; //符号栈 SqStack OPND; //数字栈 char c; char x; char theta; char a,b; InitStack(OPTR); Push(OPTR,'#'); //以'#'作为表达式的开始与结束 InitStack(OPND); c = getchar(); while(c != '#' || GetTop(OPTR) != '#'){ if(In(c)){ Push(OPND, c - 48); //数字入数字栈 c = getchar(); } else switch(CompareOPTR(GetTop(OPTR), c)){ //判断栈顶元素与读出的符号的元素的优先级 case '<' : Push(OPTR, c); c = getchar(); break; case '=' : Pop(OPTR,x); //只为了消去一对括号 c = getchar(); break; case '>': Pop(OPTR, theta); Pop(OPND, b); Pop(OPND, a); Push(OPND, Caculate(a, theta, b) ); break; } } return GetTop(OPND); //最后运算得到的数字存在数字栈中,取出 }
(4)迷宫的求解解答路径的问题:
此算法思想便是DFS,即深度优先算法,按一个方向去查找,如果不通换方向,如果四个方向均不通,退回一格,继续递归,具体看代码吧
typedef struct{ int x; int y; }PosType; typedef struct{ int ord; PosType seat; int direction; }MazeType; typedef struct{ MazeType *base; MazeType *top; int stacksize; }MazeStack; int MazeMap[9][9] = {{0,0,0,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,0}, {0,1,0,0,1,0,0,1,0}, {0,1,0,1,1,0,1,1,0}, {0,1,0,1,0,1,0,1,0}, {0,1,1,1,1,1,0,1,0}, {0,0,1,0,0,1,0,0,0}, {0,1,1,1,1,1,1,1,0}, {0,0,0,0,0,0,0,1,1}}; int InitStack(MazeStack &S) { S.base = (MazeType*)malloc(STACK_INIT_SIZE * sizeof(MazeType)); if(!S.base) exit(OVERFLOW); S.top = S.base; S.stacksize = STACK_INIT_SIZE; return OK; } int Push(MazeStack &S, MazeType &e) { if(S.top - S.base >= S.stacksize){ S.base = (MazeType *)realloc(S.base, S.stacksize + STACKINCREMENT * sizeof(MazeType)); if(!S.base) exit(OVERFLOW); S.top = S.base + S.stacksize; S.stacksize += STACKINCREMENT; } *S.top++ = e; return OK; } int Pop(MazeStack &S, MazeType &e) { if(S.top == S.base) return ERROR; e = *-- S.top; return OK; } int StackEmpty(MazeStack &S) { if(S.top == S.base) return OK; else return ERROR; } int Pass(PosType &pos) //判断是否可以通过 { if(MazeMap[pos.y][pos.x] == 0) return ERROR; printf(" -> (%d , %d)",pos.y, pos.x); return TRUE; } void FootPrint(PosType pos){ //对已走过的路径标记2 MazeMap[pos.y][pos.x] = 2; } PosType NextPos(PosType nowpos, int &direction){//对下一个坐标的分析,如果没走过,那么退出执行下一个坐标的处理,否则回到原来的那个坐标,分析其他坐标 switch(direction){ case 1: ++nowpos.x; if(MazeMap[nowpos.y][nowpos.x] != 2) break; --nowpos.x; case 2: direction = 2; ++nowpos.y; if(MazeMap[nowpos.y][nowpos.x] != 2) break; --nowpos.y; case 3: direction = 3; --nowpos.x; if(MazeMap[nowpos.y][nowpos.x] != 2) break; ++nowpos.x; case 4: direction = 4; --nowpos.y; if(MazeMap[nowpos.y][nowpos.x] == 2){ //如果方向到南时这个坐标仍遍历过,那么我们置其为0,相当于堵死这个坐标 ++nowpos.y; MazeMap[nowpos.y][nowpos.x] = 0; } } return nowpos; } void MakePrint(PosType pos) { printf("\n(%d, %d)走不通,作废!", pos.y, pos.x); MazeMap[pos.y][pos.x] = 0; } int MazePath(PosType start, PosType end){ PosType nowpos; MazeStack S; MazeType e; int nowstep; nowpos = start; nowstep = 1; do{ if(Pass(nowpos)){ //当前坐标可通过的情况 FootPrint(nowpos); e.ord = nowstep; //记录走到该坐标的步数 e.seat = nowpos; //记录点的坐标 e.direction = 1; //记录此时的方向 Push(S, e); if(nowpos.x == end.x & nowpos.y == end.y) //到达终点 return OK; nowpos = NextPos(nowpos, e.direction); ++nowstep; } else{ if(!StackEmpty(S)){ //当前坐标为0的情况 while(e.direction == 4 & !StackEmpty(S)){ //当所有方向都已完成遍历一遍 MakePrint(e.seat); Pop(S, e); //退回上一个坐标 printf("倒退到:(%d, %d)", nowpos.y, nowpos.x); } if(e.direction < 4){ //当前坐标还有方向没有遍历 ++e.direction; Push(S,e); nowpos = NextPos(e.seat, e.direction); //入栈其他的方向。并对下一个坐标进行分析 } } } }while(!StackEmpty(S)); }
当然,栈最重要的一点仍然是递归。递归应用的就是栈的原理,我们通过栈来记录最近的一个状态,也就是现在的一个函数。如果我们通过递归进入本身的函数,那么视为入栈,当函数返回时,视为出栈,最后栈空,返回主函数。大概流程如此。
栈这个数据结构在算法中是十分重要的,因为他支持了DFS算法。以后写解题报告,我会提及这个数据结构的应用。