数据结构与算法(四):栈

472 阅读5分钟

栈和队列是软件设计中常用的两种数据结构,它们的逻辑结构和线性表相同。其特点在于运算受到了限制:栈按“后进先出”或“先进后出”的规则进行操作,队列按“先进先出”的规则进行操作,故称它们为操作受限制的线性表。

栈(Stack)是限制在表的一端进行插入和删除操作的线性表。允许进行插入、删除操作的这一端称为栈顶(Top),另一个固定端称为栈底。当表中没有元素时称为空栈。

顺序栈

利用顺序存储方式实现的栈称为顺序栈。类似于顺序表的定义,栈中的数据元素用一个预设的足够长度的一维数组来实现:datatype data[MAXSIZE],栈底位置可以设置在数组的任意一个端点,而栈顶是随着插入和删除而变化的,用int top来作为栈顶的指针,指明当前栈顶的位置,同样将data和top封装在一个结构中。

顺序栈的操作

栈的结构定义

#define MaxSize 20
typedef int ElemType;

typedef struct {
    ElemType data[MaxSize];
    int top;//栈顶
}Stack;

1.创建空栈

Status InitStack(Stack *S){
    S->top = -1;
    return OK;
}

2.判断是否为空栈

BOOL StackEmpty(Stack *S){
    if (S->top == -1) {
        return true;
    }
    return false;
}

3.入栈

Status PushData(Stack *S, ElemType e){
    //满了 不能入栈
    if (S->top == MaxSize -1)  return Error;
 
    S->top ++;
    S->data[S->top] = e;
    return OK;
}

4.出栈

Status PopData(Stack *S, ElemType *e){
    //空栈 不能出栈
    if (S->top == -1) return Error;
 
    *e = S->data[S->top];
    S->top --;
    return OK;
}

5.取栈顶

Status GetTop(Stack S, ElemType *e){
    if (S.top == -1)    return Error;
     
    *e = S.data[S.top];
    return OK;
}

链栈

用链式存储结构实现的栈称为链栈。通常链栈用单链表表示,因此其结点结构与单链表的结点结构相同。

栈的结构定义

typedef struct StackNode
{
    SElemType data;
    struct StackNode *next;
}StackNode,*LinkStackPtr;

typedef struct
{
    LinkStackPtr top;//栈顶
    int count;//长度
}LinkStack;

1.创建空栈

Status InitStack(LinkStack *S)
{
    S->top=NULL;
    S->count=0;
    return OK;
}

2.判断是否为空栈

Status StackEmpty(LinkStack S){
    if (S.count == 0)
        return TRUE;
    else
        return FALSE;
}

3.入栈

Status Push(LinkStack *S, SElemType e){
    
    //创建新结点temp
    LinkStackPtr temp = (LinkStackPtr)malloc(sizeof(StackNode));
    //赋值
    temp->data = e;
    //把当前的栈顶元素赋值给新结点的直接后继;
    temp->next = S->top;
    //将新结点temp 赋值给栈顶指针;
    S->top = temp;
    S->count++;
    return OK;
}


4.出栈

Status Pop(LinkStack *S,SElemType *e){
    LinkStackPtr p;
    if (StackEmpty(*S)) {
        return ERROR;
    }
    
    //将栈顶元素赋值给*e
    *e = S->top->data;
    //将栈顶结点赋值给p
    p = S->top;
    //使得栈顶指针下移一位, 指向后一结点.
    S->top= S->top->next;
    //释放p
    free(p);
    //个数--
    S->count--;
    return OK;
}

5.取栈顶

Status GetTop(LinkStack S,SElemType *e){
    if(S.top == NULL)
        return ERROR;
    else
        *e = S.top->data;
    return OK;
}

栈与递归

栈的另一个非常重要的应用就是在程序设计语言中实现递归。递归是指在定义自身的同时又出现对自身的引用。

实现函数调用,系统需要做三件事:

(1)保留调用函数本身的参数与返回地址;
(2)为被调用函数的局部变量分配存储空间,并给对应的参数赋值;
(3)将程序控制转移到被调用函数的入口。

从被调用函数返回到调用函数之前,系统也应完成三件事:

(1)保存被调用函数的计算结果,即返回结果;
(2)释放被调用函数的数据区,恢复调用函数原先保存的参数;
(3)依照原先保存的返回地址,将程序控制转移到调用函数的相应位置。

在实现函数调用时,应按照“先调用的后返回”原则处理调用过程,因此上述函数调用时函数之间的信息传递和控制转移必须通过栈来实现。在实际实现中,系统将整个程序运行所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区(压栈),而当从一个函数退出时,就释放它的存储区(出栈)。显然,当前正在运行的函数的数据区必然在栈顶。

在一个递归函数的运行过程中,调用函数和被调用函数是同一个函数,因此,与每次调用时相关的一个重要概念就是递归函数运行的“层次”。假设调用该递归函数的主函数为第0层,则从主函数调用递归函数为进入第1层,从第1层再次调用递归函数为进入第2层,以此类推,从第i层递归调用自身则进入“下一层”,即第i+1层。反之,退出第i层则应返回至“上一层”,即i-1层。为了保证递归函数正确执行,系统需要设立一个递归工作栈作为整个递归函数执行期间的数据存储区。每层递归所需信息构成一个工作记录,其中包括递归函数的所有实参和局部变量,以及上一层的返回地址等。每进入一层递归,就产生一个新的工作记录压栈;每退出一层递归,就从栈顶弹出一个工作记录释放。因此当前执行层的工作记录必为栈顶元素,称该记录为活动记录,并称指示活动记录的栈顶指针为环境指针。

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)。

代码实现

int Fib (int n){
    if (n==0)  return 0;
    else  if (n==1)  return 1;
    else  return  Fib(n-1) + Fib(n-2);
}

计算Fib( n )的递归函数在n>1时的执行过程大致可分为五个阶段:

(1)调用Fib ( n-1 ),即进栈操作;
(2)返回Fib ( n-1 )的值,即出栈操作;
(3)调用Fib ( n-2 ),再次进栈;
(4)返回Fib ( n-2 )的值,出栈;
(5)计算Fib ( n )的值,然后返回。