可以先了解线性表的无头单向非循环链表(单链表)---数据结构——单链表的增加、删除、查找、修改,详细解析_昵称就是昵称吧的博客-CSDN博客,和线性表的带头双向循环链表---数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_昵称就是昵称吧的博客-CSDN博客
目录
一、线性表之栈
1、栈的表示和实现
1.1栈的概念及结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除。
操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈,出数据也在栈顶。
可以将栈理解为弹夹,压栈就是压入子弹,出栈就是打出子弹。
结构如下所示:
1.2栈的实现表示方法
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
2、设计栈的功能实现
2.1栈空间通过动态存储数据
typedef int StackDataType;//重定义类型名
typedef struct Stack
{
StackDataType* a;//创建一个指针,用来接收数组首元素地址,数组里面的类型是StackDataType
//用数组来描述栈,因为出栈和入栈都是一个地方
int top;//相当于数组的下标,可以是0或-1,这里我们选择0
int capacity;//数组的容量
}ST;
结构体里用一个指针接收动态开辟的内存空间的地址。
2.2栈空间的初始化
//初始化结构体,将结构体变量的地址传递过来,是地址传递,这样就能改变实参的内容了
void StackInit(ST* ps)
{
assert(ps);//地址传递,断言好习惯
//动态开辟一个数组的空间,数组的元素类型是StackDataType,将首元素地址赋给指针a
ps->a = (StackDataType*)malloc(sizeof(StackDataType) * 4);
if (ps->a == NULL)//判断是否成功开辟动态空间
{
perror("the mistake is");
}
ps->top = 0; //相当于数组下标,可以初始化0或-1,这里用0
//用0数组下标从0开始,用-1数组下标从-1开始
ps->capacity = 4; //从开辟的动态空间可以看出数组的容量是4
}
通过地址传递,初始化结构体里的内容。
2.3栈空间的销毁
//销毁结构体里的内容,地址传递,才能改变实参的内容
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);//释放动态开辟的内存
ps->a = NULL;
ps->top = 0; //将结构体里的 相当于数组下标top 和 容量capacity 置为0
ps->capacity = 0;
}
通过释放函数free释放动态开辟的内存,达到销毁栈空间的目的。
2.4入栈的功能
//入栈,相当于往数组填充数据x
void StackPush(ST* ps,StackDataType x)
{
assert(ps);
if (ps->top == ps->capacity)//申请的动态空间满了,数组的元素和容量相等,需要增容
{
//用relloc函数申请动态空间,创建临时指针变量是怕动态开辟空间失败,增容一般是上一次的2倍
//注意realloc和malloc函数里的参数,realloc有两个,malloc只有一个
StackDataType* tmp = (StackDataType*)realloc(ps->a, sizeof(StackDataType) * ps->capacity * 2);
if (tmp == NULL)//判断是否成功开辟动态空间
{
perror("the mistake is");
}
else //增容成功
{
//printf("增容成功\n");
ps->a = tmp;//将新开辟的动态空间地址赋给指针a
ps->capacity *= 2;
}
}
//不需要增容或增容之后
ps->a[ps->top] = x;//想数组填充数据
ps->top++; //数组下标加1,方便后面下一次入栈;
//同时top从0开始,填充一个数据top就自增一次,所以数据个数和top相等
}
需要注意的是,如果栈存放的数据,达到了栈空间动态开辟的容量,便需要增容的操作。
2.5打印栈里数据的功能
//打印栈里的数据
void StackPrint(ST* ps)
{
int i = 0;
for (i = 0; i < ps->top; i++)//因为top初始化的时候是0,存一个数据,top就自增1一次
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
类似打印数组里的数据,遍历一次。
2.6出栈的功能
//出栈,相当于数组最后一个数据除去,一般除数组最后一个元素的方法就是下标减1,效果上相当于除去了
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);//防止数据里面没有数据,就不需要出栈
//之前入栈的时候,因为top从0开始,填充一个数据top就自增一次,所以数据个数和top相等
ps->top--;//一般除数组最后一个元素的方法就是下标减1,效果上相当于除去了
}
解释请看注解,非常详细。
2.7返回栈顶的数据
//返回栈顶的元素,相当于数组的最后一个元素,元素的类型是StackDataType
StackDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top>0);//防止数据里面没有数据,就不用返回
//因为top从0开始,填充一个数据top就自增1一次,删除一个数据top就自减1一次,所以数据个数和top相等
return ps->a[ps->top - 1];//因为下标top是初始化从0开始的,所以数组最后一个元素的下标是top-1
}
解释请看注解,非常详细。
2.8返回栈里的数据的个数
//返回栈里的元素的个数,相当于数组里的元素的个数,就是我们前面说的top,个数的类型就是整型int
int StackSize(ST* ps)
{
assert(ps);
return ps->top;//栈里的元素个数和top相等
}
解释请看注解,非常详细。
2.9判断栈里是否有数据
//判断栈里是否有元素,用布尔型bool,真就为1,假就为0
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//ps->top==0 为真,则返回值是1;为假,返回值是0
}
解释请看注解,非常详细。
3、总代码
这是总代码的地址所在,需要的可以自取:放代码: 代码 - Gitee.com
4、 代码运行实例
二、线性表之队列
1、栈的表示和实现
1.1队列的概念和结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
结构如下所示:
1.2队列的实现表示方法
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
而链表中,使用单链表的结构实现会更好。因为队列结构的入队列相当于单链表的尾插,出队列相当于单链表的头删。
2、设计队列的功能实现
2.1单链表节点的设计
typedef int QDataType;//类型重命名
typedef struct QueueNode//节点用结构体表示
{
struct QueueNode* next;//指向下一个节点
QDataType data;//队列中存储的数据
}QNode;
2.2队列的特殊处理
为了方便使用单链表的尾插(入队列),创建一个指针tail,指向链表的尾节点;
为了方便使用单链表的头删(出队列),创建一个指针head,指向链表的第一个节点。
用结构体放入这两个变量,方便使用。
typedef struct Queue//定义一个结构体,用来存放链表的第一个节点和尾节点
//第一个节点用于对头出(头删),尾节点用于队尾入(尾插)
{
QNode* head;
QNode* tail;
}Queue;
2.3初始化指针head和指针tail
//初始化第一个节点和尾节点
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;//初始化两个节点都用空指针NULL
}
我们会使用这两个指针,所以要先初始化指针head和tail,不然会变成野指针。
2.4队尾入(尾插)
// 队尾入(尾插)
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));//开辟新节点
if (newnode == NULL)//防止动态空间开辟失败
{
printf("malloc fail\n");
exit(-1);//结束函数
}
//开辟空间成功
newnode->data = x;//将想放的数据放入节点里
newnode->next = NULL;//单链表节点里的指针要先赋空指针NULL
if (pq->tail == NULL)//判断链表里是否有节点
{
pq->head = pq->tail = newnode;//没有节点,将新节点赋给第一个节点head和尾节点tail
}
else
{
pq->tail->next = newnode;//有节点,直接单链表尾插
pq->tail = newnode;//尾节点向后走一位,方便写一次队尾入(尾插)
}
}
节点是通过动态内存开辟的,这样可以使避免内存空间的浪费,队尾入首先要判断链表里是否有节点,若没有,动态开辟的第一个节点的地址,要先赋给指针 head 和 tail,因为我们想让指针head指向第一个节点,指针tail指向尾节点。
2.5对头出(头删)
// 队头出
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head);//防止链表里没有节点,就不需要队头出(头删)
// 1、一个
// 2、多个
if (pq->head->next == NULL)//如果链表里只有一个节点的时候
{
free(pq->head);//直接free点这节点的空间
pq->head = pq->tail = NULL;//将指针head和tail置空
}
else//链表有两个及以上的节点
{
QNode* next = pq->head->next;//先找到第一个节点head的下一个节点next
free(pq->head);//释放第一个节点的空间
pq->head = next;//将节点next作为新的第一个节点,方便下一次的队列出(头删)
}
}
对头出(头删)的时候,我们要注意一种情况:如果链表只有 一个节点 或者 一直对头出(头删),直到最后一个节点,这个时候如果没有上面的 if 语句判断链表是否只有一个节点,那么当通过free(pq->head)最后一个节点的空间释放的时候,指针tail就会变成野指针,因为它所指向的那块空间被释放了,因为只有一个节点的时候,指针head和tail指向的都是这一个节点,所以要考虑这种情况。
2.6返回对头的数据(链表里的第一个节点的数据)
//访问对头的数据(链表里的第一个节点的数据)
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);//防止链表没有节点,没有节点就访问不了结构体里的数据,不然就会报错
return pq->head->data;//访问对头(第一个节点)里的数据
}
解释请看注解,非常详细。
2.7返回队尾的数据(链表里的尾节点的数据)
//访问对尾的数据(链表里的尾节点的数据)
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->head);//防止链表没有节点,没有节点就访问不了结构体里的数据,不然就会报错
return pq->tail->data;//访问对尾(尾节点)里的数据
}
解释请看注解,非常详细。
2.8返回队列(链表)里的数据个数
//队列(链表)里的元素个数
int QueueSize(Queue* pq)
{
assert(pq);
int size = 0;
QNode* cur = pq->head;
while (cur)//遍历一次,直到链表最后一个节点里的指针(NULL)
{
++size;
cur = cur->next;
}
return size;
}
解释请看注解,非常详细。
2.9判断链表里是否有元素
//判断链表里是否有元素
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//看第一个节点是否为空指针,
//为空就是没有元素,不为空就是有元素
}
解释请看注解,非常详细。
2.10队列空间的销毁
//销毁空间,即返回每个节点开辟的空间
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)//遍历一次,直到链表最后一个节点里的指针(NULL)
{
QNode* next = cur->next;
free(cur);//依次释放每个节点的空间
cur = next;
}
pq->head = pq->tail = NULL;//释放完,指针head和 tail置空
}
因为节点是动态开辟的,所以通过函数free释放动态内存空间。
3、总代码
这是总代码的地址所在,需要的可以自取:放代码: 代码 - Gitee.com