一、什么是栈
栈是一类特殊的线性表,栈只允许在一端进行插入和删除,允许插入和删除的一端叫做栈顶,另一端叫做栈底。栈的性质是:后进先出(Last In First Out)。
1.后进先出
栈的性质是后进先出,那么什么是后进先出呢?顾名思义,就是后来的数据第一个跑出去。举个例子,小时候都玩过子弹枪吧,当你的小圆球子弹用光了之后,你总要添加子弹吧,那么最后一个添加的子弹正好在弹夹的最上面,也就第一个被发射出去,栈就是这样的。
2.栈的核心操作
栈中的数据就像子弹一样可以被添加和发射,换个名词:压栈和出栈。
压栈:把数据存放到栈中的过程(添加子弹)。
出栈:将数据从栈中弹出的过程(发射子弹)
下面我来为大家用丑陋的绘画模拟压栈和出栈的过程
现在这个丑陋的没盖水桶就是我们的栈,在没有数据进入的时候,栈顶和栈底都是在一个位置上,旁边形状特异的球就是数据,现在我们要把球投进水桶里,也就是压栈。
现在我们已经把1号球扔了进去,栈顶也随着数据的压栈而改变,但是栈底是不会发生变化的。
很好,现在外面就剩一个小球了,但是我现在想把球扔出去,即数据的出栈,在数据出栈之后,栈顶也会随着下降,如图所示:
3.定义小结
从栈的定义和模拟栈数据的压栈出栈过程中,我们可以初步得出以下要点:
1.栈的结构是后进先出,即后进来的数据先被弹出。
2.在压栈的过程中,我们也可以在数据
没有压完时选择出栈,这样我们一组数据的出栈顺序就会变得多种多样。
3.在压栈和出栈的过程中,随着最上面的元素被压进或被取出,栈顶位置是在变化的,而栈底不变。
二、栈的基本操作
1.栈的定义
栈本质上就是线性表,只不过是操作受限而已,所以和顺序表,链表有着异曲同工之处,那么定义中都有哪些元素呢?
首先,栈有栈顶,栈底,这里栈顶我们用top来表示,栈底因为他一直没动,我们也懒得为他再命名一下,直接归0!其次,我上面模拟的这个大水桶总不可能一直往里面扔球吧,扔着扔着就满了,所以容量也是必不可少的,整体的数据存储我们用一个数组来表示。至此,栈的定义就呼之欲出了。
typedef struct Stack
{
int* a;//承接数据的数组
int top;//栈顶
int capacity; //表示栈的容量
}ST;
在这里我们使用int的方式,也可以使用STDataType来表示,不过不要忘记声明一下。
typedef int STDataType;
2.栈的初始化
我们回到最开始还没有数据进入的大水桶,可以看到里面是什么也没有的,但是栈其实做得更绝,初始化的时候连水桶边都不给你,容量直接设置为0,水桶刚出生就出来营业了。
具体实现如下:
void STInit(ST* pst)
{
//断言检测结构体传过来没有,栈还没出生怎么初始化啊
assert(pst);
pst->a = NULL;//数组清空
pst->top = 0;//栈顶在最下面
pst->capacity = 0;//刚出生的桶不许有容量
}
值得一提的是,断言assert函数的头文件是 #include<assert.h>
这样我们就完成了栈的初始化。
3.压栈(数据入栈)
现在我们来实现数据入栈的操作,既然要向桶里投球,肯定要先检查桶是不是满的,或者是不是刚出生的没有容量的,检测完了之后我们就可以放数据了,放完数据不要忘记栈顶位置也是随之改变的。如果没有容量我们可以动态扩容,直接让水桶强制成年。那么怎么进行扩容呢,首先我们先了解一个函数:realloc
在官网中,我们可以看到realloc里面有两个参数,第一个是我们要开辟空间的对象,第二个是要开辟空间的大小,下面我们用到我们的函数中。
首先,扩容的判断条件是什么?就是当我们水桶满了的时候,即top=capacity
//当栈顶位置到达容量时,需要进行扩容
if (pst->top == pst->capacity)
{
//如果我们的初始容量是0,就把capacity设置为4,不是0就把初始容量扩大一倍
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
//我们要进行扩容的是a数组,后面给出要扩容的大小
int* tmp = (int*)realloc(pst->a, (newcapacity) * sizeof(int));
//看是否扩容成功
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
//更新容量
pst->capacity = newcapacity;
最后我们把数据放入数组中,然后进行栈顶位置的改变
void STPush(ST* pst, int x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
int* tmp = (int*)realloc(pst->a, (newcapacity) * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
4.出栈
出栈的时候,我们直接把栈顶的位置改变就可以了,但是我们改变的方式是--top,存在一定的风险,如果栈里没有元素,再减就会发生错误,所以我们要断言一下栈是否为空。
void STPop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
pst->top--;
}
5.判断栈是否为空
判断栈是否为空就十分简单了,我们只要看栈顶就行了,如果top为0就返回true,否则false
bool STEmpty(ST* pst) {
assert(pst);
return pst->top == 0;
}
6.获取栈顶元素
有一个错误的思想就是觉得栈顶元素一定是a[pst->top],但是其实不是这样的,我们还是拿大水桶举例:
在最开始没有元素进入时栈顶的值为0,当进入一个元素时,top的值转而为1,但是实际上1此时是栈顶元素,所以栈顶元素其实是a[pst->top-1]
STDataType STTop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->a[pst->top-1];
}
7.获取栈元素的数量
这个就很简单了,直接返回top代码如下:
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
8.销毁栈
销毁栈要先释放数组,然后置空
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
三、总结
其实栈的实现非常简单,我们只需要理解他的出入栈方式即可实现,结合大水桶与小球,希望大家好好理解。