
2.2 后进先出结构:栈
在程序设计中,栈是一种很常用的数据结构,也是一种重要的线性结构,它是线性表的特殊表现形式。栈必须通过顺序表或链表来实现。本节将介绍栈的特点和操作栈的C语言代码。
2.2.1 栈的概念
栈是一个后进先出的线性表,它要求只在表尾进行删除和插入等操作。所以,栈其实就是一个线性表(顺序表、链表),不过,它在操作上有一些特殊的要求和限制。第一,栈的元素必须先进后出,这与一般的顺序表不同;第二,栈的操作只能限定在这个顺序表的表尾进行。
日常生活中有很多栈的表现形式,例如,当仓库中堆码货物时,先来的货物放在下面,后来的货物码放在上面,而要取出货物时,总是先取上面的(即后放入的先取出),最后才能取到放在下面的货物。
注意 栈按照“后进先出”(Last In First Out,LIFO)的原则处理数据。
栈结构如图2-12所示,在栈中只能在一端进行操作(即保存和取出数据都只能从线性表的一端进行),该操作端称为栈顶,另一端称为栈底。

图2-12 栈示意图
提示 在很多教材中都将“栈”称为“堆栈”,其实,“栈”和“堆”是两个不同的概念,你可以简单理解为:栈是有序的,而堆是无序的。
在栈中,只有栈顶元素是可以访问的。栈的基本操作只有两个:
·入栈(Push):即将数据保存到栈顶。进行该操作前,先修改栈顶指针,使其向上移一个元素位置,然后将数据保存到栈顶指针所指的位置。
·出栈(Pop):即将栈顶的数据弹出,然后修改栈顶指针,使其指向栈中的下一个元素。
·栈是一种特殊的线性表,因此,栈也可以使用两种存储表示方法。
·顺序栈:即使用一组地址连续的内存单元依次保存栈中的数据。一般是定义一个指定大小的数组来作为栈,序号为0的元素就是栈底,再定义一个变量top保存栈顶的序号即可。
·链式栈:使用链表形式保存栈中各元素的值,链表尾部(指向地址为NULL)为栈底,链表首部(head指针所指向元素)为栈顶。
下面介绍顺序栈的实现方式,链式栈的实现代码与此类似,读者可自行编写相关代码。
2.2.2 操作栈
对于顺序栈,在程序中一般是使用数组来保存栈中各元素,同时还需设置一个变量top,在该变量中保存一个数组元素的序号,用来表示栈顶位置。
下面介绍C语言操作栈的代码。
1.定义顺序栈的结构
【程序2-8】操作栈的头文件“2-8 SeqStack.h”
1 typedef struct stack //定义顺序栈结构体 2 { 3 DATA data[SIZE+1]; //数据元素 4 int top; //栈顶 5 }SeqStack;
【程序说明】
以上代码定义栈的结构,其中类型DATA表示栈中元素的数据类型,需在主调程序定义(可定义为简单数据类型,也可定义为结构体),在定义数组data时,使用了符号常量SIZE表示栈的最大容量,该符号常量需在主调程序中设置(也可以在当前文件中设置,在主调程序中设置的好处是不用再修改该头文件的内容,以适应各种不同的情况)。
注意 因为C语言的数组下标是从0开始的,为了与日常习惯相同,栈的数据从下标1开始保存(当top为0时表示栈为空),因此,定义数组data的大小为SIZE+1。
整型变量top用来保存栈顶的序号,当top=0时表示栈为空,当top=SIZE时表示栈满。
2.初始化栈
顺序栈的初始化操作需要做两件事:首先按符号常量SIZE指定的大小申请一片内存空间,用来保存栈中的数据;然后设置栈顶指针的值为0,表示是一个空栈。具体代码如下;
6 SeqStack *SeqStackInit() //初始化栈 7 { 8 SeqStack *p; 9 if(p=(SeqStack *)malloc(sizeof(SeqStack))) //申请栈内存 10 { 11 p->top=0; //设置栈顶为0 12 return p; //返回指向栈的指针 13 } 14 return NULL; //申请内存失败, 返回空值 15 }
【程序说明】
第9行申请内存,第11行设置栈顶为0,然后返回申请内存的首地址。如果申请内存失败,将返回NULL。因此,在主调程序中应判断初始化是否成功,然后再进行后续操作。
因为栈使用的内存空间是通过malloc函数分配的,因此,在不使用栈时应调用free函数释放所分配的内存。该函数代码如下:
16 void SeqStackFree(SeqStack *s) //释放栈所占用空间 17 { 18 if(s) 19 free(s); 20 }
编程经验 在不使用栈时,应显式调用free函数释放内存,这是一种好的编程习惯。当程序终止运行时,系统会将所有内存释放,但如果程序一直在运行,且在不断申请内存,则最终可能会导致内存不足的错误。
3.判断栈的状态
在对栈进行操作之前通常需要先判断栈的状态,再决定是否进行操作,例如,入栈前应先判断栈是否已满,然后再决定是否进行入栈操作。而在出栈时应先判断栈是否已经为空,若已经为空,则没有可出栈的数据。下面的这些函数用来进行栈状态的判断。
21 int SeqStackIsEmpty(SeqStack *s) //判断栈是否为空 22 { 23 return(s->top==0); 24 } 25 void SeqStackClear(SeqStack *s) //清空栈 26 { 27 s->top=0; 28 } 29 int SeqStackIsFull(SeqStack *s) //判断栈是否已满 30 { 31 return(s->top==SIZE); 32 }
【程序说明】
·第21~24行是判断栈是否为空的函数,根据栈顶指针top是否为0,判断栈是否为空。
·第25~28行是清空栈的函数,设置栈顶指针top为0,清空栈。
·第29~32行是判断栈是否已满的函数,判断栈顶指针top是否已等于符号常量SIZE,从而判断栈是否已满。
4.入栈操作
入栈和出栈操作是栈的基本操作,入栈(Push)操作是将数据元素保存到栈中,具体的算法如下:
(1)首先判断top是否大于等于SIZE,若是,则给出溢出信息,做出错处理(不能再入栈)。
(2)设置top=top+1(栈顶指针加1,指向入栈地址)。
(3)将入栈元素保存到top指向的位置。
根据该算法编写代码如下:
33 int SeqStackPush(SeqStack *s,DATA data) //入栈操作 34 { 35 if((s->top+1)>SIZE) //栈顶上一个位置超过最大位置 36 { 37 printf("栈溢出!\n"); //显示出错 38 return 0; 39 } 40 s->data[++s->top]=data; //将元素入栈 41 return 1; 42 }
【程序说明】
该函数有两个参数,参数s是一个指向操作的栈的指针,参数data是需要入栈的数据元素。第35行表示栈若溢出,则不进行入栈操作,否则执行第40行修改栈顶指针的值,再将元素入栈。
5.出栈操作
出栈(Pop)操作与入栈相反,是从栈顶弹出一个数据元素,具体的算法如下。
(1)首先判断top是否小于等于0,若是,则给出栈为空信息,做出错处理(没有数据出栈)。
(2)将栈顶指针top所指位置的元素返回。
(3)设置top=top-1(使栈顶指针减1,指向栈的下一个元素,原来栈顶元素被弹出)。
根据该算法编写代码如下:
43 DATA SeqStackPop(SeqStack *s) //出栈操作 44 { 45 if(s->top==0) //判断栈是否为空 46 { 47 printf("栈为空!"); 48 exit(0); 49 } 50 return (s->data[s->top--]); //弹出栈顶元素 51 }
【程序说明】
·该函数只有一个参数,s是一个指向操作的栈的指针。函数返回值为一个DATA类型的数据,返回值是栈顶的数据元素。在主调程序中需定义一个DATA类型的变量来接收该返回值。
·第50行首先返回top所指位置的元素,然后修改top的值,使其指向栈中下一个元素,达到抛弃原来栈顶元素的效果(即弹出)。
提示 出栈时必须判断栈是否为空,若不判断而直接弹出栈顶元素,并修改栈顶指针,将得到一个错误的数据,并使栈顶指针指向不明内存区域,可能会让系统出错。
6.获取栈顶元素
使用出栈函数操作后,原来栈顶的元素就被抛弃(弹出)了,也就是说,出栈后原来栈顶的元素就不存在了。在有的情况下,可能需要获取栈顶元素,但又还需要继续保留该元素在栈顶,这时就需要使用获取栈顶元素的函数,具体代码如下:
52 DATA SeqStackPeek(SeqStack *s) //读栈顶元素 53 { 54 if(s->top==0) //判断栈是否为空 55 { 56 printf("栈为空!"); 57 exit(0); 58 } 59 return (s->data[s->top]); //返回栈顶元素 60 }
该函数与SeqStackPop函数的代码很相似,只是第59行不同,在该行中只返回栈顶元素,并不执行修改栈顶指针的操作,所以返回栈顶元素后,原来的栈顶元素仍然存在。
7.测试栈的操作
编写好栈的操作函数后,接下来编写一个简单的测试程序,测试栈的操作过程。具体代码如下。
【程序2-9】测试栈的操作文件“2-9 SeqStackTest.c”
1 #include <stdio.h> 2 #include <stdlib.h> //引入分配内存的头文件 3 #define SIZE 50 //栈的大小(保存元素的数量) 4 typedef struct 5 { 6 char name[15]; 7 int age; 8 }DATA; //定义入栈元素 9 #include "2-11 SeqStack.h" //引入自定义的作函数文件
【程序说明】
以上代码中,第3行定义栈中能保存的元素数量,第4~8行定义DATA类型,表示栈中每次保存的元素,本例中定义为一个结构体,保存两个字段(姓名和年龄),第9行包含对栈操作的头文件(本节前面编写的代码)。
接下来编写主函数,具体代码如下:
10 int main() 11 { 12 SeqStack *stack; 13 DATA data,data1; //定义栈操作的元素 14 stack=SeqStackInit(); //初始化栈 15 printf("入栈操作:\n"); 16 printf("输入姓名 年龄进行入栈操作:"); 17 scanf("%s%d",data.name,&data.age); 18 SeqStackPush(stack,data); //入栈 19 printf("输入姓名 年龄进行入栈操作:"); 20 scanf("%s%d",data.name,&data.age); 21 SeqStackPush(stack,data); //入栈 22 printf("\n出栈操作:\n按任意键进行出栈操作:"); 23 getch(); //把程序暂停下来,等待用户的输入 24 25 data1=SeqStackPop(stack); //出栈 26 printf("出栈的数据是(%s,%d)\n" ,data1.name,data1.age); //显示出栈数据 27 printf("再按任意键进行出栈操作:"); 28 getch(); 29 30 data1=SeqStackPop(stack); //出栈 31 printf("出栈的数据是(%s,%d)\n" ,data1.name,data1.age); //显示出栈数据 32 SeqStackFree(stack); //释放栈所占用的空间 33 getch(); 34 return 0; 35 }
以上代码比较简单,不再逐句讲解。这些代码主要完成了两次入栈和两次出栈的操作。编译执行代码,按提示输入相关信息,如图2-13所示。

图2-13 【程序2-9】执行结果
执行【程序2-9】的过程如图2-14所示,具体过程如下:
第14行初始化栈时,从内存中申请一片空间,设置top的值为0,指向栈底,此时栈为空,如a图所示。
执行第18行将输入的数据入栈,此时栈顶变量top的值为1,如图b所示。
执行第21行将输入的数据入栈,此时栈顶变量top的值为2,如图c所示。
执行第25行进行出栈操作,此时将栈顶数据返回,并修改top的值,使其由原来的2变为1。这样就将栈顶元素丢弃(其实,该数据可能仍然保存在内存中,再次执行入栈操作时,就会将其覆盖),如图d所示。
执行第30行进行出栈操作,此时将栈顶数据返回,并修改top的值,使其由原来的1变为0。此时,top指向了栈底,表示栈为空,如图e所示。
注意 由图2-14可看到出栈后数据仍然在内存中,只是栈顶top指向下面,使其数据不可访问,当有数据再次入栈时才会覆盖原来的数据。

图2-14 入栈、出栈操作
2.2.3 实例:算术表达式求值
计算机程序设计中,很多地方都需要使用栈。例如,在调用函数时,就需要将函数的返回地址、主调函数各变量的值等数据入栈。当调用函数执行结束时,再从栈中弹出变量的值以及返回地址,以恢复主调程序的继续执行。
另外,在所有编译系统中都必须提供的算术表达式求值功能,也是通过栈来实现的。下面以表达式求值为例介绍栈的实际应用。
对于算术表达式的求值,主要就是解决算术运算符的优先级问题,有以下规则:
·先进行乘除运算,再进行加减运算(乘除优先级大于加减);
·对于相同优先级的运算符,从左向右计算;
·若要改变优先级,可使用括号。对有括号的表达式,先计算括号内,再计算括号外。
任何算术表达式都是由操作数和运算符组成的,为了简化程序,在本例中只考虑加减乘除这4种算术运算,允许添加括号。这样,4种运算符和2个括号(左括号、右括号)就构成了6种运算符。在一个表达式中,需要根据两个运算符的优先级来决定先计算哪一部分。例如,有以下表达式:
(2+3)*5
很容易就知道先算括号中的“2+3”,再将其和乘以5。但是,计算机怎么知道该这样计算呢?从计算机角度出发,只能逐个扫描表达式字符串中的字符,例如,对于上面的算式,有下面的扫描过程:
首先扫描到的字符是左括号“(”,这时还不知道右括号在哪里,因此,需要将左括号保存起来(入栈)。
接着继续扫描下一个字符“2”,也不知道该字符需要和后面的数据进行什么操作(加、减、乘、除或和下一个数字组成一个更大的数)。
继续扫描下一个字符“+”,这时就知道是用数字2加上某一个数,但具体加什么数还不知道,因此,需要将加号“+”保存起来(入栈)。
继续扫描下一个字符“3”,这时就知道是2+3,是否就直接将其相加了呢?还不行,因为如果后面出现乘或除,应该要先计算,因此,需要将数字3保存起来(入栈)。
继续扫描下一个字符“)”,这时,就应该将前面保存的2+3取出来(出栈)计算,得出结果。
在上面的过程中,遇到右括号“)”,则计算前面加号“+”的内容,表示右括号的优先级低于加号。
如何让计算机知道这些预定义的优先级呢,这就需要预先将定义的规则编写成相应的代码进行判断。假设有以下两个操作符构成的算式:
data1 O1 data2 O2 data3
其中,data1、data2、data3表示3个操作数,O1、O2表示两个操作符,如果O1的优先级大于等于O2,则直接从左向右计算,若O1的优先级小于O2,则需要先对右侧data2和data3进行计算。
各算术运算符之间的优先级如表2-1所示。
表2-1 运算符的优先级

