栈的结构和基本操作
本篇博客会讲解栈。栈是一种线性的数据结构,它满足“后进先出”的特性。栈有两端,分别是栈顶和栈底。每次插入或者删除数据,都是在栈顶的方向进行的。画个图解释一下:假设上面是栈顶,下面是栈底。
插入数据时,就从上面(栈顶)放数据进去。比如插入1的操作如下图:(这种操作称作“入栈”,或者叫push)
再插入2:
再依次插入[3 4 5]。
删除数据时,也是删除栈顶的元素,这种操作称作“出栈”,或者叫pop。对上面的栈进行出栈操作:第一次会删除栈顶的5。
再出栈,会删除4:
以此类推。栈的操作还有:取栈顶元素,比如上图中的栈顶元素是3。再比如,判断栈是否为空,显然上图中栈非空;以及栈中元素个数,上图中栈里有3个元素。
以上就是栈这种数据结构的常见操作了。栈满足“后进先出”的特性,如果先依次插入[1 2 3 4 5],全部入栈后,再出栈,出栈顺序就是[5 4 3 2 1],后入栈的会先出栈,即“后进先出”,简称LIFO,即Last In First Out。
数组栈vs链式栈
栈这种数据结构应该如何实现呢?我们可以使用数组或者链表实现。如果大家对顺序表和链表不太熟,建议大家先阅读我关于顺序表和链表这两种数据结构的解读,再来继续学习。顺序表,单链表,链表。
我们有3个选择:使用数组(即顺序表)实现,使用单链表实现,以及使用双链表实现。
- 使用数组,为了利用顺序表尾插、尾删效率高的优点,我们入栈使用尾插,出栈使用尾删,效率不错。
- 使用单链表,为了利用单链表头插、头删效率高的优点,我们入栈使用头插,出栈使用头删,效率不错。
- 若使用双链表,咋玩都行,因为任意位置的插入删除效率都很高。
如果在单链表和双链表中选一个,我会选择单链表,毕竟少用一点指针,效率也不错。而如果在数组和单链表中选一种实现,我会选择数组,因为在效率差不多的情况下,我们可以比较一下它们之间的劣势,两害相权取其轻。顺序表的劣势在于它需要扩容,不过这不是什么大问题,因为如果采取每次扩2倍的话,扩容不会太频繁,空间浪费也不会太多(最多浪费一半)。而链表的劣势在于,其内存空间不是连续的,这导致缓存的利用率不是很高,如果需要频繁访问数据,效率会降低不少。所以,我选择实现数组栈。
结构的定义
先来定义结构。和顺序表类似,我们使用一个指针来维护动态开辟的数组,使用top记录栈顶元素(注意:如果把top初始化成0,top-1才是栈顶元素的下标,这点后面会讲解),使用capacity来记录当前栈的容量,方便容量不够时扩容。
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
初始化
先来实现一个函数,对栈进行初始化,以下是函数的声明:
void StackInit(Stack* ps);
使用malloc,一开始开辟4个数据的空间。注意和顺序表类似,我们是在外面先创建好一个Stack结构体,再把结构体的地址传给函数,所以ps不可能为NULL,需要断言检查指针的有效性。
void StackInit(Stack* ps)
{
assert(ps);
// 开辟4个空间
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
// 初始化
ps->top = 0;
ps->capacity = 4;
}
入栈
入栈操作类似顺序表的尾插,这是函数的声明:
void StackPush(Stack* ps, STDataType data);
由于一开始我们把top初始化成0,第一次插入数据后,会在下标为0的位置插入数据,然后把top更新成1;第二次插入数据,会在下标为1的位置插入数据,然后把top更新成2……所以,每次的操作是,在下标为top的位置插入数据,然后top加1,此时top标识栈顶元素的下一个位置的下标。
注意:当top和capacity相等时,说明栈已满,需要使用realloc扩容。
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
// 检查容量
if (ps->top == ps->capacity)
{
// 扩容2倍
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
// 扩容成功
ps->a = tmp;
ps->capacity *= 2;
}
// 数据压栈
ps->a[ps->top++] = data;
}
出栈
出栈操作类似顺序表的尾删,以下是函数的声明:
void StackPop(Stack* ps);
出栈前要断言栈非空,然后让top减1即可。
void StackPop(Stack* ps)
{
assert(ps);
// 检查非空
assert(!StackEmpty(ps));
ps->top--;
}
取栈顶元素
下面实现一个函数来取栈顶元素,以下是函数声明:
STDataType StackTop(Stack* ps);
当栈非空的情况下,取下标为top-1的元素。
STDataType StackTop(Stack* ps)
{
assert(ps);
// 检查非空
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
获取元素个数
下面实现一个函数来获取栈中的元素个数。函数的声明如下:
int StackSize(Stack* ps);
top就是元素个数。
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
判空
下面实现一个函数来判断栈是否为空,以下是函数声明:
bool StackEmpty(Stack* ps);
当top为0的状态,栈为空。
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
销毁
最后销毁栈,函数的声明如下:
void StackDestroy(Stack* ps);
释放掉空间即可。
void StackDestroy(Stack* ps)
{
assert(ps);
// 释放空间
free(ps->a);
ps->top = 0;
ps->capacity = 0;
}
总结
- 栈是一种“后进先出”的线性的数据结构,每次操作都在一端(即栈顶)进行。
- 栈建议使用数组来实现,因为顺序表的尾插、尾删效率高,且缓存命中率较高。
感谢大家的阅读!