前言
栈作为一种重要的线性结构,我们对栈的学习是必不可少的。本篇文章将会详细地介绍栈是如何具体实现的。
文章末尾附带源码。
一、栈是什么
在学习栈是如何实现的之前,我们需要直到栈是什么,它与普通的线性结构有什么区别。简而言之,栈就是操作受限的线性表,它限定仅在栈顶进行插入删除操作。因此,栈又称为后进先出的线性表。
栈可以使用顺序表实现,也可以使用单链表实现。用顺序表实现相对更加简便一点,因为顺序表在尾部插入数据代价较小,所以本篇文章将会以顺序表的方式实现栈。
二、头文件的编写
为了使代码可读性高,分工明确,我们将一些库函数头文件,函数定义等放在一个头文件里面。我们首先创建一个叫做 “Stack.h” 的头文件。
1.引入库函数头文件
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h>
2.定义栈结构体
// 栈的元素类型 typedef int STDataType; // 栈的结构体 typedef struct Stack { // 指向数组的指针 STDataType* a; // 标识栈顶位置,其值为栈顶元素的下标+1 int top; // 栈的容量 int capacity; }ST;
同样的,我们将数据类型重命名为STDataType,方便以后改变。跟顺序表一样,我们定义一个指向数组的指针,数组内存放着数据,数据类型就是 STDataType 类型。因为栈只能在栈顶插入删除,所以我们需要一个标识栈顶位置的top。我们打算使用动态的分配空间的方式来给数组开辟空间,所以也需要一个表示栈的容量的capacity。
需要注意的是,top的含义不一样,后面函数的功能就会有变化。top的值可以是栈顶元素的下标,那么初始值就为-1,top的值也可以是栈顶元素的下标+1,那么初始值就为0,在这里,我们top的含义是指向栈顶元素的下一个。
其实也可以跟顺序表一样,将top理解为数组里面的元素个数,其效果是完全相同的。
3.声明栈的功能函数
// 初始化 void STInit(ST* pst); // 销毁 void STDestroy(ST* pst); // 在栈顶插入 void STPush(ST* pst, STDataType x); // 在栈顶弹出 void STPop(ST* pst); // 查看栈顶元素 STDataType STTop(ST* pst); // 判断栈是否为空 bool STEmpty(ST* pst); // 查询栈的元素个数 int STSize(ST* pst);
跟顺序表一样,无论栈里面是否有元素,我们创建的结构体都会存在,于是我们创建结构体时只需要创建结构体本身而不是跟单链表一样创建一个指向结构体的指针那样。但是我们操作栈的时候往往是需要改变栈结构体数据的,所以对于要改变结构体数据的函数,我们都需要传入一级指针,栈结构体的地址来间接改变结构体的内部数据。
三、主函数文件的编写
为了方便测试,我们将测试函数与主函数放在一起,在主函数里面调用不同的测试函数。我们创建一个叫做 “test.c” 的源文件。
1.包含头文件
#include"Stack.h"
2.编写测试用例
void test() { // 创建结构体 ST s; // 初始化 STInit(&s); // 栈顶插入 STPush(&s, 1); // 栈顶插入 STPush(&s, 2); // 栈顶插入 STPush(&s, 3); // 栈顶插入 STPush(&s, 4); // 栈顶插入 STPush(&s, 5); // 如果栈不为空 while (!STEmpty(&s)) { // 打印栈顶元素 printf("%d ", STTop(&s)); // 弹出栈顶元素 STPop(&s); } printf("\n"); // 销毁 STDestroy(&s); return 0; }
以上测试用例为我随手编写,读者可根据自身情况去编写测试用例来验证结果。
3.主函数的编写
int main() { test(); return 0; }
四、功能函数的编写
同样的,我们将各个功能函数打包在一个文件里面。我们创建一个叫做 “Stack.c” 的源文件。
1.包含头文件
#include"Stack.h"
2.栈的初始化
void STInit(ST* pst) { // pst是指向栈结构体的指针,不可能为空,为空说明传参错误 assert(pst); // 还未开辟数组,令a指向NULL pst->a = NULL; // 栈顶元素下标+1,由于没有栈顶元素,故为0 pst->top = 0; // 数组容量,初值为0 pst->capacity = 0; }
在创建了栈结构体后,传参传入结构体的地址,这个地址是一定不为空的,所以可以在前面断言一下,以防传参错误。
3.栈的销毁
void STDestroy(ST* pst) { // pst不可能为空 assert(pst); // 释放数组空间 free(pst->a); // 释放空间后的指针需要置空 pst->a = NULL; // 空栈没有栈顶元素 pst->top = 0; // 数组的容量也设置为0 pst->capacity = 0; }
对数组进行销毁是比较方便的操作,无需遍历整个数组,直接释放整个数组空间就可以完成销毁所有元素的操作,但是只做到这是不够的,我们还需要修正参数为初始值。这样才算是完成了整个销毁操作。
4.入栈
void STPush(ST* pst, STDataType x) { // pst为指向栈结构体的指针,不可能为空 assert(pst); // 如果栈的元素个数与栈的容量相同 if (pst->top == pst->capacity) { // 使用三目运算符设置新的容量 int newCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity; // 用realloc函数开辟可以存储新容量的元素个数的空间 STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newCapacity); // 开辟空间失败时 if (tmp == NULL) { // 弹出反馈 perror("realloc fail"); // 终止程序 exit(-1); } // 让指向数组的指针指向新的数组空间 pst->a = tmp; // 把新的容量赋值给旧的容量 pst->capacity = newCapacity; } // 在旧栈顶的下一个下标位置插入新元素 pst->a[pst->top] = x; // 栈顶位置的下一个下标+1 ++pst->top; }
前面我们有所过,虽然这个top的真正含义是标识栈顶元素的下一个下标,但是将top理解为这个栈或者说这个数组内的有效元素个数是完全没有问题的,毕竟我也觉得这样理解起来方便一点。在理清楚top的作用后,这个函数其实是非常非常简单的,在最后一个元素的下一个位置插入,最后修改一下相应的参数就好。唯一需要注意的就是扩容问题,插入操作难免会遇到存储上限的时候,合理的分情况扩容就可以很好的解决空间浪费或者空间不够用的问题。
5.出栈
void STPop(ST* pst) { // pst不可能为空 assert(pst); // 空栈不能删除 assert(pst->top > 0); // 令top-1即可,相当于使最后一个数据无效 --pst->top; }
出栈操作比较简单,令top-1即可,因为top的含义是标识栈顶元素的下一个下标,top-1后,原来的最后一个元素就成了无效数据,倒数第二个元素就是栈顶元素了。
6.返回栈顶元素内容
// 返回的是元素内容,数据类型为我们在头文件里定义的STDataType STDataType STTop(ST* pst) { // pst不可能不存在 assert(pst); // 存在元素才能找栈顶元素 assert(pst->top > 0); // 返回top-1下标的数组元素 return pst->a[pst->top - 1]; }
由于top是栈顶元素的下一个,所以top-1的下标就是栈顶元素的下标,通过数组返回即可。
7.判断栈是否为空
// 判断真假的问题,我们在这里使用布尔型的返回类型 bool STEmpty(ST* pst) { // pst不应为空,为空说明传参错误 assert(pst); // 栈为空返回true,不为空返回false return pst->top == 0; }
因为top也可以表示为栈的有效数据的个数,所以可以根据top是否为0来判断栈是否为空。
8.查询栈的有效元素个数
// 返回有效数据的个数而不是数据内容,所以返回值为int int STSize(ST* pst) { // pst不会为空 assert(pst); // 返回top的值 return pst->top; }
同样,top既然可以作为栈有效元素的个数,那么直接将top返回即可。
五、代码整合及结果演示
1.代码整合
若在整合后出现某些函数不安全的错误,请在头文件里面加上下面这行代码。
#define _CRT_SECURE_NO_WARNINGS 1
1.头文件 Stack.h 部分
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> // 栈的元素类型 typedef int STDataType; // 栈的结构体 typedef struct Stack { // 指向数组的指针 STDataType* a; // 标识栈顶位置,指向栈顶元素的下一个 int top; // 栈的容量 int capacity; }ST; // 初始化 void STInit(ST* pst); // 销毁 void STDestroy(ST* pst); // 在栈顶插入 void STPush(ST* pst, STDataType x); // 在栈顶弹出 void STPop(ST* pst); // 查看栈顶元素 STDataType STTop(ST* pst); // 判断栈是否为空 bool STEmpty(ST* pst); // 查询栈的元素个数 int STSize(ST* pst);
2.源文件 Stack.c 部分
#include"Stack.h" void STInit(ST* pst) { assert(pst); pst->a = NULL; pst->top = 0; pst->capacity = 0; } void STDestroy(ST* pst) { assert(pst); free(pst->a); pst->a = NULL; pst->top = 0; pst->capacity = 0; } void STPush(ST* pst, STDataType x) { assert(pst); if (pst->top == pst->capacity) { int newCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity; STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newCapacity); if (tmp == NULL) { perror("realloc fail"); exit(-1); } pst->a = tmp; pst->capacity = newCapacity; } pst->a[pst->top] = x; ++pst->top; } void STPop(ST* pst) { assert(pst); // 空栈不能删除 assert(pst->top > 0); --pst->top; } STDataType STTop(ST* pst) { assert(pst); // 存在元素才能找栈顶元素 assert(pst->top > 0); return pst->a[pst->top - 1]; } bool STEmpty(ST* pst) { assert(pst); // 栈为空返回true,不为空返回false return pst->top == 0; } int STSize(ST* pst) { assert(pst); return pst->top; }
3.源文件 test.c 部分
#include"Stack.h" void test() { ST s; STInit(&s); STPush(&s, 1); STPush(&s, 2); STPush(&s, 3); STPush(&s, 4); STPush(&s, 5); while (!STEmpty(&s)) { printf("%d ", STTop(&s)); STPop(&s); } printf("\n"); STDestroy(&s); return 0; } int main() { test(); return 0; }
2.结果演示
总结
栈是特殊的线性表,但他比普通的线性表要简单很多,只要将普通的线性表了解透彻,那么学习栈将会非常轻松。所以说,打好基础也是很重要的,前面学的好,会帮助理解后面的内容,这是一个很好的良性循环。
栈的内容不是很多,也比较简单,所以篇幅不算很长,但还是难免出现一些瑕疵,如果发现错误,欢迎指正。