栈结构初探

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
> — 百度百科

栈结构是数据结构中相对来说比较简单的一个结构了,当然,栈也是属于逻辑结构中的线性表结构,只不过它是操作受限的。什么意思呢?我们前面在探索线性表的时候都知道,我们可以在线性表的任意位置插入和删除元素。但是对于栈结构来说,只能在栈顶执行相关的操作。用生活中的例子来举例的话: 就是一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。

基于顺序存储和链式存储设计一个栈结构 - 图1

所以说,从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

对于第一次接触栈这种数据结构的同学来说,可能会有一定的疑惑,因为相比于数组和链表,栈带来的更多的是限制。事实上,从功能上来说,数组或者链表确实可以替代栈。但是特定的数据结构是针对于特定的场景的抽象,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。

如何实现顺序栈

从前面栈的定义中可以得出,栈主要包含两个操作,入栈出栈。也就是在栈顶插入一个数据和从栈顶删除一个数据。实际上,栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,叫做顺序栈,用链表实现的栈,叫做链式栈。下面我们就来一一实现。

按照国际惯例,我们还是要实现定义一些宏和辅助的变量,代码如下:

  1. #define OK 1
  2. #define ERROR 0
  3. #define TRUE 1
  4. #define FALSE 0
  5. #define MAXSIZE 20 /* 存储空间初始分配量 */
  6. typedef int Status;
  7. typedef int SElemType;

因为我们使用的是 C 语言来实现顺序栈,所以需要定义一个结构体类型,这里我们就定义一个 SqStack 的结构体类型,代码如下所示:

  1. typedef struct {
  2. SElemType data[MAXSIZE];
  3. int top;
  4. }SqStack;

通过上面的代码可以看出,我们在顺序栈结构体中定义了两个属性,一个是栈顶指针 top ,注意,这里和指针变量是两个概念。然后是一个 SElemType 类型的数组 data ,我们针对栈的入栈和出栈操作就是针对于这个数组。

好的,定义完了基本的数据结构之后,我们来捋一下我们需要实现的一些栈相关的基本的操作。(这里读者可以自己思考一下再继续往下阅读本文。)

按照我们之前探索数组和链表的惯例,我们需要先初始化一个空的栈,然后需要一个方法来判断栈是否为空,接着我们要能有一个方法可以清空顺序栈。最后需要有最重要的两个方法来实现入栈和出栈。因为这里的逻辑比较简单,所以接下来我们直接上代码。

初始化一个顺序栈

  1. Status InitStack(SqStack *S)
  2. {
  3. S->top = -1;
  4. return OK;
  5. }

这里有一个注意点,就是当初始化一个空的栈的时候,我们这个栈的 top 指针是指向 -1 的,这里的 -1 并不要求是固定的值,只是我们设置的一个标志位,因为当栈不为空的时候,top 指针指向的其实是 data 数组中存储的最后一个值的下标位置。这里之所以并没有开辟内存空间,还记得我们在声明一个顺序结构存储的数组吗,我们用到了指向整型数组的指针,而指针本身在初始化之前是一个野指针,所以必须要通过 malloc 或 calloc 来分配一段内存空间,然后让指针指向这个内存空间。而这里我们是直接声明的一个数组,数组的话在入栈的时候直接添加即可。

置空一个顺序栈

再次回忆一下对顺序存储结构的数组置空的操作,我们只是直接把 length 置为空,并没有取手动的 free 掉数组的内存空间,这就是顺序存储结构的特点。所以同理,置空一个顺序栈的话,只需要把 top 指针指向 -1,就表示这个栈已经没有任何元素了。代码如下:

  1. Status ClearStack(SqStack *S)
  2. {
  3. S->top = -1;
  4. return OK;
  5. }

判断顺序栈是否为空

那么,如何判断一个顺序栈是否为空呢?显然,我们只需要判断 top 栈顶指针是否为 -1,如果为 -1,说明顺序栈已经为空,如果不等于 -1,说明还有元素尚未出栈。代码如下:

  1. Status StackEmpty(SqStack S)
  2. {
  3. if (S.top == -1) return TRUE;
  4. else return FALSE:
  5. }

获取顺序栈的长度

因为顺序栈是通过 top 栈顶指针来保存最新插入元素在数组中的下标位置,所以说获取顺序栈的长度显然就跟 top 指针有关系。那么,简单分析一下,我们就能得出顺序栈的长度刚好就是 top 的值加一。代码如下:

  1. int StackLength(SqStack S)
  2. {
  3. return S->top + 1;
  4. }

获取栈顶元素

我们首先要明确一点,获取栈顶元素和出栈一个元素并不是两回事,所以说,我们并没有对栈内部的元素有所修改,所以并不需要传入 SqStack 指针,但是呢,我们返回栈顶元素,所以需要将元素的地址传入。
但是我们在获取栈顶元素前,还需要判断一下,这个栈是否为空。

  1. Status GetTop(SqStack S, SElemType *e)
  2. {
  3. if (S.top == -1) return ERROR;
  4. *e = S.data[S.top];
  5. return OK;
  6. }

