概念

是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素的操作,进行数据的插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则

压栈:栈的插入操作叫做进栈/压栈/入栈,入的数据在栈顶

出栈:栈的删除操作叫做出栈,出的数据也在栈顶

栈的结构就像平常所见的羽毛球桶一样,从顶端放入羽毛球,从顶端取出羽毛球

如下图,入栈的顺序是1,2,3,那么出栈顺序就是3,2,1
栈的结构示意图
栈可以使用链表实现,为了方便理解,降低难度,这篇文章中我们使用数组实现

如果想尝试链表实现但还不会链表,可以看我之前写的这篇文章:链表

接下来,我们用代码来实现栈的结构以及它的相关操作

代码实现

栈结构

栈的内部,有一个数组用于存放数据,因为栈是可以直接获取顶端元素的,因此还要有一个记录元素个数的整型,为了实现数组的扩容,还需要一个整型用于记录当前数组的容量

1
2
3
4
5
6
7
8
typedef int STDataType;//方便修改储存的数据类型

typedef struct Stack
{
STDataType* a;//保存数据
int top;//记录数据个数
int capacity;//记录数组容量
}ST;

初始化

对栈的结构进行初始化

函数的接口处传入栈的指针
1.对栈的指针断言,如果指针为空则结束程序
2.将数组置空
3.将元素个数和容量初始化为0

1
2
3
4
5
6
7
8
9
void STInit(ST* pst)
{
assert(pst);

pst->a = NULL;
//top指向栈顶元素的下一个数据
pst->top = 0;
pst->capacity = 0;
}

相信你已经看到上面代码注释中,==top指向栈顶元素的下一个数据==,这是为什么呢?
我们在前面讲到,栈使用数组保存数据,top记录数据个数,而数组元素下标是从0开始的。如果top指向栈顶元素,那么当top=0时,top指向数组首元素(即此时有栈顶数据),但是数据个数为0,两者相悖。

因此在这里规定top指向栈顶的下一个位置,那么栈顶应该为top-1

销毁

对栈的内存占用进行销毁

函数的接口传入栈的指针
1.断言栈指针
2.将数组置空
3.将元素个数和容量置0

1
2
3
4
5
6
7
void STDestroy(ST* pst)
{
assert(pst);

pst->a = NULL;
pst->top = pst->capacity = 0;
}

入栈

从栈顶压入数据

扩容

用newcapacity保存新的容量,如果容量为0那么新容量赋值为4,否则容量扩大为原来的2倍

为了平衡内存和使用性能,一般扩容的倍数是1.5或2倍

1
2
3
4
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;

STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity *sizeof(STDataType));

入栈函数

函数接口传入栈指针和新数据x
1.断言栈指针
2.判断元素个数是否和容量相等,如果相等进行扩容
3.判断tmp是否为空指针,如果为空说明空间开辟失败
4.将新容量和空间赋值给capacity和a
5.将x赋值给栈顶的下一个位置
6.元素个数top加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void STPush(ST* pst, STDataType x)
{
assert(pst);
//扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}

pst->a = tmp;
pst->capacity = newcapacity;
}

pst->a[pst->top] = x;
pst->top++;
}

出栈

删除栈顶元素

函数接口传入栈指针
1.断言栈指针
2.断言元素个数,如果个数为0则不能出栈
3.元素个数top减1,此时无法访问栈顶数据,相当于栈顶被删除

1
2
3
4
5
6
7
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);

pst->top--;
}

取栈顶

获取栈顶数据

函数接口传入栈指针,返回栈顶数据
1.断言栈指针和元素个数
2.将栈顶元素返回

1
2
3
4
5
6
7
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);

return pst->a[pst->top - 1];
}

判空

判断栈是否为空

函数接口传入栈指针,返回bool类型值
1.断言栈指针
2.直接返回比较结果

1
2
3
4
5
6
bool STEmpty(ST* pst)
{
assert(pst);

return pst->top == 0;
}

获取数据个数

 
函数接口传入栈指针,返回整型
1.断言栈指针
2.直接返回栈的数据个数

1
2
3
4
5
6
int STSize(ST* pst)
{
assert(pst);

return pst->capacity;
}

注意

在对结构体或类进行操作时,即使像获取个数、判空这种一行可以实现的功能,也不要直接访问,而是通过函数实现
例如下面的代码就是一种危险操作:

1
2
3
4
5
ST stack;
if(stack.capacity == 0)//直接对结构体进行访问
{
printf("栈为空\n");
}

没有函数内部加以限制,直接访问可能会错误修改数据或者访问的并不是想要的数据

因此为了提高安全性,一定要通过封装好的方法函数访问内部数据

如果文章内容有误,欢迎指出