在前面的文章中,我们阐述了单链表以及双向链表的具体用法,本篇文章将对数据结构中的另一成员——栈,进行系统性的介绍,相比于之前的链表以及单链表,栈相对简单,易于理解。
栈的概念及结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的先进后出原理,类比来说就是和弹夹一样,先压进来的子弹最后打出去,最后压进来的子弹,先打出去。
原理如图所示
栈的实现
要实现栈的结构,我们目前有三种合适的做法,单链表,双向链表,数组。
这三种结构都能让我们实现栈的结构
那么哪一种才是最高效的呢
首先我们可以排除双向链表,因为既然单链表都可以实现栈结构,那么没必要再多创建一个指针去实现双向链表的栈结构
而单链表和数组都可以高效的实现栈结构,但是各有各的优点,
单链表没有容量的限制,可以做到随用随取
而数组的缓存利用率高
我们这里用数组进行实现
栈的基本结构
用数组实现栈,那么结构就和顺序表中的结构一致
我们需要数组的指针,并动态开辟内存
需要栈顶元素来确定栈顶的位置,并且需要容量变量,及时的开辟空间
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
栈的基本函数
我们要实现的功能函数,如图所示
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
我们的函数中每个都用assert判断ps是否为空,这是为了保障函数的安全,利用断言直接提示
接下来我们一一进行具体介绍
初始化栈
栈的初始化很好理解,只需要将数组指针置为NULL,将capacity置0
值得一提的是top的初始化
top的初始化分为两种,一种是-1,一种是0
初始化为-1则 top指向栈顶的数据
初始化为0 则 top指向栈顶的下一个数据
我的代码中使用top初始化为-1的情况
代码如下
void STInit(ST* pst)
{
assert(pst);
pst->a=(STDataType*)malloc(sizeof(STDataType) * 4);
pst->capacity = 0;
//top指向栈顶数据
pst->top = -1;
//top指向栈顶数据的下一个
//pst->top = 0;
}
入栈
由于栈是先进后出型,则只有一种插入方式,即从栈顶插入
我们首先要考虑的就是容量问题,我们可以用capacity和top进行比较,由于我们top初始化为-1,则top就是总数据数量-1,所以如果capacity-1 与top 相等,则说明容量已满,需要扩容
而扩容我们也需要分为两种情况,一种是容量为0,即初始化状态,另一种就是真正容量满的情况
第一种我们直接给4个数据的大小,之后如果容量满了就按二倍进行扩容
这个用三目操作符即可完成扩容数量的确定
再利用realloc进行扩容
最后记得将top先自增再赋值
如果top初始化为0,则先赋值,再自增。
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top + 1 == pst->capacity)
{
int newcapacity =(pst->capacity == 0 )?4: 2 * pst->capacity;
STDataType* tmp = (STDataType*)realloc(pst->a,newcapacity*sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail\n");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->top++;
pst->a[pst->top] = x;
}
出栈
出栈极其简单,即将top自减1即可,这样将无法访问到删除的元素,或者会被下一个新元素所顶替
代码如下:
void STPop(ST* pst)
{
assert(pst);
pst->top--;
}
获得栈顶元素
首先我们需要判断,栈顶是否有元素,用断言进行判断
再返回栈顶元素即可
代码如下:
int STTop(ST* pst)
{
assert(pst);
assert(pst->top > -1);
return pst->a[pst->top];
}
获得有效元素个数
即top初始化为-1,则返回栈顶下标top+1
若top初始化为0,则直接返回top即可
代码如下:
int STSize(ST* pst)
{
assert(pst);
return pst->top + 1;
}
检测栈是否为空
直接用栈顶元素下标与初始化进行比较
相同则返回true,反正返回false
代码如下:
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == -1 ? true : false;
}
栈的销毁
利用free将其数组指针空间销毁即可,再将其他变量置为相应的空值
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = 0;
pst->top = -1;
}
以上即栈函数的实现,比较简单,大家好好掌握
是否该将仅一句话的函数删除
这个问题其实也是我之前想提的问题,如果函数仅仅一句话,那么是不是不需要写函数呢,例如出栈函数,仅仅用top--就能实现,为什么还要用函数封装呢?
这个问题,不能站到自己的视角去看,比如top的初始化就有两种方法,如果我将其初始化为-1,而你当做初始化为0,那么就可能在使用过程中出现错误,如果封装在函数中,我们有assert断言函数帮我们进行保护,则不需要进行担心,因此尽管他只有一句话,但仍有可能引起歧义,所以我们将其封装为函数进行使用。
栈的打印
利用判断为空函数,出栈函数,利用while循环依次打印即可
代码如下
while (!STEmpty(&s))
{
printf("%d\n", STTop(&s));
STPop(&s);
}
总结
总体来说,栈的实现是比较简单的,相比于之前的单链表和双向链表,由于栈是由顺序表实现,所以我们感觉更为简单,总而言之,如果你的顺序表掌握的不错,那么栈你一定可以理解,希望这篇文章对你有所帮助!