年轻是我们唯一拥有权利去编织梦想的时光!
•知识回顾
大家好啊!我是vince,我们继续进入纯C实现数据结构的坑里来,上一篇文章 vince 详解了线性表之链表,并且拿双向带头循环链表开涮,和大家一起玩转了双向带头循环链表。
线性表除了前面的顺序表和链表结构,还有下面的栈和队列结构, 这一篇文章 vince 就将和大家一起去一一吃透栈和队列。☀️也希望 vince 的总结在方便后面复习的同时也能给大家带来帮助。
当然在大家看这篇文章之前,vince 还是建议大家先复习复习前面的顺序表和链表,毕竟栈和队列与他们息息相关。
知识连线时刻(直接点击即可)
复习回顾
详解顺序表
详解双向带头循环链表
• 知识点一:栈
栈是一种特殊的线性表 \colorbox{#FFA500}{ 栈是一种特殊的线性表} 栈是一种特殊的线性表,其只限定在表的尾部进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。不含元素的表称为空栈。
压栈:栈的插入数据操作叫做压栈 / 进栈 / 入栈。
出栈:栈的删除数据叫做出栈。
栈中元素遵守后进先出的原则,因此栈又被称为后进先出的线性表。后进先出是相对的。
图解分析:
区别:
栈(数据结构中的栈):是今天要总结的数据结构中的栈,栈中的数据遵循后进先出的原则。这个栈是一个数据结构。
栈帧(即内存中的栈):这个栈是操作系统中划分的区域,也叫做栈。用来函数调用时,建立栈帧用的。
相似处:
性质类似,都符合后进先出原则。
图解分析:
栈的实现一般可以用数组(顺序表)或者链表来实现,这个根据具体情况而定,但相对无规定下而言顺序表的结构稍优一些。因为顺序表在尾插和尾删上的代价比较小一点。因此下面我们拿顺序表来实现栈。
图解分析:
下面就是顺序表实现栈的具体操作步骤:
顺序表实现栈的初始化和顺序表初始化类似有两种初始化方法:
一是整体初始化为空;二是初始化需要的数据;
代码展示:
//栈初始化
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;//这里top初始化为0,所以后面top指向的是栈顶的后一个元素位置
ps->capacity = 0;
}
栈初始化这里关于top的初始化问题还存在两种情况:
top初始化为0时,top在后面就一直指向栈顶元素的下一个位置;
top初始化为-1时,top就指向的是栈顶元素位置。
图解分析:
入栈 / 压栈这里其实就和顺序表的头插操作原理一样。因此在数据入栈的时候需要注意检查栈是否已满。满则需要扩容操作。
代码展示:
//入栈
void StackPush(ST* Ps, SLDataType x)
{
assert(Ps);
//满了就扩容一下
if (Ps->top == Ps->capacity)
{
int newCapacity = Ps->capacity == 0 ? 4 : Ps->capacity * 2;
SLDataType* newnode = (SLDataType*)realloc(Ps->a, sizeof(SLDataType) * newCapacity);
if (newnode == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
Ps->a = newnode;
Ps->capacity = newCapacity;
}
}
Ps->a[Ps->top] = x;
Ps->top++;
//Ps->a[Ps->top++] = x;//这样也可以
}
出栈即删除栈顶元素。和顺序表的头删操作原理是一样的。当然在这里需要判断栈是否为空,如果为空,就不进行出栈操作。
代码展示:
//出栈即删除数据
void StackPop(ST* Ps)
{
assert(Ps);
assert(Ps->top > 0);//要删除,就一定要保证栈内数据不为空
--Ps->top;//这里直接--top就可以,不用抹除数据
}
这里是一个判断栈是否为空的函数,函数返回值类型是bool类型,因此这里需要注意在头文件里面加入stdbool.h的头文件。bool值就是 true 和 false 两个,当然以下代码有两种写法可以参考一下。
代码展示:
//判断栈是否为空
bool StackEmpty(ST* Ps)
{
assert(Ps);
//这是常规写法
/*if (Ps->top > 0)
// {
// return true;
// }
//else //Ps->top == 0 时候就为空
//{
// return false;
//}
//简单写法
return Ps->top == 0; //这里直接当Ps->top==0时候:返回Ps->top==0就为空
}
用该函数来记录栈内数据的多少,即来表示栈的大小。直接用top就可以来起到记录栈内数据的作用。
代码展示:
//记录栈内数据个数
int StackSize(ST* Ps)
{
assert(Ps);
return Ps->top;//返回栈内数据个数
}
栈在使用结束之后,要对其进行销毁处理,将不用的内存还给操作系统,避免发生内存泄漏或后面出现内存访问冲突等一些列问题。
代码展示:
//栈销毁
void Destory(ST* Ps)
{
assert(Ps);
free(Ps->a);
Ps->a = NULL;
Ps->top = Ps->capacity = 0;
}
以上就是顺序表类型栈的实现的一些列操作步骤,这里顺序表实现栈其实很多操作原理都和顺序表类似,因此这就是刚开始让去回顾顺序表和链表的原因。当然,这里只详细使用顺序表实现了栈,链表依然能实现栈,很多原理和链表中的操作相似。
• 知识点二:队列
队列也是一种特殊的线性表 \colorbox{#FF8C00}{ 队列也是一种特殊的线性表} 队列也是一种特殊的线性表,只允许在一端进行插入数据操作,在另一端进行删除数据操作。允许删除的一端,叫做队头;允许插入的一端,叫做队尾。
入队列:进行插入操作的一端称为队尾 。
出队列:进行删除操作的一端称为队头。
队列中的元素遵循先进先出原则,就像近几年大家常干的做核酸排队一样,排在前面的做完就走,排在后面的继续向前移动。
图解分析:
队列也可以拿数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
图解分析:
下面就是单链表实现队列的具体操作步骤:
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue//在创建一个结构体,用来存放指向头结点的指针和指向尾结点的指针
{
QNode* head;
QNode* tail;
}Queue;
在初始化之前,首先需要来看看头文件里面对队列的定义操作,因为这里需要定义两个结构体,所以这里将代码展示讲解一下。第一个结构体是链表的结点定义,里面包含数据和指向下一结点的指针成员变量。第二个结构体,则是在第一个结构体的基础上定义的,可以理解为其子结构,这里是用来存放指向队头和队尾的指针的。
初始化实际上就和链表中单链表初始化类似。
代码展示:
//队列初始化
void QueueInit(Queue* Pq)
{
assert(Pq);
Pq->head = Pq->tail = NULL;
}
队列的插入数据其实就是链表的尾插操作。比较简单,直接进行插入操作即可。
代码展示:
//插入数据
void QueuePush(Queue* Pq, QDataType x)
{
assert(Pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
assert(newnode);//断言一下
newnode->val = x;
newnode->next = NULL;
if (Pq->tail == NULL)
{
assert(Pq->head == NULL);
Pq->head = Pq->tail = newnode;
}
else
{
Pq->tail->next = newnode;
Pq->tail = newnode;//tail更新一下
}
}
队列的删除操作,因为队列是先进先出,所以删除实在对头进行的,其实就类似于链表的头删。在删除的时候需要注意判断队列为空的时候就不能进行删除操作。
代码展示:
//删除队列数据
void QueuePop(Queue* Pq)
{
assert(Pq);
assert(Pq->head && Pq->tail);
if (Pq->head->next == NULL)//判断当只有一个结点的时候,直接free掉头节点
{
free(Pq->head);
Pq->head = Pq->tail = NULL;
}
else
{
QNode* next = Pq->head->next;
free(Pq->head);
Pq->head = next;
}
}
这里是一个判断队列为空的函数,依然返回值和栈的判空一样是bool类型。
代码展示:
//判断是否为空
bool QueueEmpty(Queue* Pq)
{
assert(Pq);
return Pq->head == NULL;//这里判断头或者尾都可以
}
利用这函数来计算队列的长度,将队列的进出做到动态分析。
代码展示:
//计算队列长度
size_t Queuesize(Queue* Pq)
{
int size = 0;
assert(Pq);
QNode* cur = Pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
访问队列的队头元素,比较简单。
代码展示:
//访问队列头数据
QDataType QueueFront(Queue* Pq)
{
assert(Pq);
return Pq->head->val;
}
这里访问队尾元素,也比较简单。、
代码展示:
//访问队列尾数据
QDataType QueueBack(Queue* Pq)
{
assert(Pq);
return Pq->tail->val;
}
队列在这里和栈一样,在使用结束的时候,需要对其进行销毁,将其开辟使用后不继续使用的空间都归还给操作系统。
代码展示:
//队列的销毁
void QueueDestory(Queue* Pq)
{
assert(Pq);
QNode* cur = Pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
Pq->head = Pq->tail = NULL;//最终将头指针和尾指针也置空
}
队列的操作步骤总结,这里队列是拿单链表来实现的,其中重要的函数操作原理实际上和单链表中的操作类似。
•vince 结语
来到这里,大家应该发现其实只要对前面的顺序表和链表掌握完全,拿下栈和队列就不在话下。此时,纯C实现数据结构栈和队列的核心操作你已经学完了,结合前面的顺序表和链表希望大家对数据结构前面这一部分内容有更好的结合和认识。后面vince还会继续详解数据结构其他的内容。
如果各位大佬们觉得有一定帮助的话,就来个赞和收藏吧,如有不足之处也请批评指正。
代码不负有心人,98加满,向前冲啊
以上代码均可运行,所用编译环境为 vs2019 ,运行时注意加上编译头文件#define _CRT_SECURE_NO_WARNINGS 1