小白都能看懂的 “栈”

什么是栈?首先引用维基百科的解释:

栈(stack)是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。

有点难懂?没关系,我们来看一张图: 

没错,桌上的这一叠盘子就是一个栈结构(实际上还需要加一些限定条件,一会会讲到)。其实我们每天都会与“栈”接触。可以说只要理解了这一叠盘子,你就理解了栈!

首先根据维基百科的专业定义,栈有两种基本操作:弹栈和压栈:

图片

对应到我们的这一叠盘子,通常情况下也是两个操作:取盘子和放盘子:

图片

还记得我们上文说过这一叠盘子如果要成为栈,还需要一些限定条件吗?下面我们就来看一下这个条件:

上述对盘子的所有操作必须在盘子顶进行!

说得更直白点,就是取盘子的时候只能从顶部取,不能从中间取;放盘子的时候只能放在顶部,不能插到中间。

于是我们发现,我们每次从这一叠盘子里取出的盘子都是最后叠上去的,最先叠上去的只有在上面的盘子全部取完以后才能取出来。而上面这句话,就是栈结构的最大特点:

后进先出(LIFO, Last In First Out)

OK。以上就是认识栈结构所需要的所有知识,是不是很简单。

图片

下面我们再用一组图片深入理解一下栈结构与其操作过程:

图片

这是一个单向开口的空间,所有元素都通过顶部的开口进入和弹出,内部的橙色方块就是进栈的元素。很明显,先进入的元素会被后进入的元素盖住,取数据的时候只能先取后进入的数据。此时最上面的元素C就被称之为栈顶,最下面的元素A自然就成为了栈底

图片

这就是压栈过程,新放入的元素D放在栈的最上面,代替原来的元素C成为了新的栈顶。栈底仍然为A。

图片

这是弹栈(出栈)过程,元素D被取出,元素C再次成为栈顶,元素A依然是栈底,并且只有等到A上面的所有元素(B、C)全部取出时,A才有机会出栈。

图片

除了压栈和弹栈这两个基本操作,栈结构还有两个特殊状态。此处停止下滑,请思考一下是哪两个。

图片

左边的栈中没有任何元素,所以被称之为空栈,右边被填满了元素,因此被称之为满栈。一般情况下,在程序初始化栈的时候要保证栈为空,在后续的压栈与弹栈操作流程中还要在每次操作前检测栈状态,避免在满栈状态下压栈,在空栈的状态下弹栈,一旦出现上面两种情况,就会导致数据溢出或访问到非法数据,轻则导致程序崩溃死机,重则引起设备失控,甚至出现伤人事件!这并不是危言耸听,试想如果一架运行着的飞机突然自动控制单元出现了栈溢出,导致控制程序死机,无法被控制,也无法切换手动运行。后果自然是非常严重的!

说了这么多,好像栈是一个很简单,且很不灵活的结构,那这玩意儿到底有啥用?

图片

可不要小看了这个结构,在如今的计算机科学、编程与算法中,栈是非常重要,也是用得非常多的一种数据结构,下面我们举几个例子:

  • 函数调用:在计算机程序中,函数的调用和返回借助栈来实现。每次调用函数时,函数的参数和局部变量都会被存储在栈中,当函数执行完成后,栈会弹出这些数据,返回调用点。

  • 表达式求值:栈可以用于解析和求值表达式,包括中缀表达式转换为后缀表达式以及后缀表达式的求值。

  • 内存管理:栈用于存储函数的局部变量和临时数据,对内存的分配和释放起到了关键作用。

  • 括号匹配:栈可以用于检查括号匹配,例如检查一个字符串中的括号是否正确闭合。

  • 后退和撤销:在许多应用程序中,栈可以用于实现后退和撤销功能,例如文本编辑器中的撤销操作。

上面这些例子都是我们日常生活中频繁遇到的场景,由此可见栈无处不在。比如就在写这段话的时候,我还撤销了一个错别字。

图片

到这里你以为就结束了吗?NO! NO! NO! 作为实践主义的一员,我深信实践是检验真理的唯一标准,没有实践,怎么能说理解!接下来,我们一起来用 C 语言实现一个栈结构!

图片

发车之前,我们先明确一点,栈在软件上一般有数组和链表两种实现方式,链表形式会比较复杂,并且涉及到另外的数据结构,本文既然是讲栈的,就尽量不引入其他结构来避免理解困难,因此下文会实现一个纯数组栈。

首先我们来定义一个最简单的栈结构:

#define MAX_SIZE 100  // 栈的最大容量

typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

可以看到,这个结构里有一个数组和一个变量。其中 data 数组就是栈容器,用于存储数据,而 top 变量就用于指示当前栈顶的栈顶计数(有些地方会用一个指针,即栈顶指针。这里直接用计数比较好理解,作用是一样的)。

定义完结构后我们来进行初始化:

// 初始化栈
void init(Stack *stack) { stack->top = -1; }

这里的初始化逻辑很简单,就是将 top 变量的值设置为 -1 。在后续的程序中只要这个值为 -1,我们就认为当前栈为空:

