目录
一、生活中的栈:叠盘子的启示
在深入探索数据结构中的栈之前,让我们先把目光投向日常生活中一个再熟悉不过的场景 —— 叠盘子。想象一下,饭后收拾餐具,你将洗净的盘子一个个叠放在一起。最先放上去的盘子,被压在了最底层;而最后放上去的盘子,稳稳地处在最顶端。当需要取用盘子时,你会从最上面开始拿取,最后才会用到最下面的那个盘子。这看似平常的叠盘子、取盘子过程,其实就蕴含着栈的核心特性 —— 后进先出(Last In First Out,LIFO) 。
后进先出是栈的标志性特征,就像一个只允许单向进出的特殊容器,新元素总是从一端进入(入栈),而出栈时,也是从这一端取出元素,且最后进入的元素最先被取出。这种特性使得栈在处理数据时有着独特的优势,它可以有效地管理和存储那些需要按照特定顺序处理的数据,尤其是当数据的处理顺序与存储顺序相反时,栈就成为了理想的数据结构选择。
二、栈的基本概念
2.1 栈是什么
在数据结构的世界里,栈是一种特殊的线性表。它就像一个只允许从一端进出的特殊容器,所有的操作都只能在这个特定的一端进行,这一端被称为 “栈顶” 。而另一端则是 “栈底”,栈底相对固定,不参与日常的插入和删除操作。 栈的操作主要有两种:入栈(Push)和出栈(Pop)。入栈,也叫进栈或压栈,是将新元素添加到栈顶的操作,就如同把一个新盘子叠放在已有的盘子最上面;出栈,也叫退栈,是从栈顶移除元素的操作,类似于从叠放的盘子中拿走最上面的那个盘子 。当栈中没有任何元素时,我们称它为空栈,就像一摞空的盘子架子。
2.2 栈的特性:后进先出(LIFO)
栈最显著的特性就是后进先出(Last In First Out,LIFO),这是理解栈的关键。我们以一个简单的数字序列为例,假设有数字 1、2、3 依次入栈。首先,1 入栈,此时栈里只有 1;接着 2 入栈,2 位于栈顶,1 在栈底;然后 3 入栈,3 成为栈顶元素,2 和 1 依次在其下方。当进行出栈操作时,3 最先出栈,因为它是最后入栈的;接着是 2 出栈,最后是 1 出栈。这个过程清晰地展示了后进先出的特性,就像叠放和取用盘子一样,最后放上去的盘子最先被拿走 。
后进先出的特性使得栈在很多场景下都有着独特的应用价值。比如在程序中处理函数调用时,函数的参数和局部变量就会按照后进先出的顺序被压入栈和弹出栈,确保函数的正确执行和返回 。再比如在表达式求值中,通过栈可以有效地处理运算符的优先级,保证计算结果的准确性 。
三、栈的实现方式
栈作为一种重要的数据结构,在实际应用中有着广泛的用途。为了满足不同场景的需求,栈有多种实现方式,其中数组和链表是两种最常见的实现方式 ,它们各自有着独特的特点和适用场景。下面,让我们深入了解这两种实现方式。
3.1 数组实现栈
3.1.1 栈的结构定义
使用数组实现栈时,我们可以定义一个结构体来表示栈。这个结构体通常包含以下几个成员:
typedef struct Stack {
int *data; // 存储栈中元素的数组
int top; // 栈顶指针,指向栈顶元素的位置
int capacity; // 栈的容量,即数组的大小
} Stack;
在上述代码中,data是一个动态分配的数组,用于存储栈中的元素;top表示栈顶指针,它记录了当前栈顶元素在数组中的索引位置;capacity则表示栈的最大容量,即数组能够容纳的元素个数 。通过这种结构体定义,我们可以方便地管理和操作栈。
3.1.2 初始化栈
初始化栈是使用栈的第一步,其主要作用是为栈分配内存空间,并设置初始状态。初始化函数的实现如下:
Stack* createStack(int capacity) {
Stack *stack = (Stack*)malloc(sizeof(Stack));
stack->data = (int*)malloc(capacity * sizeof(int));
stack->top = -1; // 初始化栈顶指针为-1,表示栈为空
stack->capacity = capacity;
return stack;
}
在这个函数中,首先使用malloc函数为栈结构体和存储元素的数组分配内存空间。然后,将栈顶指针top初始化为 - 1,这是因为在数组中,-1 表示栈为空,此时栈中没有任何元素。最后,设置栈的容量为传入的参数capacity,这样就完成了栈的初始化 。
3.1.3 入栈操作
入栈操作是将一个新元素添加到栈顶的过程。在进行入栈操作时,需要先检查栈是否已满,如果栈满,则需要进行扩容操作。入栈操作的实现代码如下:
void push(Stack *stack, int value) {
if (stack->top == stack->capacity - 1) {
// 栈满,进行扩容
stack->capacity *= 2;
stack->data = (int*)realloc(stack->data, stack->capacity * sizeof(int));
}
stack->data[++stack->top] = value; // 先将栈顶指针加1,然后将元素存入栈顶位置
}
在上述代码中,首先判断栈是否已满,即stack->top是否等于stack->capacity - 1。如果栈满,则将栈的容量扩大为原来的 2 倍,并使用realloc函数重新分配内存空间 。然后,将栈顶指针stack->top加 1,使其指向新的栈顶位置,再将新元素value存入该位置,完成入栈操作。
3.1.4 出栈操作
出栈操作是从栈顶移除元素的过程。在进行出栈操作时,需要先检查栈是否为空,如果栈空,则不能进行出栈操作。出栈操作的实现代码如下:
int pop(Stack *stack) {
if (stack->top == -1) {
// 栈空,抛出异常或返回错误值
printf("栈为空,无法出栈\n");
return -1;
}
return stack->data[stack->top--]; // 先返回栈顶元素,然后将栈顶指针减1
}
在这段代码中,首先判断栈是否为空,即stack->top是否等于 - 1。如果栈空,则打印错误信息并返回 - 1,表示出栈失败 。否则,返回栈顶元素stack->data[stack->top],然后将栈顶指针stack->top减 1,使其指向新的栈顶位置,完成出栈操作。
3.1.5 获取栈顶元素