栈(上)之顺序栈

一、栈的定义
定义:栈(stack):栈是限定仅在表的一端进行插入或删除操作的线性表。
我们把允许插入和删除操作的一端称为栈顶(top),另一端称为栈底(bottom)。不含任何数据元素的栈称为空栈。栈又称为“后进先出(Last In First Out,简称LIFO)的线性表”,简称为LIFO结构。
栈的插入操作,称为进栈/入栈/压栈。
栈的删除操作,称为出栈/弹栈。
不过要注意的是,最先进栈的元素不代表最后出栈。栈对线性表的插入删除位置做了限制,但并没有对出栈和入栈的时间做限制。也就是说,在不是所有元素都入栈的情况下,事先入栈的元素也可以在任意时间出栈,只要保证每次出栈的元素都是栈顶元素就可以。
示例:有3个元素:1、2、3按顺序依次入栈,则我们可能得到以下出栈结果:
⒈1、2、3进,3、2、1出。得到结果为321
⒉1进、1出、2进、2出、3进、3出。得到结果为123
⒊1进、2进、2出、1出、3进、3出。得到结果为213
⒋1进、1出、2进、3进、3出、2出。得到结果为132
⒌1进、2进、2出、3进、3出、1出。得到结果为231
思考:能否得到结果为312的出栈序列?
答案:不可能,若3先出栈,则意味着1和2已入栈,此时2的出栈一定在1之前。
练习:已知一个栈的入栈顺序是a,b,c,d,e则不可能得到的出栈顺序是:
A、abcde
B、edcba
C、dceab
D、decba

二、栈的顺序存储结构及实现
1、顺序栈的结构定义
typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];
int top;//栈顶元素位置
}SqStack;
当top=-1时,该栈为空栈。当top=MAXSIZE-1时,该栈为满栈。
2、进栈操作Push
//代码见附录
3、出栈操作Pop
//代码见附录
对于进栈和出栈操作来说,二者都没有用到循环语句,因此时间复杂度为O(1)。
4、清空栈操作
清空一个栈的操作可以用以下方法实现:不断弹栈,直至栈内没有元素为止。
但是这样做实际上是没有必要的。若要清空一个栈,则意味着该栈内的元素已经“无用”了,这时不用再每个元素进行弹栈操作,而直接将栈顶的位置拉下至栈底即可,即:
top=-1;
这样,当新的元素再次压栈时,会覆盖掉原始的“无用”数据。
//代码见附录
三、栈的链式存储结构及实现
对于顺序栈来说,主要的缺点就是栈的大小已经固定,若有超过栈长的元素个数,则此时栈会发生“溢出”。这时我们可以采用链式栈的存储结构,这样就不用再考虑栈的空间是否足够大的问题。
1、链栈的结构定义
栈的链式存储结构,简称为链栈。
思考:对于栈的链式存储结构来说,栈顶指针是在链表头结点位置更好,还是在链表尾节点位置更好?
答:头结点位置更好
链表有头指针,而栈的主要操作也是在栈顶进行,那么我们就可以将二者合一,将单链表的头指针作为栈顶指针,即栈的链式存储结构的栈顶指针为单链表的头指针。