顺序栈压栈

压栈是我们学习栈结构中最为重要的操作之一,我们先简单理一下逻辑。在压栈之前,我们需要判断栈是否为空吗?显然是不需要的,但是因为我们现在是在实现一个顺序栈,也就是说这个栈的空间是有限的,所以我们需要判断栈是否已经满了,如果已满,我们就不能再执行压栈操作了。而排除掉这些边界值条件后,我们需要分两步来执行压栈操作。第一步,我们要找到我们需要在顺序栈内部的数组里面值的下标位置,而这个位置显然就只能通过 top 来找到,而默认情况下,top 是为负一的,所以我们只需要对 top 执行加一的操作,然后将传入的值赋值给新的 top 值所对应的下标。

  1. Status PushData(SqStack *S, SElemType e)
  2. {
  3. // 顺序栈已满
  4. if (S->top == MAXSIZE -1) return ERROR;
  5. S->top++;
  6. S->data[S->top] = e;
  7. return OK;
  8. }

这里其实还有另外一种更简便的写法,如下:

  1. Status PushData(SqStack *S, SElemType e)
  2. {
  3. // 顺序栈已满
  4. if (S->top == MAXSIZE -1) return ERROR;
  5. S->data[++S->top] = e;
  6. return OK;
  7. }

因为前缀的自增操作符的本质是先执行自增操作,再把自增操作之后的增值拷贝返回回来,所以这里采取前缀自增操作可以让代码看起来更加简洁。

顺序栈出栈

好的,我们现在需要实现的是顺序栈的出栈。对于出栈这个操作,我们也需要考虑边界值的情况。那就是当一个栈为空,那么对这个栈再执行出栈操作显然是不合理的。而栈是否为满并不影响我们的出栈操作。出栈的话也需要分两步操作,第一步需要准备一个指针接收通过 top 栈顶指针取出的栈顶元素的值,第二步需要将 top 指针减一来达到栈的长度减一的效果。代码如下:

  1. Status Pop(SqStack *S, SElemType *e)
  2. {
  3. if (S->top == -1) return ERROR;
  4. *e = S->data[S->top];
  5. S->top--;
  6. return OK;
  7. }

同样的,跟入栈操作一样,这里也可以优化成下面的代码:

Status Pop(SqStack *S, SElemType *e)
{
    if (S->top == -1) return ERROR;
    *e = S->data[S->top--];
    return OK;
}

通过后缀的自减运算符,来实现先返回原来的值的拷贝,再对原来的值进行减一的操作。

遍历顺序栈

最后我们需要实现的是遍历一个顺序栈,简单分析一下,我们只需要从下标0开始遍历到栈顶指针的位置。当然,如果栈为空的话,那么就没必要进行遍历了。

Status StackTravese(SqStack S)
{
    // 如果栈为空,那就没有必要遍历
    if (S.top == -1) return ERROR;
    int i = 0;
    while (i <= S.top) 
    {
        printf("%d ", S.data[i]);
        i++;
    }
    printf("\n");
    return OK;
}

同样的,这里我们也可以将 while 循环内部的 i++ 优化一下:

Status StackTravese(SqStack S)
{
    // 如果栈为空,那就没有必要遍历
    if (S.top == -1) return ERROR;
    int i = 0;
    while (i <= S.top) 
    {
        printf("%d ", S.data[i++]);
    }
    printf("\n");
    return OK;
}

至此,我们实现了关于顺序栈的八大操作,接下来我们来验证一下:

int main()
{
    SqStack S;
    int e;
    if (InitStack(&S) == OK)
    {
        for (int j = 1;j < 10;j++)
        {
            PushData(&S, j);
        }
    }
    printf("顺序栈中元素为:\n");
    StackTraverse(S);

    Pop(&S, &e);
    printf("弹出栈顶元素为: %d\n", e);
    StackTraverse(S);
    printf("是否为空栈: %d\n", StackEmpty(S));
    GetTop(S, &e);
    printf("栈顶元素:%d \n栈长度:%d\n", e, StackLength(S));
    ClearStack(&S);
    printf("是否已经清空栈 %d, 栈长度为:%d\n", StackEmpty(S), StackLength(S));


    return 0;
}

最后打印输出如下:

顺序栈中元素为:
此栈中所有的元素:1 2 3 4 5 6 7 8 9
弹出栈顶元素为: 9
此栈中所有的元素:1 2 3 4 5 6 7 8
是否为空栈: 0
栈顶元素:8
栈长度:8
是否已经清空栈 1, 栈长度为:0

如何实现一个链式栈

我们在前面已经实现了顺序存储的栈,接下来,我们需要实现链式存储的栈。首先,我们需要明确的是链式栈也是需要有 top 指针,而 top 指针指向的是栈的栈顶结点。在压栈和出栈的时候,我们都是通过栈顶指针来找到对应的结点,然后执行在这个结点之前插入新的结点或者删除这个结点。我们先定义一下结点:

