哼! 哼 ! 啊...
又又又来分享知识了,小伙子你渴望知识的力量吗?快端上来吧
1.队列
- 首先来说下队列的基本结构吧,从图中就可以看出,从队头出去,从队尾入数据互不干扰;也就说队头只出数据,队尾只入数据
- 队列具有公平性,入数据和出数据都是有着一对一的关系。也就说进去是 1 2 3 4,出来就一定是1 2 3 4。
- 应用的场景一般有判断,就好比如节假日去饭店吃饭,好吃的店铺往往会有会多人排队,这个时候就用到了队列。
- 怎么个事呢?,假如服务员每次叫号,餐厅内位置空出来了就会去叫 1号落座点菜,这个时候就是出队了,1要销毁,剩下就是2 3 4 5 6 7 8
- 而后面来的人,来排队就分配号码,就用前面的号码 +1
-
假如我在8号,前面排队排到了4号,我想知道还差几位到我用餐,这个时候就知道当前位置,减去他的位置,得出还差几位,如果系统高级一些还会有预计时间
2.1基本结构和头尾节点结构体
- 基本结构和单链表节点一样,其实就是用单链表实现的
- 第二个头尾节点结构体,可以解决二级指针和多传参数的问题,为什么呢?
- 结构体内放指针,那么要怎么改变指针,那么就只需要传结构体指针就行了,phead和ptail分别指向队列的头和尾
- 你想想把两个指针放到结构体里面,那你传参数的时候是不是只要传结构体指针就行,对就是这样这样足以改变。
typedef int QuDataType;
typedef struct QListNode
{
QuDataType val;
struct QuDataType* next;
}QNode;
typedef struct Queue
{
QNode* phead;//这里的头尾指向的类型肯定是链表的
QNode* ptail;//这两块空间可以指向QNode,要不然也不是这个类型
int size;
}Queue;
2.2队列的实现
- 那实现之前,我用选择使用什么呢?
- 数组,单链表,双链表,这里我们选择使用单链表
- 数组怎么也做不好这件事,因为每次出队列的时候都要挪动数据,整个数据都要向前移动,不适合
- 双链表怎么样都行,但是还是要优先考虑单链表,因为比起单链表,双链表要多一个指针,浪费不少空间,还要多管理一个指针
- 所以这个地方,链表的第一个节点出数据,最后一个几点入数据
2.3初始化(Init) 销毁(Destroy)
- 如果你之前写过单链表就会很简单了;单链表实现增删改查
- 初始化和销毁
- 销毁部分也很简单,在销毁链表前保存下一个节点。
//初始化
void QueueInit(Queue* ps)
{
assert(ps);//确保这个结构体不为NULL
ps->phead = NULL;
ps->ptail = NULL;
ps->size = 0;
}
//销毁链表
void QueueDestroy(Queue* ps)
{
assert(ps);
QNode* cur = ps->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);//free
cur = next;
}
}
2.4插入(Push) 删除(Pop)
- 改变结构体指针的指针,就需要二级指针,改变单链表头指针就是需要二级指针
- 用一个结构体去装头尾指针,传结构体的指针就行了,这样就可以改变结构体内的指针了;这样避免二级指针还避免了多传了参数。
- 因为队列的结构原因我们只需要尾插就行了,尾插前开辟空间
- 当然也分为单个节点和多个节点的情况
- 当节点为一个的时候,头尾节点都要指向一个地方
- 多个节点和单链表的尾插一样
//入队
void QueuePush(Queue* ps,QuDataType x)
{
assert(ps);//保证结构体指针不为NULL
//空间申请,并初始化防止对野指针使用
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("QuPush()::malloc");
return;
}
newnode->next = NULL;
newnode->val = x;
//都指向一个节点
if (ps->phead == NULL)//ps->phead == ps->ptail 这两个地址不相等
{
ps->phead = ps->ptail = newnode;
}
//多个节点
else
{
ps->ptail->next = newnode;
ps->ptail = newnode;//让尾节点指向新的尾
}
ps->size++;
}
2.4.1删除Pop
- 出队,也就是头删,也要分为两种情况
- 一个节点的情况,free释放第一个节点的时候phead和ptail指向同一个地方,这块空间被释放,但是指针变量还在,为了防止ptail指针为野指针,所以都要置为NULL
- 头删,保存头节点的下一个节点,然后删除头节点,把下一个节点地址给头节点指针
//出队
void QueuePop(Queue* ps)
{
assert(ps); //确保这个结构体不为NULL
assert(ps->size != 0);//没数据就不能删了
//一个节点
if (ps->phead->next == NULL)//此时说明只有一个节点了
{
free(ps->phead);//此时头尾指针都指向头节点,随便free一个就行
ps->phead = ps->ptail = NULL;//都指向头节点,都要置NULL
}
//正常情况
else
{
QNode* next = ps->phead->next;
free(ps->phead);
ps->phead = NULL;
ps->phead = next;//成为新的头
}
ps->size--;
}
2.5队头和队尾的数据
- 为什么几行代码还要写一个函数,因为有断言这一步操作可以省去很多的麻烦,调用函数逻辑方面也会更加清晰
- 主要就是传过来参数的断言判断,防止拿到无效数据为NULL
- 如果在主函数调用,可能存在很多未发现的问题
- 取数据,直接访问就行,因为已经有了头尾指针
//队尾数据
QuDataType QueueBack(Queue* ps)
{
assert(ps);//不保证尾节点是否为NULL
assert(ps->ptail);
return ps->ptail->val;
}
//队头数据
QuDataType QueueFront(Queue* ps)
{
assert(ps);//断言ps,指针保证有没有这个结构体,不保证头节点是否为NULL
assert(ps->phead);
return ps->phead->val;
}
2.6判NULL和有效数据
- 判空,如果有效数据个数为0返回true(1),反之有数据就返回false(0)
- 判空运用到打印全部数据
- 有效个数拿结构体里面的Size的个数,在出队和入队时的+ -;
bool QueueEmpty(Queue* ps)
{
assert(ps);
return ps->size == 0;//不为NULL返回1
}
//有效个数
int QueueSize(Queue* ps)
{
assert(ps);
return ps->size;
}
3.完整代码
- Queue.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int QuDataType;
typedef struct QListNode
{
QuDataType val;
struct QuDataType* next;
}QNode;
typedef struct Queue
{
QNode* phead;//这里的头尾指向的类型肯定是链表的
QNode* ptail;//这两块空间可以指向QNode,要不然也不是这个类型
int size;
}Queue;
//初始化
void QueueInit(Queue* ps);
//入队和出队
void QueuePush(Queue* ps,QuDataType x);
void QueuePop(Queue* ps);
//队头和队尾的数据
QuDataType QueueFront(Queue* ps);
QuDataType QueueBack(Queue* ps);
//有效个数
int QueueSize(Queue* ps);
//判空
bool QueueEmpty(Queue* ps);
//销毁
void QueueDestroy(Queue* ps);
- Queue.c,代码的核心逻辑
#include "Queue.h"
//初始化
void QueueInit(Queue* ps)
{
assert(ps);//确保这个结构体不为NULL
ps->phead = NULL;
ps->ptail = NULL;
ps->size = 0;
}
//入队
void QueuePush(Queue* ps,QuDataType x)
{
assert(ps);//保证结构体指针不为NULL
//空间申请,并初始化防止对野指针使用
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("QuPush()::malloc");
return;
}
newnode->next = NULL;
newnode->val = x;
//都指向一个节点
if (ps->phead == NULL)//ps->phead == ps->ptail 这两个地址怎么可能相等?
{
ps->phead = ps->ptail = newnode;
}
//多个节点
else
{
ps->ptail->next = newnode;
ps->ptail = newnode;//让尾节点指向新的尾
}
ps->size++;
}
//出队
void QueuePop(Queue* ps)
{
assert(ps); //确保这个结构体不为NULL
assert(ps->size != 0);//没数据就不能删了
//一个节点
if (ps->phead->next == NULL)//此时说明只有一个节点了
{
free(ps->phead);//此时头尾指针都指向头节点,随便free一个就行
ps->phead = ps->ptail = NULL;//都指向头节点,都要置NULL
}
//正常情况
else
{
QNode* next = ps->phead->next;
free(ps->phead);
ps->phead = NULL;
ps->phead = next;//成为新的头
}
ps->size--;
}
//有效个数
int QueueSize(Queue* ps)
{
assert(ps);
return ps->size;
}
//队尾数据
QuDataType QueueBack(Queue* ps)
{
assert(ps);//不保证尾节点是否为NULL
assert(ps->ptail);
return ps->ptail->val;
}
//队头数据
QuDataType QueueFront(Queue* ps)
{
assert(ps);//断言ps,指针保证有没有这个结构体,不保证头节点是否为NULL
assert(ps->phead);
return ps->phead->val;
}
//判空
bool QueueEmpty(Queue* ps)
{
assert(ps);
return ps->size == 0;//不为NULL返回1
}
//销毁链表
void QueueDestroy(Queue* ps)
{
assert(ps);
QNode* cur = ps->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);//free 为NULL 不影响
cur = next;
}
}
- test.c,测试部分
void queuetest()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 3);
QueuePush(&q, 5);
QueuePush(&q, 7);
QueuePop(&q);
printf("%d\n", QueueBack(&q));
printf("%d\n", QueueSize(&q));
printf("%d\n", QueueEmpty(&q));
while (!QueueEmpty(&q))//不为NULL返回0,! 结果取反后是1,1就执行
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
}
4.总结
- 虽然栈比较简单,但是也不要掉以轻心,学好当前知识尤为重要;
- 后续还有关于栈和队列的经典例题的讲解,敬请期待!