队列的概念
队列是一种特殊的线性结构
只允许在一端进行插入操作,在另一端进行删除操作,队列具有先进先出FIFO(First In First Out)
入队:进行插入操作,进行插入的一端称为队尾
出队:进行删除操作,进行删除的一端称为队头
选择用链表实现队列
我们这里选择使用链表去实现队列,原因如下:
因为队列在尾进在头出,如果用顺序表实现,那么出队(头删)就需要挪动顺序表的数据,效率低。
所以这里选择链表去实现
在选择链表时,可选使用头指针的链表也可以使用带头哨兵位头节点的链表,但是这里哨兵位节点并不会发挥很大的作用,所以这里选择用带头指针的链表实现
队列的逻辑图和物理实现图对比如下:
用head
和tail
指针指向头和尾,在头部出队,只需将head
指向下一个节点;在尾部入队,只需尾插一个节点即可,所以这么实现会比较简单
这里要注意的一点,在图中,逻辑图中箭头顺序为从右到左,物理图中箭头由左到右。它们表示的意思不一样,不要被相反的顺序影响自己的逻辑
队列的实现
我们先写一个链表的结构体:
typedef int QueueDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QueueDataType data;
}QNode;
在主函数中,为了方便,我们定义一个头指针
head
指向队头和尾指针tail
指向队尾QNode* head = NULL; QNode* tail = NULL;
如果这么写的话,在设计函数时,就要至少传2个指针,并且为二级指针,这是否的麻烦
所以我们可以再定义一个结构体,将head
和tail
都放到这个结构体中,那么设计函数时只需要传一个指针就可以了
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
这样设计的话,函数中传这个结构体的参数,通过结构体指针就可以改变结构体了,所以也不用传二级指针了
这里在结构体内还定义了一个int size
,这是表示队列的长度,这个设计并非多余,在后面的实现中就会发现这个size
能提高效率
这里的队列实现需要定义2个结构体,这是在之前的数据结构中没有出现的
初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
将head
和tail
都初始化为空,size
为0
销毁
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* nex = cur->next;
free(cur);
cur = nex;
}
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
入队
入队,首先就是开辟一个新节点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
然后就是入队了,在队尾入队
这里如果head==NULL
,就说明当前队列还没有元素head
和tail
都为空
所以让head
指向这个第一个节点,同时这个节点也是自己的尾,tail
也应指向这个节点
pq->head = pq->tail = newnode;
如果head
不为空,就说明当前队列中有元素,直接在tail
后面插入新节点即可
void QueuePush(Queue* pq, QueueDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = pq->tail->next;
}
pq->size++;
}
出队
出队,首先应用断言判断一下队列是否为空
assert(!QueueEmpty(pq));
在队头出队
首先用next
指针保存head->next
,然后就可以free
掉head
了,然后再将head
指向新的头next
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
在这里会发现,如果队列只要一个节点,next
为空,然后虽然能够让head
为空,但tail
不会被改变,所以我们需要改一下:
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;
}
所以最终代码:
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
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--;
}
判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
队列长度
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
这里就最能看出在结构体中定义size
的好处了
如果不去定义,那么在这里求长度就会遍历一遍队列,导致效率低
而之前定义了size
,这里直接返回size
即可,效率高
返回队头元素
QueueDataType QueueFront(Queue* pq)
{
assert(pq);
return pq->head->data;
}
一般通过QueueFront
和QueuePop
2个函数来得到队列中的数据
打印队列中数据:
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
返回队尾元素
QueueDataType QueueBack(Queue* pq)
{
assert(pq);
return pq->tail->data;
}