在表2-1中,数字“1”表示O1优先级大于等于O2,“-1”表示O1的优先级小于O2,“0”表示左括号遇到了与其配对的右括号,这时不需要计算。
按照表2-1所列规则,就可直接编写代码判断两个运算符的优先级了。
提示 从前面的分析可知,在表达式的计算过程中,既要保存操作数,又要保存运算符。这时,可定义两个栈,一个用来保存操作数,一个用来保存运算符。算法实现过程如图2-15所示。
根据图2-15所示算法编写代码。

图2-15 表达式求值流程图
【程序2-10】表达式求值程序文件“2-10 CalcExp.c”
1 #include<stdio.h> 2 #include<stdlib.h> 3 #define SIZE 50 4 typedef int DATA; //定义栈元素类型 5 #include "2-8 SeqStack.h"
【程序说明】
在计算表达式时,假设每个操作数都是一个整数,因此,在第4行将DATA定义为一个int类型,如果是其他数据类型,则需要修改该行的定义。第5行包含操作栈的头文件。
从图2-15可看到,程序中每次读取的字符都需要判断是否为运算符,因此,单独编写一个函数用来判断,具体代码如下:
6 int IsOperator(char c) //检查字符是否为运算符 7 { 8 switch(c) 9 { 10 case'+': 11 case'-': 12 case'*': 13 case'/': 14 case'(': 15 case')': 16 case'=': 17 return 1; 18 break; 19 default: 20 return 0; 21 break; 22 } 23 }
【程序说明】
该函数比较简单,如果字符c是运算符则返回1,否则返回0。从程序中第10~16行可看到,程序中只将+、-、*、/、(、)、=判断为运算符。
提示 在计算表达式时,为了方便判断,增设了一个等号“=”作为表达式的定界符,即表达式以“=”开始,以“=”结束(当然,也可选择其他字符来定界)。
在计算表达式的值时,需要比较前后两个运算符的优先级,按照表2-1所示的规则编写以下函数,用来判断两个运算符的优先级。
24 int PRI(char oper1,char oper2) //判断两个运算符的优先级 25 //oper1>oper2返回1 26 //oper1<oper2返回-1 27 //oper1、oper2分别为左、右括号返回0 28 { 29 int pri; 30 switch(oper2) //判断运算符优先级 31 { 32 case '+': 33 case '-': 34 if(oper1=='(' | |oper1=='=') //为左括号或表达式开始符号 35 pri=-1; // oper1<oper2 36 else 37 pri=1; //oper1>oper2 38 break; 39 case '*': 40 case '/': 41 if(oper1=='*'||oper1=='/'||oper1==')') 42 pri=1; //oper1>oper2 43 else 44 pri=-1; //oper1<oper2 45 break; 46 case '(': 47 if(oper1==')') //右括号右侧不能马上出现左括号 48 { 49 printf("语法错误!\n"); 50 exit(0); 51 }else 52 pri=-1; //oper1<oper2 53 break; 54 case')': 55 if(oper1=='(') 56 pri=0; //都有号配对, 返回0 57 else if (oper1=='=') 58 { 59 printf("括号不匹配!\n"); 60 exit(0); 61 }else 62 pri=1; //oper1>oper2 63 break; 64 case '=' : 65 if(oper1=='(') 66 { 67 printf("括号不匹配!\n"); 68 exit(0); 69 }else if(oper1=='=') 70 pri=0; //等号配对, 返回0 71 else 72 pri=1; // oper1>oper2 73 break; 74 } 75 return pri; 76 }
该函数中,根据传入的两个运算符进行判断,得到不同的优先级。在主调函数中再根据优先级决定是入栈还是计算。
对于优先级高的运算符,需将运算符两侧的数据进行计算,因此,编写以下函数用来根据运算符和两个操作数进行计算。
77 int Calc(int a,int oper,int b) //计算两个操作数的结果 78 { 79 switch(oper) //根据运算符计算 80 { 81 case'+':return a+b; 82 case'-': return a-b; 83 case'*': return a*b; 84 case'/' //对除法要考虑除以0错误 85 if(b!=0) //除数不能为0 86 return a/b; 87 else 88 { 89 printf("除以0溢出!\n"); 90 exit(0); 91 } 92 } 93 }
编程经验 该函数代码简单,对于除法需要考虑除以0错误的问题,这是所有算术运算都需要考虑的。
编写好这些辅助函数,就可以编写计算表达式值的函数了,具体代码如下:
94 int CalcExp(char exp[]) //表达式计算函数 95 { 96 SeqStack *StackOper,*StackData; //指向两个栈的指针变量 97 int i=0,flag=0; //flag作标志, 用来处理多位数 98 DATA a,b,c,q,x,t,oper; 99 100 StackOper=SeqStackInit(); //初始化两个栈 101 StackData=SeqStackInit(); 102 103 q=0; 104 x='='; 105 SeqStackPush(StackOper,x); //首先将等号(=)进入操作符栈 106 //x=SeqStackPeek(StackOper); //获取操作符栈的首元素 107 c=exp[i++]; 108 while(c!='=' || x!='=') //循环处理表达式中的每个字符 109 { 110 if(IsOperator(c)) //若输入的是运算符 111 { 112 if(flag){ //操作数未入栈 113 SeqStackPush(StackData,q); //将操作数入栈 114 q=0; //操作数清0 115 flag=0; //标志清0, 表示操作数已入栈 116 } 117 switch(PRI(x,c)) //判断运算符的优先级 118 { 119 case -1 //当前运算符大于前一运算符 120 SeqStackPush(StackOper,c); //运算符进栈 121 c=exp[i++]; 122 break; 123 case 0 //括号配对 124 c=SeqStackPop(StackOper); //运算符(括号、等号)出栈(抛弃) 125 c=exp[i++]; //取表达式下一字符 126 break; 127 case 1: 128 oper =SeqStackPop(StackOper); //运算符出栈 129 b=SeqStackPop(StackData); //两个操作数出栈 130 a=SeqStackPop(StackData); 131 t=Calc(a, oper,b); //计算结果 132 SeqStackPush(StackData,t); //将运算结果入栈 133 break; 134 } 135 }else if(c>='0'&&c<='9') //若输入字符在0~9之间 136 { 137 c-='0'; //将字符转换为对应的数值 138 q=q*10+c; //多位数的进位处理 139 flag=1; //设置标志, 表示操作数未入栈 140 c=exp[i++]; //取表达式下一字符 141 } 142 else 143 { 144 printf("输入错误!\n"); 145 getch(); 146 exit(0); 147 } 148 x=SeqStackPeek(StackOper); //获取栈顶的运算符 149 } 150 q=SeqStackPop(StackData); 151 SeqStackFree(StackOper); //释放栈所占用的空间 152 SeqStackFree(StackData); 153 return q; //出栈, 返回结果 154 }
【程序说明】
该函数代码较多,下面逐个介绍各部分的作用。
·第94行定义函数头,该函数需要一个字符串作为参数,返回值是一个整型数(表达式的计算结果)。
·第97行定义了一个标志变量flag,该标志变量用来处理多位数的情况。因为下面的代码一次只从表达式字符串中获取1个字符,如果操作数为多位数,则需要将该数的每一位获取后再入栈,因此使用标志变量flag,当其值为0时,表示没有操作数要入栈,当其值非0时,表示有操作数需要入栈。具体用法看下面的代码。
·第100、101行初始化两个栈,分别用来保存运算符和操作数。
·第103行初始化变量q为0,变量q用来保存多位的操作数。
·第104、105行初始化变量x为等号“=”,并将该符号作为表达式的第一个运算符入栈。
·第107行从表达式字符串中获取第1个字符,开始扫描表达式的操作。
·第108~149行为一个循环,在该循环中将逐个处理表达式字符串中的每个字符,直到遇到表达式结束字符“=”为止。
·第110~134行处理获取的字符为运算符的情况。
·首先执行第112~116行,如果flag为非0,表示有操作数未入栈,先将操作数入栈,再设置标志flag为0。
·第117~134行处理不同优先级运算符的情况,若当前运算符(保存在变量c中)比前一个运算符(保存在变量x中)优先级大(PRI函数返回的值为-1),则在第120行将运算符入栈,再获取表达式的下一个字符进行处理。若PRI函数返回的值为0,表示括号配对,这时只需将运算符栈中的左括号弹出(第124行)即可,然后再获取表达式的下一个字符进行处理。若PRI函数返回值为1(前一运算符优先级大),则需要将前一运算符弹出(第128行),再将前两个操作数弹出(第129、130行),再调用Calc函数进行计算(第131行),然后将计算结果入栈(第132行)。这时需要注意,变量c中还保存着一个运算符未处理,因此,不能再去处理表达式中的下一字符,而是继续判断变量c中保存的运算符和栈中前一个运算符的优先级(即到第148行获取运算符栈的栈顶元素,然后又回到循环的初始位置)。
·如果读入的字符不是运算符,第135~141行判断是不是数字(即字符0~9),若为数字,则执行第137~140行的操作,第137行将字符(这里是ASCII码)转换为对应的数值,第138行累加多位数(将以前数值乘10,当前位累加到个位),第139行设置标志flag为非0值,表示有数据还没压入操作数栈,第140行获取表达式的下一字符继续进行处理。
·如果读入的字符既不是运算符,也不是0~9的数字,则执行第142~147行提示错误。
·第148行从运算符栈的栈顶获取上一个运算符,方便第117行比较两个运算符的优先级,以进行不同运算。
·当处理完表达式中所有字符串后,最终结果保存在操作数栈中,第150行将其弹出保存到变量q中,第153行返回该变量的值。
·第151、152行释放两个栈所占用的内存空间。
提示 表达式求值函数的计算过程有点复杂,因此解释得比较详细,读者可结合图2-15理解这部分代码。
编写好CalcExp函数,主函数就很简单了。在主函数中只需提示用户输入一个表达式字符串,然后调用CalcExp函数即可。主函数可编写为以下样式:
155 int main() 156 { 157 int c; 158 char exp[80]; 159 printf("请输入要计算的表达式(以=结束):"); 160 scanf("%s",exp); 161 printf("%s%d\n",exp,CalcExp(exp)); 162 getch(); 163 return 0; 164 }
这部分代码简单,就不再逐句介绍了。
编译执行以上代码,输入一个表达式,可得到该表达式的值,如图2-16所示。

图2-16 【程序2-10】执行结果