// 判断栈是否为空
bool isEmpty(Stack *stack) { return stack->top == -1; }

自然,当 top 值为 MAX_SIZE - 1 时,就代表当前栈已满(有一个元素时值为0):

// 判断栈是否已满
bool isFull(Stack *stack) { return stack->top == MAX_SIZE - 1; }

压栈:


// 压栈
void push(Stack *stack, int value) {
    if (isFull(stack)) {
        printf("栈已满,无法入栈\n");
    } else {
        stack->data[++stack->top] = value;
    }
}

将数据压栈就是把数据放入数组,同时栈顶计数加一表示当前加入了一个数据。这里要注意,压入数据之前必须要检查栈是否满,避免栈溢出。

弹栈(出栈):

// 出栈
int pop(Stack *stack, int *value) {
    if (isEmpty(stack)) {
        printf("栈已空,无法出栈\n");
        return -1;
    } else {
        *value = stack->data[stack->top--];
        return 0;
    }
}

弹栈则是对外弹出(返回)数组内有效数据的顶部数据,同时,栈顶计数减一表示当前取出了一个数据。同样,必须确保栈不为空再进行弹栈操作。

查看栈顶元素:

// 获取栈顶元素
int top(Stack *stack, int *value) {
    if (isEmpty(stack)) {
        printf("栈已空,无栈顶元素\n");
        return -1;
    } else {
        *value = stack->data[stack->top];
        return 0;
    }
}

该操作和弹栈很类似,但必须要注意,该操作不会导致栈顶计数的变化,只是查看元素。可以理解为,你小时候每次走过玩具店都要看一眼橱窗,然后对妈妈说:我就看看,不买~

打印栈中的元素:

// 打印栈中的元素
void printStack(Stack *stack) {
    printf("栈中的元素为:");
    for (int i = 0; i <= stack->top; i++) {
        printf("%d ", stack->data[i]);
    }
    printf("\n");
}

调试接口,从栈顶到栈底依次打印出栈内数据,用于查看栈内的数据状态。

最后我们用一个 main 函数将上述接口都串起来,形成一个可以运行的程序:

int main() {
    int ret = -1, value = 0;
    Stack stack;
    init(&stack);
    push(&stack, 3);
    push(&stack, 5);
    push(&stack, 7);
    printStack(&stack);
    ret = pop(&stack, &value);
    if (ret == 0) {
        printf("出栈元素为:%d\n", value);
    } else {
        printf("出栈失败");
    }
    printStack(&stack);

    return 0;
}

这个程序的运行逻辑如下:

定义一个栈 > 初始化栈 > 将3压入栈 > 将5压入栈 > 将7压入栈 > 打印当前栈状态 > 弹栈 > 打印弹出数据 > 打印当前栈状态。

这里再次停止往下滑,先思考一下输出结果会是什么。下面来看下运行结果:

jay@jaylinuxlenovo:~/test/stack$ ./stack 
栈中的元素为:3 5 7 
出栈元素为:7
栈中的元素为:3 5 

是不是和你想的一样呢?对于栈的概念,你是不是真的理解了呢。下面附上完整代码,感兴趣的小伙伴可以自己尝试运行一下。

/***************************************************************
 * @file           stack.c
 * @brief
 * @author         WKJay
 * @Version
 * @Date           2023-12-07
 ***************************************************************/
#include <stdio.h>
#include <stdbool.h>

#define MAX_SIZE 100  // 栈的最大容量

typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

// 初始化栈
void init(Stack *stack) { stack->top = -1; }

// 判断栈是否为空
bool isEmpty(Stack *stack) { return stack->top == -1; }

// 判断栈是否已满
bool isFull(Stack *stack) { return stack->top == MAX_SIZE - 1; }

// 压栈
void push(Stack *stack, int value) {
    if (isFull(stack)) {
        printf("栈已满,无法入栈\n");
    } else {
        stack->data[++stack->top] = value;
    }
}

// 出栈
int pop(Stack *stack, int *value) {
    if (isEmpty(stack)) {
        printf("栈已空,无法出栈\n");
        return -1;
    } else {
        *value = stack->data[stack->top--];
        return 0;
    }
}

// 获取栈顶元素
int top(Stack *stack, int *value) {
    if (isEmpty(stack)) {
        printf("栈已空,无栈顶元素\n");
        return -1;
    } else {
        *value = stack->data[stack->top];
        return 0;
    }
}

// 打印栈中的元素
void printStack(Stack *stack) {
    printf("栈中的元素为:");
    for (int i = 0; i <= stack->top; i++) {
        printf("%d ", stack->data[i]);
    }
    printf("\n");
}

int main() {
    int ret = -1, value = 0;
    Stack stack;
    init(&stack);
    push(&stack, 3);
    push(&stack, 5);
    push(&stack, 7);
    printStack(&stack);
    ret = pop(&stack, &value);
    if (ret == 0) {
        printf("出栈元素为:%d\n", value);
    } else {
        printf("出栈失败");
    }
    printStack(&stack);

    return 0;
}

到站,下车!

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WKJay_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值