本文主要讨论使用c语言实现队列。
什么是队列?
上篇文章我们学习了栈以及实现;我们知道栈的元素进出规则是先进后出,而队列与栈正好相反,队列的规则是先进先出。
就比如现在有一个排队的队伍,从末尾进去的元素会逐渐向另一端靠近,当达到头部的时候离开队伍。这就是先进先出。
所以,队列是一种先进先出的数据结构:
- 入队:在队尾插入元素
- 出队:在队头删除元素
实现
头文件
同样先来看头文件。我们使用链表来实现队列。如果是使用数组,则不方便实现先进先出的功能,需要使用循环数组来完成。我们先来看代码:
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int QType;
typedef struct Node
{
QType data;
struct Node* next;
}Node;
typedef struct Queue
{
Node* front;
Node* rear;
}Queue;
void QueueInit(Queue* q);
void QueueDestory(Queue* q);
void QueuePush(Queue* q, QType x);
void QueuePop(Queue* q);
int QueueSize(Queue* q);
int QueueEmpty(Queue* q);
QType QueueFront(Queue* q);
QType QueueBack(Queue* q);
这里我们创建了front和rear两个指针。链式队列必须同时维护 front 和 rear,否则就无法在 O(1) 时间内完成入队和出队。front表示头指针,rear表示尾指针。如果没有尾指针,那么插入操作每次都需要遍历链表找到尾部,这样的效率并不高。所以我们使用头尾指针来实现效率更高。
源文件
1、初始化
初始化很简单,只需要将头尾指针置空即可。
cpp
void QueueInit(Queue* q)
{
assert(q);
q->front = q->rear = NULL;
}
2、销毁
销毁时需要将每个节点都销毁,因此这时必须进行遍历,并且,最后不要忘记将头尾指针置空防止造成野指针。
cpp
void QueueDestroy(Queue* q)
{
assert(q);
Node* cur = q->front;
while (cur)
{
Node* next = cur->next;
free(cur);
cur = next;
}
//释放完后front和rear仍然指向原来的旧值,会造成野指针
//所以需要置空
q->front = q->rear = NULL;
}
3、创建新节点
该部分我们已经写过很多遍了。这里就不再说明,直接上代码:
cpp
Node* BuyNewNode(QType x)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (newnode == NULL)
{
perror("malloc failed!");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
4、插入新元素
队列中的元素总是从尾部插入。要注意的是,如果是空队列,还需要对front进行处理。代码如下:
cpp
void QueuePush(Queue* q, QType x)
{
assert(q);
Node* newnode = BuyNewNode(x);
if (newnode == NULL)
{
printf("创建节点失败!\n");
return;
}
//如果是空队列
if (q->rear == NULL)
{
q->front = q->rear = newnode;
return;
}
q->rear->next = newnode;
q->rear = newnode;
}
5、删除元素
删除元素时除了考虑空队列外海需要考虑当队列只有一个元素时,不能只处理front,rear也要一并处理。代码如下:
cpp
void QueuePop(Queue* q)
{
assert(q);
if (q->front == NULL)
{
printf("队列为空!\n");
return;
}
if (q->front == q->rear)
{
free(q->front);
q->front = q->rear = NULL;
return;
}
Node* next = q->front->next;
free(q->front);
q->front = next;
}
6、计算队列元素个数
如果我们想要得知队列的元素个数,在当前的情况下就需要遍历一遍队列来让标记递增。实现代码如下:
cpp
int QueueSize(Queue* q)
{
assert(q);
if (q->front == NULL)
{
//空队列元素为0
return 0;
}
Node* cur = q->front;
int tmp = 0;
while (cur)
{
tmp++;
cur = cur->next;
}
return tmp;
}
不过,如果我们的队列对元素个数的获取使用十分频繁的话,为了保证O(1),我们直接在结构的时候多创建一个size变量用于记录即可:
cpp
typedef struct Node
{
QType data;
struct Node* next;
int size;//记录队列元素个数
}Node;
7、返回队首/队尾元素
返回元素十分简单,注意判空就行。直接上代码:
cpp
QType QueueFront(Queue* q)
{
assert(q);
assert(q->front);
return q->front->data;
}
QType QueueRear(Queue* q)
{
assert(q);
assert(q->rear);
return q->rear->data;
}
8、判空函数
我们从上面的功能中发现需要判断队列为空的部分有很多,因此我们也可以单独写一个判空的函数,调用时使用assert断言即可判断。代码如下:
cpp
int QueueEmpty(Queue* q)
{
assert(q);
return q->front == NULL;
}
测试
最后我们对代码进行测试。测试函数如下:
cpp
void Test()
{
Queue q;
QueueInit(&q); // 初始化队列
printf("队列是否为空?%s\n", QueueEmpty(&q) ? "是" : "否");
// 入队
printf("入队元素:1 2 3 4 5\n");
for (int i = 1; i <= 5; i++)
{
QueuePush(&q, i);
printf("队头: %d, 队尾: %d, 队列大小: %d\n",
QueueFront(&q), QueueRear(&q), QueueSize(&q));
}
printf("\n出队\n");
while (!QueueEmpty(&q))
{
printf("出队元素: %d\n", QueueFront(&q));
QueuePop(&q);
if (!QueueEmpty(&q))
printf("队头: %d, 队尾: %d, 队列大小: %d\n",
QueueFront(&q), QueueRear(&q), QueueSize(&q));
else
printf("队列为空\n");
}
// 再次判空
printf("\n出队完成后,队列是否为空?%s\n", QueueEmpty(&q) ? "是" : "否");
QueueDestroy(&q); // 销毁队列
}
运行结果如下:
