栈和队列,都是线性表的一种表现形式,首先先看栈.栈(stack)是限定仅在表尾进行插入和删除操作的线性表.我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈.栈又称为后进先出的线性表,简称为LIFO结构。

      栈的特殊之处在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行,这就使得:栈底是固定的,最先进栈的只能在栈底.栈的插入操作,叫做进栈,也称压栈,入栈.栈的删除操作叫做出栈.

      但是,这并不说明最先进栈的元素一定是最后出栈.别怀疑,我们举个例子来说明吧.假设有1,2,3三个数字要进栈,出来会有几种情况呢?

      第一种,也算是大家的第一反应:1,2,3进去,然后3,2,1出来,出栈次序是321,此时最先进栈的1是最后一个出来的.

      第二种,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.

      除了以上五种次序以外,思考下还会有其他出栈次序么?答案是没有,为什么呢?自己好好思考下.

      理论上,线性表的操作特性栈都具备,但是由于它的特殊性,所以在操作上会有些变更。比如栈的插入和删除操作,我们习惯命名为push和pop.接下来我们具体看看它的一些操作。

     首先看看它的定义:

View Code
 1 ADT 栈(stack)
2 Data
3 同线性表,元素具有相同类型,相邻元素具有前驱和后继关系。
4 Operation
5 InitStack(*S):初始化操作,建立一个空栈S
6 DestoryStack(*S):若栈存在,则销毁它.
7 ClearStack(*S):将栈清空.
8 StackEmpty(S):若栈为空,返回true,否则返回false.
9 GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
10 Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
11 Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值.
12 StackLength(S):返回栈S中的元素个数。
13 endADT

      栈是线性表的一种表现形式,那么,栈也一定会有顺序存储结构和链式存储结构之说。那么,先看看顺序存储结构吧.栈的顺序存储也叫做顺序栈.

      还记得线性表是用数组实现的吧,那么对于栈来说用数组哪一端做栈顶和栈底比较好?没错,下标为0的一端作为栈底,这样做可以让变化最小。来看看栈的结构定义:

View Code
1 typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
2
3 /* 顺序栈结构 */
4 typedef struct
5 {
6 SElemType data[MAXSIZE];
7 int top; /* 用于栈顶指针 */
8 }SqStack;

       若现在有一个栈,StackSize是5,则栈普通情况,空栈,栈满的情况如下图所示:

             

       可以看看栈的push操作的代码~~

View Code
 1 /* 插入元素e为新的栈顶元素 */
2 Status Push(SqStack *S,SElemType e)
3 {
4 if(S->top == MAXSIZE -1) /* 栈满 */
5 {
6 return ERROR;
7 }
8 S->top++; /* 栈顶指针增加一 */
9 S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
10 return OK;
11 }

       以及栈的pop操作代码~~

View Code
1 /* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
2 Status Pop(SqStack *S,SElemType *e)
3 {
4 if(S->top==-1)
5 return ERROR;
6 *e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
7 S->top--; /* 栈顶指针减一 */
8 return OK;
9 }

      从上面两段代码可以看出,它们的时间复杂度都是O(1).

      使用顺序栈有一个很大的缺陷,那就是必须事先确定叔祖存储空间大小,万一不够用,需要扩展数组的容量那是非常麻烦的一件事.其实,我们完全可以用一个数组来存储两个栈,数组的两个端点,两个栈有两个栈底,这样,我们就可以让一个栈的栈底为数组的始端,即下标0处.另一个栈的栈底为数组的末端,即下标为数组长度-1处.这样,如果两个栈要增加元素,就是两端点向中间做延伸.

     两栈共享空间的结构的代码:

View Code
1 /* 两栈共享空间结构 */
2 typedef struct
3 {
4 SElemType data[MAXSIZE];
5 int top1; /* 栈1栈顶指针 */
6 int top2; /* 栈2栈顶指针 */
7 }SqDoubleStack;

        对于两栈共享的push方法,我们除了要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stackNumber.插入元素的代码如下:

View Code
 1 /* 插入元素e为新的栈顶元素 */
2 Status Push(SqDoubleStack *S,SElemType e,int stackNumber)
3 {
4 if (S->top1+1==S->top2) /* 栈已满,不能再push新元素了 */
5 return ERROR;
6 if (stackNumber==1) /* 栈1有元素进栈 */
7 S->data[++S->top1]=e; /* 若是栈1则先top1+1后给数组元素赋值。 */
8 else if (stackNumber==2) /* 栈2有元素进栈 */
9 S->data[--S->top2]=e; /* 若是栈2则先top2-1后给数组元素赋值。 */
10 return OK;
11 }

     而对于两栈共享空间的pop方法,参数只是判断栈1栈2的参数stackNumber:

