目录
[1.2.2 定义实现功能](#1.2.2 定义实现功能)
[2.1 队的形象概念](#2.1 队的形象概念)
[2.2 队列的实现](#2.2 队列的实现)
栈
1.1栈的形象概念
什么是栈,学习了408后,根据王道论坛图书出品的《考研复习:数据结构》中的解释,栈也是一种线性结构 ,可以把它想象成一个封闭的箱子,只能一本本把书放进去,不能任意插入一本,而且拿出来也只能从最顶上拿出来,不能从底部抽。所以它的数据存取方式是后进先出(Last in First out),或者先进后出。

此图是一个空的,即当前无元素到压入三个元素的栈,最底下是栈底,最顶部的元素上是栈顶。
用比较官方的说法就是:它有严格的规定,没有顺序表和链表那么随意。它只允许数据从一端进行插入和删除,对的,固定的。进行插入和删除操作的一端称为栈顶,另一端叫栈底。
那么也要有两个基本的概念和一个需要清晰的点,压栈,出栈和栈顶位置。
压栈:(push)通常是数据的插入操作,也可以叫入栈,从栈顶进入。
出栈:(pop)通常是弹出,删除操作,也从栈顶发生,从栈顶弹出。
栈顶元素的定位:Top,这个在初始化中可以定义为top为-1,也可以为0,但是不同定义意义不一样,我自己的初始化为0(这里先不说什么意义,会call back)。
1.2栈的实现
栈通常以数组或者链表的方式去实现,静态栈通常以数组的方式实现,但是在现实中不常见,因为现实中我们基本无法提前预知我们要存放多少数据。所以一般用动态增长的数组 ,也就是需要用到扩容函数malloc来动态根据需要去增长,栈还需要的操作是初始化,压栈,出栈,获取栈顶元素,知道栈内元素以及判空。那么栈的实现也是需要头文件,测试文件和定义文件,文件用来写功能,头文件用于声明,这已经是老生常谈了。
1.2.1头文件内容
-
要定义栈的结构体
-
初始化栈的内容(涉及到确定top的初始值)
-
入栈操作
-
出栈操作
-
返回栈顶元素
-
判空
-
知道栈中目前有多少元素
-
销毁栈
cpp// 支持动态增长的栈 typedef int STDataType; typedef struct Stack { STDataType* _a; int _top; // 栈顶 int _capacity; // 容量 }Stack; // 初始化栈 void StackInit(Stack* ps); // 入栈 void StackPush(Stack* ps, STDataType data); // 出栈 void StackPop(Stack* ps); // 取栈顶元素 STDataType StackTop(Stack* ps); // 获取栈中有效元素个数 int StackSize(Stack* ps); // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 int StackEmpty(Stack* ps); // 销毁栈 void StackDestroy(Stack* ps);以上是头文件的代码,用于声明,定义了Stack这一个结构体。这些足以基本栈的基本操作。声明了之后,才可以被调用。当然定义也可以和声明一起写,因为当你把定义写了后,也完成了声明,定义是一种特殊的声明。声明就如上面代码一样,仅仅显示返回类型,和参数类型。
1.2.2 定义实现功能
一共是需要完成7个基本功能,所以要写七个函数体,其实学习了动态顺序表,有的部分其实比较好理解了。
首先是初始化栈,在定义文件里,不能忘记包含头文件,不然定义会出错。
易错点
也是我犯的错误,我原本以为如果在头文件**"保留结构体,注释掉函数声明,让定义去完成声明"是可行的,但是后面运行就会发生这个现象** 
为什么定义写了,主函数依旧不能实现栈?定义不也是声明吗?这是我一直不理解的问题。
然后我去问了千问这一llm,才搞懂我的误区。❌ 为什么不能靠 .c 文件中的函数定义来"代替"头文件中的函数声明?
原因是main.c和Stack.c是分开编译的,在运行主函数的时候,编译器根本不知道其他.c文件有没有定义这个函数功能,main.c 调用了 STInit(&s),但没有提前声明 这个函数(即没有函数原型),编译器会报错,无法跑,它只会预先知道头文件的内容。也就是说函数的定义对其他文件不可以见,它一般只会在本文件内充当声明作用,对外不可见。
清楚了这一点后,就很知道为何报错了。原因是我把init初始化等函数屏蔽后,在主函数想要调用,但是我只包含了头文件,无法读取另外的函数文件,但是头文件又被注释了,就等于不知道有声明。那无法编译就能说的通了。所以还是要声明里全部有函数声明和结构体。
初始化功能
初始化栈,得给数组a动态申请一块空间,容量大小最好和栈大小一样。因为少了会出错产生随机值,多了会浪费。为什么少了会产生随机值,就是你申请了4个整形的空间,然后你空间大小设置为5,那么当你填充了四个数据后,实际情况是你满满当当了,但是在代码比较中,还没有刀空间5的上限,它就会误以为没满,但实际上申请的空间已经填满了,强行塞入就会发生越界访问。数据放到了被流放还没有征用的空间。是这个意思。
cpp
void STInit(ST*st)
{
st->a =(stacktype*)malloc(sizeof(stacktype)*4);
if (st->a ==NULL)
{
perror("malloc fail");
return;
}
st->capacity = 4;
st->top = 0;//top是栈顶元素的下一个位置,类似于下标
}
malloc需要强制转换类型,一般情况下是不会申请失败的。申请后要把空间也设定为相应的值,不然一定会踩坑。最后就是需要call back了。top=-1和top为0分别意味着什么呢。
top就是栈顶位置,把栈横过来就可以看成数组,push是尾插操作,当top定义为-1时,top就是当前元素位置,压栈的话得提前把top+1,也就是下标为0的位置,然后存放数据。我的定义时初始化为0,是栈顶元素的下一个位置。
压栈
具体压栈如下代码。然后满仓的情况下,就需要扩容,我是四个空间的扩大。
cpp
void STPush(ST* st,int x)
{
assert(st);
//满容量的情况
if (st->top==st->capacity)
{
stacktype* tmp = (stacktype*)realloc(st->a,st->capacity*sizeof(stacktype)*4);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
st->a = tmp;
st->capacity *= 4;
}
st->a[st->top] = x;
st->top++;
}
出栈
也是从栈顶,也就是数组的最后删除,其实只要把大小-1就行,这样遍历就不会遍历到原先的空间,本来是100个元素的空间,然后把空间删了一个,遍历就最多遍历到99。为了保险起见,还是要判断是否为空,栈都为空了还删啥呢是不。
cpp
void STPop(ST* st)
{//判是否是空栈,为空就是0,不为空=1
assert(st);
assert(!STEmpty(st));
st->top--;
}
而是不是为空怎么判断,就是看大小是不是0,不是0就返回false,为0就返回true,那!=ture,意思就是false,而assert遇到false,就会拦截,成本很低。而我设置的初始化top=0,也就是栈顶元素的下一个位置是起始位置,那就说明没有元素,要是top初始化为-1,那么当前位置就是起始位置,top==-1就是空
判空代码
cpp
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
计算栈大小
也看top的数值,初始化为0,那么top既是元素的下标,也是前面个数的总大小,下标为n,那说明前面有n个元素。代码就出来了。这是始终以初始化top=0为依据
cpp
int STSize(ST* st)
{
assert(st);
return st->top;
}
获取栈顶元素
就是要获取下标为top-1的元素,因为top是栈顶元素的下一个位置。依旧要判空,如果不判空可能会发生错误,栈是空的就没必要去访问。
cpp
int STTop(ST* st)
{
assert(st);
assert(!STEmpty(st));
return st->a[st->top-1];
销毁栈
而销毁栈就很简单了,把申请的栈空间释放,然后大小归零就行
cpp
void STDestory(ST* st)
{
assert(st);
free(st->a);
st->a = NULL;
st->capacity = 0;
st->top = 0;
}
OK,到这里一个动态维护的栈就完成了基本实现,还是相对比较容易的,让我们来测试一下
1.3栈的测试
cpp
#include"Stack.h"
int main()
{
ST st;
STInit(&st);
STPush(&st, 1);
STPush(&st,2);
STPush(&st,3);
STPush(&st,4);
STPush(&st,5);
STPush(&st,6);
while (!STEmpty(&st))
{
printf("%d ", STTop(&st));
STPop(&st);
}
STDestory(&st);
return 0;
}
这里有一个点需要注意,就是要遍历打印,就要获取一个栈顶元素,然后抛出,再获取,再抛出。
栈的运行结果
运行的还是比较成功的,实现了栈,接下来可以实现一下队列
队列
2.1 队的形象概念
现实生活中时时刻刻都会有排队的情况,排在最后的叫队尾,排在第一的叫队头。那么队列其实也和现实生活中的一样。只允许在队的一端进行插入操作,在队的另一端删除操作的特殊线性表。队列具有先进先出 FIFO(First In First Out) 。入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头。
但是有个和现实生活中不一样,现实里你不想排队了或者素质不高可以不排或者插队,但是队列有及其严格的规定,不能插队。
队列结构
2.2 队列的实现
队列也可以数组和链表的结构实现,但使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。首先较为复杂的是队列的定义,当时博主学习的时候,在写函数时会结构体定义和队列定义写反。再仔细从头到尾捋了几遍后,有了一个较为清晰的认知。
队列可以用单链表实现,队列的插入就传入单链表的头指针,通过循环找到尾指针后,进行尾插,就可以完成队列的push。如果是删除,就传入单链表的头指针,改变头指针的指向,指向第二个节点,就等于队列的pop,这个是逻辑上清晰,但是理解有点困难,链表和队列概念混合在一起了。
然后呢,就对队列进行封装,单独写一个结构体,来体现这个队列这个结构。这个队列和链表要分开来定义,队列的结构体保存单链表的节点指针,队列的底层是由链表实现的。加了一层封装,操作的就是Queue的结构体,不再是单链表的结构体,把单链表的操作封装出来作为队的操作函数。
再通俗一点就是队列结构体包含了单链表的节点指针,可以通过指向链表的头尾,操作单链表,实现队列功能。所以定义的两个结构体,一个是链表节点的结构体,第二个是队列的结构体
cpp
typedef int QDatatype;
typedef struct QueueNode
{
struct QueueNode* next;
QDatatype data;
}QNode;
这是什么呢,这是链表结构体的定义,用于实现队列的操作。后面把它封装起来,就是Queue的结构体,不单单是单链表的结构体,我的理解就是这样了
cpp
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
队列的结构体定义一个头指针和尾指针,然后在一些声明,由于队列是由单链表实现的,所以队列声明的函数操作和单链表相差不大。也是插入删除,销毁,初始化,不一样的是获取队头元素和队尾元素。完整的头文件声明是这样的。
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int QDatatype;
typedef struct QueueNode
{
struct QueueNode* next;
QDatatype data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDatatype x);
void QueuePop(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
QDatatype QueueFront(Queue* pq);
QDatatype QueueBack(Queue* pq);
是不是和单链表有点类似。然后我们就来具体实现一下
- 队列的初始化,就是把队头和队尾都置空,然后就队列的大小也设置为0;
- 队列的插入:要分两种情况判断,一种是队列空的情况,一种是队列不为空的情况:①队列为空的话,只要在队尾插入元素就好。因为为空的话,队尾插入一个元素,那个元素不单单是队头,也是队尾。②队列不为空的话,只要在队的尾巴的下一个节点插入就可以了,队列的插入是在队尾进行的
- 队列的删除:队列只有一个元素的情况下,只需要free掉队头就可以了。要是不为空,就是从队头删除,但前提是要保留队头的下一个元素,使得原来的队头出去后,第二个节点可以成为新的队头。因为队列是在队头删除的数据。
- 队列的大小:用size,队列插入一次就加加,删除就减减。
- 判空,如果队头和队尾都为空,那就是返回true。
- 至于获取队头和队尾元素,那就直接获取队头队尾的数据就好了。
以下是队列的代码
cpp
#include"Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDatatype x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)
{
assert(pq->tail == NULL);
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head != NULL);
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
QDatatype QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDatatype QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
那么队列的实现源代码也是写出来了,接下来就去测试一下吧
ok,那么队列的基本实现也完成了,到目前栈和队列的概念,定义和实现基本都有了较为清晰的认知。希望我的讲解大家能够接受,如果不对也请批评指正!