用C语言实现一个栈
一、准备工作
1.什么是栈?
栈(Stack),是一种线性数据结构。那线性结构是什么呢?线性结构就是数据元素排成像一条线一样的结构,每个元素只有前后两个方向的关系,比如我们前几期内容讲过的顺序表、单链表、双向循环链表都属于线性结构。但栈的特殊性在于它的操作受限,只允许在固定的一端进行插入和删除元素操作,比如一摞盘子,只能从最上面拿或放。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守 后进先出——LIFO(Last In First Out)的原则。
2.实现一个栈,我们需要些什么?
(1)数组
实现一个栈,我们往往使用数组,因为数组在内存中分配连续的地址空间,使得栈顶元素的访问无需遍历中间的结点,并且数组实现的栈在操作的过程中时间复杂度为O(1),代码也比较简洁。在这里,提到了数组,不难联想到顺序表,其实二者类似,只不过栈只能在栈顶插入删除数据,而顺序表不受限制。当然,栈也分为静态栈和动态栈,由于静态栈开辟的数组空间是固定的,不够灵活,因此,我们往往采用动态栈:通过动态内存开辟的方式,让数组的容量跟着需求来。
(2)top
top指的是栈顶元素的位置,当栈中没有插入数据时,top为-1,待插入数据后,top+1变为0,刚好和数组的下标相对应(第一个数据下标为0),每次插入数据后,栈顶位置都会变化,要+1。
(3)capacity
capacity:表示当前数组能够存多少个数据,指的是栈的容量。(注意:这里的容量不是指数组有多少个字节的空间,而是能够存储数据 的个数,因此,我们可以有这样的公式:数组空间的大小(单位:字节)=数组所存储的单个数据大小 * capacity)
size:表示当前数组已经存了多少个数据。
结构体
我们现在有了一个数组,top(整型)和capacity(整型)。它们虽然数据类型不同,但却紧密联系,共同实现一个栈,因此,我们可以将他们放入一个结构体中(如图)。
二、栈的实现(代码+分析)
1.初始化
结构体Stack告诉我们实现栈需要的一些内容,但是它只是一张图纸,就像房屋的设计图一样,告诉我们哪里是客厅,哪里是厨房,却不能实实在在地住进去。因此,我们需要对顺序表进行初始化(代码如下)。
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (DataType*)malloc(sizeof(DataType) * 4);//先开辟4个数据的空间
if (ps->a == NULL)//基本操作,检查动态内存开辟是否成功
{
perror("malloc fail");
return;
}
ps->top = -1;//top指的是栈顶元素的位置,现在栈中没有插入数据,因此top为-1,待插入数据后,top+1变为0,刚好和数组的下标相对应(第一个数据下标为0)
ps->capacity = 4;//表示初始化后栈中能存4个数据
}
void StackInit(Stack* ps)是初始化函数,我们发现这里传的是结构体的指针,而不是结构体本身。为什么呢,因为形参只是实参的一份临时拷贝,出了函数的作用域后就销毁了,形参在函数里再怎么变化对实参都没有什么影响,想要改变实参,应当传它的地址。一个经典的例子就是写一个交换a,b值的函数时,传地址才有效,道理是一样的。
2.入栈
入栈操作和顺序表的尾插操作类似,比较简单,需要注意的是top要+1后再插入数据,代码如下:
void StackPush(Stack* ps, DataType x)
{
assert(ps);
if (ps->top + 1 == ps->capacity)//检查是否需要扩容,top是数组的下标位,我们都知道,数组的下标位是比数组实际数据个数少1的,因此需要top+1
{
DataType* tmp = (DataType*)realloc(ps, sizeof(DataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
ps->a = tmp;//tmp指向的是调整后内存的起始地址
}
ps->capacity *= 2;//经过扩容,容量变为原来的两倍
ps->top++;//现在是插入数据,栈顶的下标位要+1了
ps->a[ps->top] = x;//在栈顶处插入数据
}
3.判断栈中是否存在有效数据
当栈为空时,其特点就是栈顶的下标位为-1,这个函数是为后面的出栈函数服务,代码如下:
bool StackEmpty(Stack* ps)//判断栈中是否存在数据,如果为空,则返回1,如果不为空,则返回0
{
assert(ps);
return ps->top == -1;//当栈为空时,其特点就是栈顶的下标位为-1
}
4.栈顶数据出栈
出栈操作和顺序表的尾删操作类似,需要注意的是,assert里面的内容为0(或者NULL)时会生效,进行断言,如果现在想让它生效,则要(!StackEmpty(ps))为0,则StackEmpty(ps)为1,此时就是栈中数据为空的情况,代码如下:
void StackPop(Stack* ps)//栈顶数据出栈
{
assert(ps);
assert(!StackEmpty(ps));//assert里面的内容为0(或者NULL)时会生效,如果现在想让它生效,则要(!StackEmpty(ps))为0,则StackEmpty(ps)为1,此时就是栈中数据为空的情况
ps->top--;//所谓出栈,就是将栈顶的下标位-1,这样在取出元素的时候就取不到已经出了栈顶数据了
}
5.获取栈顶元素
DataType StackTop(Stack* ps)//查看栈顶的元素
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top];
}
6.查看栈中数据个数
int StackSize(Stack* ps)//查看一下栈中有几个元素
{
assert(ps);
return ps->top + 1;//数组下标位+1为元素个数
}
7.栈的销毁
void StackDestroy(Stack* ps)//栈的销毁
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = -1;
ps->capacity = 0;
}
三、使用头文件——声明与定义分离
1.头文件——Stack.h
在栈中,我们为了实现它的基本功能,写了很多的函数,如果我们将结构体,函数的声明,函数的定义,以及函数的调用测试写在同一个源文件中,会大大降低代码的可读性和可维护性;因此,我们可以将结构体以及一些函数的声明放在头文件Stack.h中,代码如图:
2.源文件——Stack.c
在源文件Stack.c中,我们存放函数具体实现的代码,在第一行应加上 #include"Stack.h",代码如下:
#include"Stack.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (DataType*)malloc(sizeof(DataType) * 4);//先开辟4个数据的空间
if (ps->a == NULL)//基本操作,检查动态内存开辟是否成功
{
perror("malloc fail");
return;
}
ps->top = -1;//top指的是栈顶元素的位置,现在栈中没有插入数据,因此top为-1,待插入数据后,top+1变为0,刚好和数组的下标相对应(第一个数据下标为0)
ps->capacity = 4;//表示初始化后栈中能存4个数据
}
void StackPush(Stack* ps, DataType x)
{
assert(ps);
if (ps->top + 1 == ps->capacity)//检查是否需要扩容,top是数组的下标位,我们都知道,数组的下标位是比数组实际数据个数少1的,因此需要top+1
{
DataType* tmp = (DataType*)realloc(ps, sizeof(DataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
ps->a = tmp;//tmp指向的是调整后内存的起始地址
}
ps->capacity *= 2;//经过扩容,容量变为原来的两倍
ps->top++;//现在是插入数据,栈顶的下标位要+1了
ps->a[ps->top] = x;//在栈顶处插入数据
}
bool StackEmpty(Stack* ps)//判断栈中是否存在数据,如果为空,则返回1,如果不为空,则返回0
{
assert(ps);
return ps->top == -1;//当栈为空时,其特点就是栈顶的下标位为-1
}
void StackPop(Stack* ps)//栈顶数据出栈
{
assert(ps);
assert(!StackEmpty(ps));//assert里面的内容为0(或者NULL)时会生效,如果现在想让它生效,则要(!StackEmpty(ps))为0,则StackEmpty(ps)为1,此时就是栈中数据为空的情况
ps->top--;//所谓出栈,就是将栈顶的下标位-1,这样在取出元素的时候就取不到已经出了栈顶数据了
}
DataType StackTop(Stack* ps)//查看栈顶的元素
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top];
}
int StackSize(Stack* ps)//查看一下栈中有几个元素
{
assert(ps);
return ps->top + 1;//数组下标位+1为元素个数
}
void StackDestroy(Stack* ps)//栈的销毁
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = -1;
ps->capacity = 0;
}
3.源文件——Test.c(运行测试)
栈是不支持遍历的,没有StackPrint函数。因为栈的主要目的是提供高效的压栈(push)和弹栈(pop)操作,这两种操作的时间复杂度都是O(1)。如果允许遍历,可能需要访问中间元素,这样就会破坏栈的LIFO原则。比如,如果用户可以随便访问栈中间的元素,那栈就不再遵循严格的后进先出规则了。
#include"Stack.h"
int main()
{
Stack ST;
StackInit(&ST);
StackPush(&ST, 1);
StackPush(&ST, 2);
printf("%d ", StackTop(&ST));
StackPop(&ST);
StackPush(&ST, 3);
StackPush(&ST, 5);
printf("%d ", StackTop(&ST));
StackPop(&ST);
StackPush(&ST, 9);
while (!StackEmpty(&ST))
{
printf("%d ", StackTop(&ST));
StackPop(&ST);
}
StackDestroy(&ST);
return 0;
}
运行结果如图:
本期总结+下期预告
本期内容完成了栈的C语言实现,下期将为大家带来队列相关的内容!
感谢大家的关注,我们下期再见!