typedef struct StackNode
{
data_t data;
struct StackNode *next;
}LinkStack;
2、判定链式栈是否为空
对于链栈来说,基本不存在栈满的情况,除非内存中已没有可用空间。因此不考虑判定链式栈满的操作。
对于链栈来说,栈空的情况实际上就是判断top==NULL的情况。
//代码见附录
3、进栈操作Push
//代码见附录
4、出栈操作Pop
//代码见附录
对于进栈和出栈操作来说,二者都没有用到循环语句,因此时间复杂度为O(1)。
5、清空栈操作
//代码见附录
对比顺序栈与链栈,我们发现它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定长度,可能会存在内存空间浪费问题。但它的优势是存取时定位方便。而且链栈要求每个元素都有指针域,这同时也增加了内存开销,但对于栈的长度无限制。
综上所述,如果栈的使用过程中元素变化不可预料,有时数据量少有时却很大,则我们推荐使用链栈。反之,如果数据变化在可控范围内,则我们推荐使用顺序栈。
四、栈的应用
1、递归
递归:函数在自身的函数体内直接或间接地调用自身。
示例:递归法求斐波那契数列
int Fbi(int i)
{
if(i<2)
return i==0?0:1;
return Fbi(i-1)+Fbi(i-2);
}
int main()
{
int i;
for(i=0;i<40;i++)
printf("%d\t",Fbi(i));
printf("\n");
return 0;
}
要实现递归,必要的两个条件是递归出口和递归逻辑。在示例程序中,if(i<2)就是递归出口,而Fbi(i-1)+Fbi(i-2)就是递归逻辑。
//斐波那契数列的非递归解法略
对比递归代码和非递归(迭代)代码,我们可以看出递归和迭代的区别:迭代使用循环结构,而递归使用分支结构。
在某些程序中,递归能使得程序结构简洁清晰,容易理解。但是大量的调用递归函数会建立许多该函数的副本,需要大量的内存存储空间。而迭代法则无需大量的存储空间。
要想实现递归,我们需要明白递归的过程本质上是函数返回顺序是其调用顺序的逆序,即:先行调用的函数会在后面获得返回值。这种先行存储数据,并在之后逆序恢复得到数据的过程,显然很符合栈这种数据结构。因此,编译器使用栈来实现函数的递归。
在调用阶段,对于每层递归,函数的局部变量、参数、返回地址都被压入栈中,再去调用下次递归。在返回阶段,依次弹出位于栈顶的函数,获得计算结果。这也是为什么需要“递归出口”的原因,递归出口可以看做是从压栈到弹栈的状态转变因素。
2、后缀(逆波兰)表示法
对于数学运算来说,确定运算符的优先级是十分重要的,直接决定了该算式是否计算正确。在实际生活中,我们书写的算式都是中缀表达式,即运算符(此处特指算数运算符)在操作数中间。例如:
9+(3-1)*3+10/2
我们把这种平时使用的四则运算表达式的写法称为中缀表达式。但是对于计算机而言,中缀表达式并不方便。计算机计算都是从左到右顺序计算,在该算式中,*在+之后,但是却要先于+进行运算,而加入括号后,运算则会变得更加复杂。
对于四则运算,20世纪50年代,波兰逻辑学家Jan Lukasiewicz发明了一种不需要括号的表达式方法,称为后缀表示法,也称为逆波兰(Reverse Polish Notation,简称RPN)表示法。
对于上文的算式,使用后缀表示法为:
9 3 1 - 3 * + 10 2 / +
即运算符在两个操作数之后出现。
那么对于后缀表达法来说,计算机是怎样计算的呢?
后缀表达式的算法规则:从左到右遍历表达式,若遇到数字则进栈,遇到运算符则弹出栈顶两个元素进行运算,计算结果再次压栈,最后计算得到的结果就是最终结果。
我们以9 3 1 - 3 * + 10 2 / +进行讲解
⒈初始化一个空栈,此栈用于对要计算的操作数的进出及存储。
⒉9、3、1都是数字,因此依次入栈
⒊接下来是-,是符号,弹出栈顶两个元素作为操作数,注意先弹出的元素在符号右侧,后弹出的元素在符号左侧,即3 - 1,得到计算结果2,将2压栈。
⒋数字3进栈
⒌后面是*,栈顶两个元素弹栈进行运算 2 * 3,得到结果6,再压入栈
⒍后面是+,栈顶两个元素弹栈进行运算 9 + 6,得到结果15,再压入栈
⒎数字10和2进栈
⒏后面是/,栈顶两个元素弹栈进行运算 10 / 2,得到结果5,再压入栈
⒐最后一个符号是+,栈顶两个元素弹栈进行运算 15 + 5,得到结果20
⒑最终结果是20,栈变为空,结束运算。
那么,如何把中缀表达式转化为后缀表达式呢?
中缀表达式转化为后缀表达式的规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号则判断其与栈顶符号的优先级,是右括号或优先级低于或等于栈顶符号的则栈顶元素依次出栈并输出,直至遇到一个比其优先级低的运算符为止,并将当前符号进栈,一直到最终输出后缀表达式为止。
我们以9+(3-1)*3+10/2------>9 3 1 - 3 * + 10 2 / +为例进行讲解
1.初始化一个空栈,用于对符号进出栈使用。
2.第一个数字是9,输出9。后面的符号+入栈。
3.第三个字符是(,依然是符号,因其是左括号还未配对,故进栈。
4.第四个字符是数字3,输出,此时表达式为9 3,接着符号-进栈。
5.接下来是数字1,输出,此时表达式为9 3 1,后面是符号),此时我们需要把(之前的所有元素都出栈,直至输出(为止。此时总的表达式是9 3 1 -。
6.紧接着是符号*,因为此时的栈顶符号是+,优先级低于*,因此不输出,*进栈。紧接着是数字3,输出,总表达式为9 3 1 – 3.
7.之后是符号+,此时栈顶元素是*,比+优先级高,因此栈中元素出栈并输出(因为没有比+更低优先级的符号,所以全部出栈),总输出表达式为9 3 1 – 3 * +。然后将这个符号+进栈。
8.紧接着输出数字10,总表达式为9 3 1 – 3 * + 10。之后是符号/,所以/进栈。
9.最后一个数字为2,此时总表达式为9 3 1 – 3 * + 10 2。
10.因已到最后,所以将栈中符号全部出栈。最终获得的后缀表达式为9 3 1 – 3 * + 10 2 / +。
所以,对于计算机来说,输入中缀表达式,获得计算结果,最重要的有两步:
⒈将中缀表达式转化为后缀表达式
⒉计算后缀表达式得到计算结果
而这两步的整个过程都使用到了栈这种数据结构。

顺序栈代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 20
#define OK 1
#define ERROR 0

typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];
int top;//栈顶元素所在的数组下标位置
}SqStack;

