前言
堆栈又名栈(stack),它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。栈中没有元素时,称为空栈。
一、栈的结构
1.实现思路
栈的结构特点最核心的就是后进先出四个字,我们在实现栈这个数据结构时首先需要考虑的就是使用顺序表来实现还是使用链表来实现,而考虑的主要因素就是该结构是否可以满足或者说是否可以更方便地实现栈的核心特点后进先出。如果没有看过顺序表和链表的实现的,可以先看一下我之前的文章。
顺序表的实现
单向链表的实现
双向链表的实现
这里我们简单地分析一下,如果使用顺序表,那么实现栈就会比较简单,因为我们实现顺序表的时候就会记录顺序表容量和当前存储数据的大小,而其实在顺序表中的当前存储数据大小也就起到了充当栈顶的作用,而一旦记录了栈顶,我们实现栈的先进后出就变得非常容易,即是顺序表的尾插尾删,效率非常高。
而如果使用链表,我们在栈的结构中会用一个变量来记录存储数据的链表,而其实我们用这个变量记录链表本质也就是记录链表的头结点,而这个头结点其实就是我们的栈顶,我们的入栈和出栈操作其实就是链表的头插头删,效率也是非常不错的,所以我们在栈的结构中就只需再多用一个变量来记录栈的当前容量大小即可。
2.代码结构
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
在本篇文章中,我们就使用顺序表来实现一个栈。如果有想用链表来实现的话,参考我上面给出的思路我相信也可以比较轻松地实现。和我上面分析的思路一致,在C语言中,我们用一个结构体来定义一个栈,在这里即是这个struct Stack的结构体并将其typedef重命名为Stack,便于后面书写的简便性。在这个栈的结构中,和顺序表类似,我们需要定义一个指针_a来存储数据,一个整形变量_top来存储栈当前存储的有效数据个数(在栈中这也同时记录栈顶的位置),一个整形变量_capacity来存储栈的当前空间容量。同时为了方便我们以后更改栈的存储数据类型,我们可以在这里将类型进行typedef,即typedef int STDataType,将int重命名为STDataType,这样以后我们只用在这里更改类型即可更改栈的存储的数据类型。
二、栈的实现
1.初始化和销毁
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
初始化栈的函数,我们传入一个Stack类型的指针,然后将其内部完成初始化操作。即将_a指针赋值为空,然后将栈顶位置_top和容量_capacity大小初始化为0。
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_top = 0;
ps->_capacity = 0;
}
销毁栈的函数,类似地,我们传入一个Stack类型的指针,然后使用free函数释放栈_a的空间,并将栈顶位置_top和容量_capacity大小赋值为0即可完成销毁操作。
2.判空
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0;
}
判断栈是否为空的函数,这个函数非常简单,一句return ps->_top == 0就可以搞定,如果当前存储元素个数(栈顶位置)为0即为空,这句语句就为真,为真就返回非0结果,反之不为空这句语句即为假,为假就返回0。
3.入栈和出栈
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->_capacity == ps->_top)
{
ps->_capacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* pnew = (STDataType*)realloc(ps->_a, ps->_capacity * sizeof(STDataType));
if (pnew == NULL)
{
perror("realloc");
exit(-1);
}
ps->_a = pnew;
pnew = NULL;
}
ps->_a[ps->_top] = data;
ps->_top++;
}
进行入栈操作的函数,这个函数就和顺序表的尾插函数完全类似。首先我们需要对栈是否需要扩容进行判断,完成检查并扩容操作后,我们就进行入栈操作,即在栈顶位置插入一个元素并将栈顶位置后移一位。这里其实都和之前我们实现的顺序表完全类似,所以我相信只要之前亲自实现过顺序表的话,这里难度应该没有那么大。这里唯一需要特别讲解的就是ps->_capacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2这一语句,这里我们直接使用一个三目运算符,当_capacity为0时,我们直接设置为4,如果不为0,我们就将_capacity扩大为原来的二倍。其实简单来说,三目运算符(条件运算符)就是一种简化形式的if-else语句,所以如果我们在平常使用中有需要时可以使用三目运算符来简化一些代码。
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
if (StackEmpty(ps))
{
printf("无元素\n");
return;
}
ps->_top--;
}
进行出栈操作的函数,同理,这个函数和顺序表的尾删函数类似,我们先进行判空操作,保证出栈时栈不能为空,如果不为空我们才进行出栈操作,出栈操作即尾删对于顺序表来说其实是非常简单的,我们也不用进行数据的删除,只需要对当前存储数据个数(栈顶位置)进行前移一位即可。
4.获取栈顶元素和栈的有效个数
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
if (StackEmpty(ps))
{
printf("无元素\n");
return 0;
}
return ps->_a[ps->_top - 1];
}
获取栈顶元素的函数,这个函数也比较简单,我们直接返回栈顶元素,即顺序表的最后一个元素即可,而最后一个元素的索引则是栈顶位置_top - 1。
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top;
}
获取栈的有效个数的函数,由于在我们的栈的结构中,栈顶位置_top即是当前栈存储的元素个数,所以我们直接返回_top的大小即可。
5.测试
进行测试的main函数,用来测试我们写的栈的逻辑是否存在问题。
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
StackPush(&s, 5);
printf("%d ", StackTop(&s));
StackPop(&s);
printf("%d ", StackTop(&s));
StackPop(&s);
printf("%d ", StackTop(&s));
StackPop(&s);
StackPush(&s, 6);
printf("%d ", StackTop(&s));
StackPop(&s);
printf("%d ", StackTop(&s));
StackPop(&s);
printf("%d ", StackTop(&s));
StackPop(&s);
StackPop(&s);
StackDestroy(&s);
return 0;
}
总结
本章到这里数据结构栈的实现就已经全部完成了,栈的实现难度其实不是特别大,特别是如果亲自实现过前面的顺序表和链表的话,难度应该会更小。虽然栈看起来简单,但我们不能忽视它的重要性,如我们平常的函数调用在底层都需要借助于栈来完成,又比如一些算法也是需要依靠栈来完成。所以虽然结构基础,但它的意义却是非常之大,值得我们亲自去实现它。
如需源码,可在我的gitee上找到,下面是链接。
栈源码
如对您有所帮助,可以来个三连,感谢大家的支持。
每文推荐
林俊杰–浪漫血液
梁静茹–情歌
庄心妍–情人节
学技术学累了时可以听歌放松一下。