typedef struct StackNode
{
    // 数据域
    SElemType data;
    // 指针域
    struct StackNode *next;
}StackNode, *LinkStackPtr;

然后我们需要定义链式栈,因为链式栈并不像顺序栈有数组可以存储值,所以这里需要增加一个 count 的整型变量来记录链式栈的长度。

typedef struct {
    // 栈顶指针
    LinkStackPtr top;
    int count;
}LinkStack;

初始化一个链式栈

我们第一步要做的仍然是初始化,不过是初始化链式栈,初始化链式栈的话我们需要分两步来执行。第一步,需要先申请一个内存空间,然后让 top 指针指向这个内存空间,然后再让 top 指针指向 NULL 空指针,因为此时链式栈中并没有任何结点。第二步,将 count 置为 0.。

Status InitStack(LinkStack *S) 
{
    S->top = (LinkStackPtr)malloc(sizeof(StackNode));
    if (S->top == NULL) return ERROR;
    S->top = NULL;
    S->count = 0;
    return true;
}

置空一个链式栈

相比于顺序栈,要置空一个链式栈,就不是仅仅将 top 指针置为 0 那么简单了。我们需要遍历链式栈的所有结点,然后一一释放掉这些结点。

Status ClearStack(LinkStack *S)
{
    LinkStackPtr p, q;
    p = S->top;
    while (p) {
        q = p;
        p = p->next;
        free(q);
    }
    S->count = 0;
    return OK;
}

判断链式栈是否为空

要判断链式栈是否为空,我们只需要判断 count 是否为 0 即可、

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

获取链式栈的长度

同样的,要获取链式栈的长度也是根据 count 来的。

int StackLength(LinkStack S)
{
    return S.count;
}

获取栈顶元素

获取栈顶元素的操作也分为两步,第一步:判断链式栈是否为空,如果链式栈为空,那么就直接返回;如果链式栈不为空,那么就来到第二步;第二步:通过栈顶 top 指针获取到对应的结点,然后从结点的数据域中取出真正的值。

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

链式栈压栈

对于链式栈来说,压栈操作可以分解为以下的步骤:

  • 根据传入的值新建一个链式栈结点
  • 让新的链式栈结点的指针域指向栈顶指针所指向的结点
  • 让 top 栈顶指针指向新创建的链式栈结点
  • 让 count 加一

思路捋清楚之后,代码实现就比较简单了:

Status Push(LinkStack *S, SElemType e)
{
    LinkStackPtr temp;
    temp = (LinkStackPtr)malloc(sizeof(StackNode));
    if (temp == NULL) return ERROR;
    temp->data = e;
    temp->next = S->top;
    S->top->next = temp;
    S->count++;
    return OK;
}

链式栈出栈

链式栈的出栈,我们简单分析一下,可以分解为下列的几个步骤:

  • 判断链式栈是否为空,如果为空,那么出栈操作将是非法的
  • 如果链式栈不为空,则用传入的元素的指针作为左值来接收 top 栈顶指针指向的结点中的数据域
  • 让 top 栈顶指针指向顶结点指针域指向的下一个链式栈结点,然后释放掉顶结点
  • 让 count 减一

具体代码实现如下:

Status Pop(LinkStack *S, SElemType *e)
{
    if (StackEmpty(*S) == TRUE) return ERROR;
    LinkStackPtr p;
    p = S->top;
    *e = p->data;
    S->top = S->top->next;
    free(p);
    S->count--;
    return OK;
}

遍历链式栈

最后是遍历我们的链式栈了,相比于顺序栈,遍历链式栈实现起来并没有更复杂,代码如下:

Stack StackTraverse(LinkStack S)
{
    LinkStackPtr p;
    p = S.top;
    while(p) {
      printf("%d ", p->data);
      p = p->next;
    }
    printf("\n");
    return OK;
}

最后,我们来验证链式栈的一系列方法:

int main()
{
    printf("链式栈定义与实现\n");
    \
    int j;
    LinkStack s;
    int e;
    if(InitStack(&s)==OK)
        for(j=1;j<=10;j++)
            Push(&s,j);
    printf("栈中元素依次为:");
    StackTraverse(s);
    Pop(&s,&e);
    printf("弹出的栈顶元素 e=%d\n",e);
    StackTraverse(s);
    printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
    GetTop(s,&e);
    printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
    ClearStack(&s);
    printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));

    return 0;
}

输出结果如下:

链式栈定义与实现
栈中元素依次为:10 9 8 7 6 5 4 3 2 1
弹出的栈顶元素 e = 10
9 8 7 6 5 4 3 2 1
栈空否:0(1:空 0:否)
栈顶元素 e=9 栈的长度为9
清空栈后,栈空否:1(1:空 0:否)