int PushStack(SqStack *s,data_t e)//压栈
{
if(s->top==MAXSIZE-1)
{
printf("Stack is Full\n");
return ERROR;
}
s->top++;
s->data[s->top]=e;
return OK;
}

int PopStack(SqStack *s,data_t *e)//弹栈
{
if(s->top==-1)
{
printf("Stack is Empty\n");
return ERROR;
}
*e=s->data[s->top];
s->top--;
return OK;
}
SqStack* CreateEmptyStack()//创建栈
{
SqStack *stack = (SqStack*)malloc(sizeof(SqStack));
if(stack==NULL)
{
printf("CreateEmptyStack Error\n");
exit(0);
}
stack->top=-1;
return stack;
}
int EmptyStack(SqStack *s)//判断栈是否是空栈
{
return -1==s->top?OK:ERROR;
}
int FullStack(SqStack *s)//判断栈是否是满栈
{
return MAXSIZE-1==s->top?OK:ERROR;
}
int ClearStack(SqStack *s)//清空栈内元素
{
s->top=-1;
return OK;
}
int main()
{
/*
SqStack *stack = CreateEmptyStack();
data_t data;
PushStack(stack,20);
PushStack(stack,30);
PopStack(stack,&data);
printf("pop:%d\n",data);
PopStack(stack,&data);
printf("pop:%d\n",data);
PopStack(stack,&data);
printf("pop:%d\n",data);
if(EmptyStack(stack)==OK)
printf("Stack is Empty!\n");
if(FullStack(stack)==OK)
printf("Stack is Full!\n");
*/
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章