今天我们将学习数据结构中的栈,它是一种特殊的线性表。why——在前面我们学习顺序表、链表它们都属于线性表,它们可以在任意位置进行插入和删除数据;但是今天我们学习栈,它只能在一端进行插入和删除。下面我们就来学习并实现栈吧!
①栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
②压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
①首先理解栈是一种线性表,逻辑一定相邻物理不一定相邻。
②其次理解栈是一种特殊的线性表:即只允许在一端进行插入和删除,并且还要遵守后进先出、先进后出的原则。
③实例理解——我们生活中就有许多栈这种后进先出的实例,如:弹夹式手枪、网页的前进后退等等。
最先进栈的元素,就一定在最后出栈吗?
答案是:不一定,要看具体情况。栈只是对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,可以边进边出,只要保证是栈顶元素出栈即可。
如现在有1、2、3三个整数依次进栈,会有那些出栈次序呢?
栈是一种顺序表,即可通过顺序存储结构(数组)和链式存储结构(链表)来实现。
使用顺序存储结构,简称为顺序栈。使用链式存储结构,简称为链式栈。
对于栈这种只能在一端进行插入和删除的线性表,顺序栈&链式栈哪一端作为栈顶比较好呢?
①顺序栈:使用顺序存储结构,即使用数组实现。数组的头插头删的时间复杂度为O(N),尾插尾删的时间复杂度为O(1)。所以我们选择数组尾作为栈顶。
②链式栈:使用链式存储结构,即使用链表实现。链表的种类这么多,我们选择哪一种链表呢——双向链表比单向链表多一个指针域,为了发扬中国人的节省我们选择单向链表。单向链表的头插头删时间复杂度为O(1),尾插尾删时间复杂度为O(N)。所以我们选择链表的头作为栈顶。(由于链表的头有头指针,而栈顶指针也是必须,为什么将其合二为一呢,这个时候栈顶指针指向链表的头,哨兵位就没有意义了,所以对于链式栈来说,是不需要哨兵位的。)
我们可以通过这两种方式实现栈,但是哪种好一点呢?
答案是:(作者的观点,仅供参考)相对而言顺序栈更好一点。插入和删除顺序栈&链式栈的算法效率是差不多的,这个时候考虑到CPU缓存命中率的情况,顺序栈就比链式栈好一些了。
顺序栈使用数组实现,又分为两种结构完成:
①静态的顺序栈:使用定长数组实现,在事先就确定了栈的大小。
②动态的顺序栈:在堆区malloc动态开辟一个数组,栈的空间不够了,就扩容realloc。
③静态栈和动态栈的应用:如果事先就能确定栈的大小,那么选择静态栈。不能事先确定,就选择动态栈。但是实际中往往不能事先预料,所以动态栈的应用更多。
总结:栈的实现有3种——静态的顺序栈、动态的顺序栈、链式栈,作者这里将详细讲解动态的顺序栈实现,静态的顺序栈和链式栈实现仅提供代码参考,因为思想大致相似。
解读:
①动态栈由3个部分组成——指针a指向在堆区开辟的数组(即栈的空间);top表示栈顶位置;capacity表示栈的容量。
②多个数据组成的复杂类型一般使用结构体将其封装起来。
③代码实现:
//重定义栈储存数据类型(优点:①后期一改全改;②见名知意)
typedef int STDataType;
//动态栈的结构(多个类型的复杂结构,定义为结构体)
typedef struct Stack
{
STDataType* a;//指向开辟的数组
int top;//栈顶位置
int capacity;//栈的容量
}ST;
解读:
①给动态栈开辟空间:malloc在堆区申请空间;
②初始化栈顶位置和栈的容量;
③top的初始化有两种,那种都是可以的。初始化top = -1,top就是栈顶元素的位置;初始化top = 0,top就是栈顶元素的下一个。图示:
④代码实现:
//动态栈的初始化
void STInit(ST* pst)
{
//断言pst不为空
assert(pst);
//动态开辟一个长度为4的数组
pst->a = (STDataType*)malloc(sizeof(STDataType) * 4);
//判断是否开辟成功
if (NULL == pst->a)
{
//开辟失败打印错误信息
perror("STInit::malloc");
exit(-1);
}
//初始化栈顶位置和容量
//pst->top = -1;//top是栈顶元素的位置
pst->top = 0;//top是栈顶元素的下一个
pst->capacity = 4;//capacity数组的长度——表示栈的容量
}
解读:
①在堆区申请的空间,好的习惯——不用了就自己free还给操作系统,防止内存泄漏。
②free之后将指针置为NULL,因为free不会改变指针的指向,释放之后指针变成了一个经典的野指针。
③释放之后重置栈顶位置top和栈的容量。
④代码实现:
//动态栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
//释放动态数组
free(pst->a);
//free不会改变指针a的指向,防止野指针生成,置为空
pst->a = NULL;
//更新容量和栈顶位置
pst->top = 0;//top是栈顶元素的下一个
pst->capacity = 0;
}
解读:
①动态栈我们是使用数组实现,规定了数组的尾作为栈顶,所以出栈就是数组的尾删。
②注意特殊情况:栈为空时不能再出栈了。
③出栈我们只需改变top即可,图示:
④代码实现:
//动态栈的出栈
void STPop(ST* pst)
{
assert(pst);
//断言栈是否为空
assert(!STEmpty(pst));
//栈不为空出栈
pst->top--;//出栈即改变top的位置
}
解读:
①每一次进栈前,都判断是否要扩容。
②因为我们top开始初始化是top = 0,所以top是栈顶的下一个位置。我们要注意先插入数据,最后top++。
③图示:
④代码实现:
//动态栈的进栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
//1、先判断是否扩容
if (pst->top == pst->capacity)
{
//1、防止扩容失败,先使用一个临时变量保存开辟空间的起始地址
//2、扩容一般按照倍数扩,这里我们扩2倍
STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * pst->capacity * 2);
//判断是否扩容失败
if (NULL == tmp)
{
perror("STPush::realloc");
return;
}
//开辟成功
pst->a = tmp;//使栈的数组指针指向这块空间
pst->capacity *= 2;//更新栈的容量
}
//2、数据进栈
pst->a[pst->top] = x;
pst->top++;//更新top,(每一次进栈出栈都要更新top)
}
解读:
①因为pos我们初始化为0,所以当pos == 0即为空栈。
②为什么要将其封装成一个模块呢,直接pos == 0判断不可以吗?
答案是:不封装的话,别人也不知道你的top到底指向哪里,可能会用错。所以最好还是将其封装成一个单独的模块,别人直接调用既可以了。
③代码实现:
//判断是否为空栈
bool STEmpty(ST* pst)
{
assert(pst);
//是空栈返回真,否则返回假
return pst->top == 0;
}
解读:
①pos指向的是栈顶的下一个,所以要返回栈顶元素,top - 1即可。
②注意不要用自减操作符,因为自减操作符会带来一个副作用就是会改变top自身的值。(作者第一次就这样报错了!总结:top在每一次进栈和出栈才会改变自身。)
③代码实现:
//返回栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
//断言是否为空栈
assert(!STEmpty(pst));
//不为空栈,返回栈顶元素
return pst->a[pst->top - 1];
}
解读:
①数组的下标是从0开始的,因为top是栈顶元素的下一个,所以top的值即栈的元素个数。
②代码实现:
//栈的元素个数
int STSize(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->top;
}
//常用头文件的声明
#include
#include
#include
#include
//动态栈的结构定义
//重定义栈储存数据类型(优点:①后期一改全改;②见名知意)
typedef int STDataType;
//动态栈的结构(多个类型的复杂结构,定义为结构体)
typedef struct Stack
{
STDataType* a;//指向开辟的数组
int top;//栈顶位置
int capacity;//栈的容量
}ST;
//动态栈的初始化
void STInit(ST* pst);
//动态栈的销毁
void STDestroy(ST* pst);
//动态栈的出栈
void STPop(ST* pst);
//动态栈的进栈
void STPush(ST* pst, STDataType x);
//判断是否为空栈
bool STEmpty(ST* pst);
//返回栈顶元素
STDataType STTop(ST* pst);
//栈的元素个数
int STSize(ST* pst);
#include"Stack.h"
//动态栈的初始化
void STInit(ST* pst)
{
//断言pst不为空
assert(pst);
//动态开辟一个长度为4的数组
pst->a = (STDataType*)malloc(sizeof(STDataType) * 4);
//判断是否开辟成功
if (NULL == pst->a)
{
//开辟失败打印错误信息
perror("STInit::malloc");
exit(-1);
}
//初始化栈顶位置和容量
//pst->top = -1;//top是栈顶元素的位置
pst->top = 0;//top是栈顶元素的下一个
pst->capacity = 4;//capacity数组的长度——表示栈的容量
}
//动态栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
//释放动态数组
free(pst->a);
//free不会改变指针a的指向,防止野指针生成,置为空
pst->a = NULL;
//更新容量和栈顶位置
pst->top = 0;//top是栈顶元素的下一个
pst->capacity = 0;
}
//动态栈的出栈
void STPop(ST* pst)
{
assert(pst);
//断言栈是否为空
assert(!STEmpty(pst));
//栈不为空出栈
pst->top--;//出栈即改变top的位置
}
//动态栈的进栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
//1、先判断是否扩容
if (pst->top == pst->capacity)
{
//1、防止扩容失败,先使用一个临时变量保存开辟空间的起始地址
//2、扩容一般按照倍数扩,这里我们扩2倍
STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * pst->capacity * 2);
//判断是否扩容失败
if (NULL == tmp)
{
perror("STPush::realloc");
return;
}
//开辟成功
pst->a = tmp;//使栈的数组指针指向这块空间
pst->capacity *= 2;//更新栈的容量
}
//2、数据进栈
pst->a[pst->top] = x;
pst->top++;//更新top,(每一次进栈出栈都要更新top)
}
//判断是否为空栈
bool STEmpty(ST* pst)
{
assert(pst);
//是空栈返回真,否则返回假
return pst->top == 0;
}
//返回栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
//断言是否为空栈
assert(!STEmpty(pst));
//不为空栈,返回栈顶元素
return pst->a[pst->top - 1];
}
//栈的元素个数
int STSize(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->top;
}
//常用头文件的声明
#include
#include
#include
#include
//静态栈的结构定义,实际中并不实用
//宏常量MAXSIZE表示栈的容量(定义为宏常量,后期方便更改)
#define MAXSIZE 4
//重定义栈储存数据类型(优点:①后期一改全改;②见名知意)
typedef int STDataType;
//静态栈的结构(多个类型的复杂结构,定义为结构体)
typedef struct Stack
{
STDataType a[MAXSIZE];//定长数组——已经决定了栈的容量
int top;//栈顶指针——指向栈顶元素的位置
}ST;
//静态栈的初始化
void STInit(ST* pst);
//静态栈的出栈
void STPop(ST* pst);
//静态栈的进栈
void STPush(ST* pst, STDataType x);
//判断是否为空栈
bool STEmpty(ST* pst);
//返回栈顶元素
STDataType STTop(ST* pst);
//栈的元素个数
int STSize(ST* pst);
#include"Stack.h"
//静态栈的初始化
void STInit(ST* pst)
{
assert(pst);
//初始化一个空栈
pst->top = -1;//top是栈顶元素的位置
//pst->top = 0;//top是栈顶元素的下一个
}
//静态栈的出栈
void STPop(ST* pst)
{
assert(pst);
//断言是否为一个空栈
assert(!STEmpty(pst));
//出栈
pst->top--;
}
//静态栈的进栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
//断言栈是否满
assert(pst->top != MAXSIZE - 1);
//进栈
pst->top++;
pst->a[pst->top] = x;
}
//判断是否为空栈
bool STEmpty(ST* pst)
{
assert(pst);
//是空栈返回真,否则返回假
return pst->top == -1;
}
//返回栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
//断言栈是否为空
assert(!STEmpty(pst));
//不为空返回栈顶元素
return pst->a[pst->top];
}
//栈的元素个数
int STSize(ST* pst)
{
assert(pst);
//断言栈是否为空
assert(!STEmpty(pst));
//不为空返回栈的元素个数
return pst->top + 1;
//return pst->top++;//对吗?答案是:错的,因为自增++有个副作用,会影响自身的值
}
//重定义栈储存数据类型(优点:①后期一改全改;②见名知意)
typedef int STDataType;
//链式栈结点的结构(多个类型的复杂结构,定义为结构体)
typedef struct StackNode
{
STDataType data;//储存数据
struct StackNode* next;//储存后继结点地址
}STNode;
//链式栈的结构
typedef struct Stack
{
STNode* top;//栈顶指针——指向栈顶位置
int size;//保存栈的元素个数
}ST;
//链式栈的初始化
void STInit(ST* pst);
//链式栈的销毁
void STDestroy(ST* pst);
//链式栈的出栈
void STPop(ST* pst);
//链式栈的进栈
void STPush(ST* pst, STDataType x);
//判断是否为空栈
bool STEmpty(ST* pst);
//返回栈顶元素
STDataType STTop(ST* pst);
//栈的元素个数
int STSize(ST* pst);
#include"Stack.h"
//链式栈的初始化
void STInit(ST* pst)
{
assert(pst);
//空栈的初始化
pst->top = NULL;
pst->size = 0;
}
//链式栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
//遍历删除
STNode* cur = pst->top;//栈顶位置
while (cur)
{
STNode* next = cur->next;//保存栈顶的下一个位置
free(cur);
cur = next;//迭代
}
//防止top成为野指针
pst->top = NULL;
}
//链式栈的出栈
void STPop(ST* pst)
{
assert(pst);
//断言链表不为空
assert(!STEmpty(pst));
//不为空,出栈——即链表的头删
//草图:top topnext,分析必须要一个临时变量保存栈顶的位置
STNode* first = pst->top;//保存栈顶位置
//注意必须修改栈顶位置,再释放
pst->top = first->next;
free(first);
first = NULL;
//记得出栈更新栈的元素个数
pst->size--;
}
//链式栈的进栈
void STPush(ST* pst, STDataType x)
{
//进栈即链表的头插
assert(pst);
//创建新节点
STNode* newnode = (STNode*)malloc(sizeof(STNode));
//判断是否开辟成功
if (NULL == newnode)
{
perror("STPush::malloc");
return;
}
//malloc不会初始化,记得自己初始化
newnode->data = x;
newnode->next = NULL;
//草图:newnode top
newnode->next = pst->top;
pst->top = newnode;//不要忘修改栈顶指针
//更新栈的元素个数
pst->size++;
}
//判断是否为空栈
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == NULL;
}
//返回栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->top->data;
}
//栈的元素个数
int STSize(ST* pst)
{
assert(pst);
return pst->size;//这就是我们前面将栈的元素个数设为栈成员的好处
}
#include"Stack.h"
//静态栈的测试
int main()
{
//定义一个栈
ST st;
//初始化
STInit(&st);
//依次进栈1、2、3、4
STPush(&st, 1);
STPush(&st, 2);
STPush(&st, 3);
STPush(&st, 4);
//打印栈的元素个数
printf("%d\n", STSize(&st));
//出栈
while (!STEmpty(&st))
{
//打印栈顶元素
printf("%d ", STTop(&st));
//迭代
STPop(&st);
}
return 0;
}
①返回栈顶元素和出栈可以结合在一起的,但是为了后期我们C++学习STL我们将其分开写,即接口的功能模仿STL。
②top在出栈和进栈时都要改变。
③栈是固定在一端插入和删除的,所以顺序栈我们选择数组尾作为栈顶——即顺序栈的出栈入栈就是数组的尾删尾插;链式栈我们选择单链表的头结点作为栈顶——即链式栈的出栈入栈就是单链表的头插头删。