【数据结构】简单到有摸鱼负罪感的栈的实现
一、前言
1、什么是栈?
对于栈,百度百科上对它的介绍是这样的:
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
——————————————————————引自百度百科
其实我们可以形象的把栈比喻成一个子弹夹:
士兵在压入子弹的时候是从顶端压入,子弹出膛的的时候也是从顶端出,典型的后进先出结构。
栈这种结构其实在我们的计算机中也有很多的应用,最典型的就是我们函数栈帧:
我们在调用函数的时候会一次在栈顶形成栈帧,在函数调用结束时候也是从顶往下销毁栈帧,也是遵循了后进先出的规则。
经过上面的介绍大家会发现栈的结构比起其他的结构好像要简单很多。而实事也正是如此,因为栈的规定就是只能在栈顶入数据和出数据,所以我们实现的时候后最主要要实现的功能就两个:压栈(push)和弹栈(pop)。
接下来就让我们来实现这个简单的栈结构吧。
2、关于实现的结构选取
因为栈也是一个顺序结构,所以原则上只要是顺序结构就都能用来实现栈,比如说数组、链表、顺序表、队列……:
而其中实现最简单的就是数数组栈了,因为数组尾部数据的插入和删除都是很简单的,所以我们只需要将数组尾部设计成栈顶即可。
而链表栈,因为链表的尾删复杂度是O(n),所以我们可以将链表的头设计成栈顶,这样的话压栈和弹栈就对应的是头插和头删,我们直接点用相应的接口即可。
但队列栈的实现就要复杂得多了,实际上要用到两个队列,具体实现可以参考[LeetCode 225. 用队列实现栈]。
而我们今天既然是简单到像摸鱼的栈实现,当然选用的是最简单的数组实现栈啦。
二、目标
关于栈,我们要实现的主要功能如下:
// 栈的初始化
void StackInit(Stack* ps);
// 压栈
void StackPush(Stack* ps, DataType x);
// 弹栈
void StackPop(Stack* ps);
// 栈的打印(用于测试)
void printStack(Stack* ps);
// 返回栈顶数据
DataType StackTop(Stack* ps);
// 返回栈的数据个数
int StackSize(Stack* ps);
// 判断栈是否为空
bool StackEmpty(Stack* ps);
// 栈的销毁
void DestroyStack(Stack* ps);
三、实现
1、初始化工作
同样的我们还是先要将相应的结构定义一下:
// 重定义数据类型
typedef int DataType;
// 定义栈结构
typedef struct stack {
DataType* data; // 数组指针
int top; // 栈顶指针
int capacity; // 栈的容量
} Stack;
然后我们要将栈给初始化一下,先将栈中的数组给置空,并将top和capacity都置成0:
// 栈的初始化
void StackInit(Stack* ps) {
assert(ps);
ps->data = NULL;
ps->top = 0;
ps->capacity = 0;
}
其实这里的top的初始化可以选择两种方案,一种是初始化成0一种是初始化成-1。
如果初始化成0的话就表示top始终指向的是栈顶元素的上一个元素(未入栈):
如果选择初始化成0,那我们在实现的时候就要注意先压入数据后再让top向上走,如果选择初始化成-1就要先让top往上走在压入数据。
初始化工作完成后,我们就可以来实现各个功能了。
2、压栈(push)
2.1、图解思路
既然我们选择了最简单的数组来实现栈,那压栈就很简单了,只需要将数组对应的位置赋上新的值即可:
最后再让top向上走一步:
而当我们不断地压入数据,原先开辟出来的栈空间有可能满,所以我们在压入数据之前要先检查一下栈是否满了,如果满了就要进行扩容。
扩容我们使用realloc函数就行。如果原来的栈的容量是空,那我们就先将栈的容量初始化为10,以后再以两倍的容量进行扩容。
2.2、代码实现
// 压栈
void StackPush(Stack* ps, DataType x) {
assert(ps);
// 检查是否需要增容
if (ps->top == ps->capacity) {
int newCapacity = ps->capacity == 0 ? 10 : ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->data, newCapacity * sizeof(DataType));
if (NULL == temp) {
perror("ralloc fail!\n");
exit(-1);
}
ps->data = temp;
ps->capacity = newCapacity;
}
ps->data[ps->top] = x;
ps->top++;
}
3、弹栈(pop)
3.1、图解思路
因为我们栈中能访问到的元素是通过top来控制的,所以我们在弹栈的时候并不需要将原来栈顶的数据给删除掉,只需要让top–即可:
因为我们后面在压入数据的时候,新的数据就会将原来的数据给覆盖掉,所以我们删不删除原来的数据都是一样的。
但我们同时还要对栈进行判空,栈为空的时候就不能再弹栈了。
3.2、代码实现
// 弹栈
void StackPop(Stack* ps) {
assert(ps);
assert(ps->top > 0);
ps->top--;
}
4、打印栈(用于测试)
其实在标准库中的栈其实没有打印这个功能的,这里实现的打印其实是为了用于更好地测试。
为了符合栈的结构,所以我们在打印的时候需要从栈顶开始打印:
// 栈的打印
void printStack(Stack* ps) {
assert(ps);
int i = 0;
for (i = ps->top - 1; i >= 0; i--) {
printf("[%d]\n", ps->data[i]);
}
}
而又因为我们选择的是让top指向栈顶元素的上一个元素,所以我们这里要将i初始化成top - 1。
有了打印函数,我们就可以来测试以下上面上面所写的push和pop了,
我们先压入几个数据:
我们可以看到先压入的数据在底下,后压入的数据在上面,所以我们写的push是没有问题的。
我们再来弹栈试试:
结果显示确实是从栈顶弹出数据,所以我们写的pop也是没有问题的。
5、返回栈顶数据
这个其实就不用多说了,当栈不为空的时候返回栈顶数据即可:
// 返回栈顶数据
DataType StackTop(Stack* ps) {
assert(ps);
assert(!StackEmpty(ps));
return ps->data[ps->top - 1];
}
同样的我们这里要返回的其实是top - 1所指向的数据。
6、返回栈的数据个数
对于栈中的数据个数,因为数组的下标是从0开始的,而top指向的又是栈顶元素的上一个元素,所以我们直接返回top即可:
// 返回栈的数据个数
int StackSize(Stack* ps) {
assert(ps);
assert(ps->top >= 0);
return ps->top;
}
7、判断栈是否为空
当top为0的时候就表示栈为空,所以我们直接返回top是否等于0的判断结果就行:
// 判断栈是否为空
bool StackEmpty(Stack* ps) {
assert(ps);
return ps->top == 0;
}
8、销毁栈
因为数组是用realloc函数在堆中动态开辟的一块连续的空间,所以我们直接用free函数释放掉整个数据即可,然后再将top和capacity都置成0:
// 栈的销毁
void DestroyStack(Stack* ps) {
assert(ps);
free(ps->data);
ps->data = NULL;
ps->top = 0;
ps->capacity = 0;
}