View Code
 1 /* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
2 Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
3 {
4 if (stackNumber==1)
5 {
6 if (S->top1==-1)
7 return ERROR; /* 说明栈1已经是空栈,溢出 */
8 *e=S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */
9 }
10 else if (stackNumber==2)
11 {
12 if (S->top2==MAXSIZE)
13 return ERROR; /* 说明栈2已经是空栈,溢出 */
14 *e=S->data[S->top2++]; /* 将栈2的栈顶元素出栈 */
15 }
16 return OK;
17 }

       当然,这只是针对两个具有相同数据类型的栈的一个设计上的技巧,如果是不相同数据类型的栈,这种方法不但不能更好的处理问题,反而会使问题变得更复杂。

      栈的链式存储结构称为链栈.对于链栈来说,头结点是没有必要了。而且,它基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,此时的计算机操作系统已经面临死机崩溃的情况,而不是链栈的溢出问题。链栈的结构代码如下:

View Code
 1 /* 链栈结构 */
2 typedef struct StackNode
3 {
4 SElemType data;
5 struct StackNode *next;
6 }StackNode,*LinkStackPtr;
7
8
9
10 typedef struct LinkStack
11 {
12 LinkStackPtr top;
13 int count;
14 }LinkStack;

       链栈的push操作,假设元素值为e的新结点是s,top为栈顶指针,代码如下:

View Code
 1 /* 插入元素e为新的栈顶元素 */
2 Status Push(LinkStack *S,SElemType e)
3 {
4 LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
5 s->data=e;
6 s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */
7 S->top=s; /* 将新的结点s赋值给栈顶指针,见图中② */
8 S->count++;
9 return OK;
10 }

     相对的pop操作,也是很简单的三句操作,假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,然后释放p就可以了:

View Code
 1 /* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
2 Status Pop(LinkStack *S,SElemType *e)
3 {
4 LinkStackPtr p;
5 if(StackEmpty(*S))
6 return ERROR;
7 *e=S->top->data;
8 p=S->top; /* 将栈顶结点赋值给p,见图中③ */
9 S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中④ */
10 free(p); /* 释放结点p */
11 S->count--;
12 return OK;
13 }

      经过代码分析,我们可以知道,它们的时间复杂度也是O(1),那么它们的使用情况就需要看它们在使用过程中是否元素变化不可预料。
      栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题的核心.这就是它的优势.栈在程序设计中,有一个很重要的应用就是递归.来看一个很典型的例子:Fibonacci数列:

   一般而言,兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔都不死,那么一年以后可以繁殖多少对兔子?

  我们不妨拿新出生的一对小兔子分析一下:

  第一个月小兔子没有繁殖能力,所以还是一对;

  两个月后,生下一对小兔总数共有两对;

  三个月以后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是三对;

  …… 

  依次类推可以列出下表:

         

     表中数字1,1,2,3,5,8---构成了一个序列。这个数列有关十分明显的特点,那是:前面相邻两项之和,构成了后一项。

     当我们需要打印出前40位斐波那契数列时,可以写出如下代码:

View Code
 1 int main()
2 {
3 int i;
4 int a[40];
5 printf("迭代显示斐波那契数列:\n");
6 a[0]=0;
7 a[1]=1;
8 printf("%d ",a[0]);
9 printf("%d ",a[1]);
10 for(i = 2;i < 40;i++)
11 {
12 a[i] = a[i-1] + a[i-2];
13 printf("%d ",a[i]);
14 }
15 printf("\n");
16 }

      如果我们用递归的方法实现:

View Code
 1 int Fbi(int i)  /* 斐波那契的递归函数 */
2 {
3 if( i < 2 )
4 return i == 0 ? 0 : 1;
5 return Fbi(i - 1) + Fbi(i - 2); /* 这里Fbi就是函数自己,等于在调用自己 */
6 }
7
8 int main()
9 {
10 printf("递归显示斐波那契数列:\n");
11 for(i = 0;i < 40;i++)
12 printf("%d ", Fbi(i));
13 return 0;
14 }

       那么,什么是递归呢?其实就是一个函数可以直接调用自己或者通过一系列调用语句间接地调用自己。而且每个递归定义至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出.对比以上两种方式,我们能看出迭代和递归是不一样的,迭代使用的是循环结构,而递归使用的是选择结构.递归能使程序的结构更清晰,更简洁,更容易让人理解,从而减少读懂代码的时间,但是大量使用递归调用会建立函数的副本,消耗大量时间和内存.因此我们应该根据情况使用它们.

posted on 2011-09-05 12:06  Jeallyn  阅读(350)  评论(0编辑  收藏  举报