队列
Ⅰ 队列的定义
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列的特点:先进先出。
堆栈的特点:先进后出。
Ⅱ 队列的实现
队列有两种实现的方法,链表和数组。对于队列来说,用链表是很麻烦的,为了提高效率和简化操作,队列还是用数组来实现比较好。
A. 普通数组的不合理之处
我先建立一个数组。
在还没有数据存入的时候,队首队尾指针都在下标为0的位置,当入队列时,数据存入队首指针现在的位置,并且队首指针后移。
这时候再出队列,队尾指针指向的位置上的元素出去。
在这个过程中,队首和队尾的指针都是不断增大的,但是数组的个数是有限的。
导致即使前面的数据都已出队列,数组还有这么多空间,但是无法使用,因为队尾队首指针在不断向前。
B. 循环数组的应用
在普通的数组不能很好地完成队列操作时,我在此引入循环数组。
循环数组我们可以用下图来理解,类似于一个钟表:
当入队列时,
这就实现了从队首出队列,从队尾入队列。
还是刚才普通数组难以解决的问题,当头指针在12,尾指针在13,这时候入队列需要将其放在尾指针指向的位置,并将尾指针+1.
但是此时13+1需要将尾指针变成0,要怎么做?
这就是循环数组的精妙之处了。以这个例子来看,这是个容量为14的数组。我们设下标为index,容量为capacity。
(index + 1) % capacity就可以做到将下标增加,并使其循环。
以刚才为例,现在尾指针为13,要入队列,先将数据放在尾指针的位置,即13,再将尾指针(13 + 1) % 14 = 0,这就实现了循环。
那么数组里的有效数据个数怎么计算呢?我们来看下面两个例子。
这两种情况,刚好是head和tail的值颠倒过来,如果我们要计算有效数据个数,就需要一个公式:(tail - head + capacity) % capacity。
但是这时候我们会发现,当队列满和队列空的时候,计算出来的数据个数都是0。
所以要判断队列空还是满,我们还需要再加入一个判断,即上一次操作是入队列还是出队列。
如果上一次操作是入队列,那么如果计算出来数据个数为0,就可以判断这个队列是满的,反之上一次操作是出队列,那么队列就是空的。
Ⅲ 队列工具库的创建
循环数组的规律清楚了以后,我们就可以动手来实现队列的工具库了。
A. 队列的表示
首先我们先来表示队列。在之前的分析中,可以知道,要建立一个队列,需要队首指针head,需要队尾指针tail,要建立数组我们还需要知道数组的大小capacity,然后我们还需要知道上一次操作是什么,所以众多的元素我们需要建立一个结构体。
typedef struct QUEUE {
int head;
int tail;
int capacity;
void **data;
boolean lastAction;
}QUEUE;
这样我们就建立了一个队列的控制头。大家可能会对这个void **data
有点疑惑。为什么要这么写呢?
这样其实是建立了一个数组,其存放的元素是指针。因为我们不知道用户到底要在队列里录入什么类型的数据,所以我们无论其录入什么数据,我们都将其地址存入数组中,这样就可以实现通用数组。因为无论你的数据是什么类型,几个字节,其地址都是四个字节,所以可以通过存放地址,来保证数组的元素类型统一。这其实就是一个指针数组。
关于指针数组有疑惑的同学也可以去看我下面这篇文章👇
【C语言基础】->指针与二维数组->同一个人的换装游戏
另外还有一个说明,boolean
是我自己在我的头文件中定义的一个类型,其本质是unsigned char
。
#ifndef _TYZ_QUEUE_H_
#define _TYZ_QUEUE_H_
#include "tyz.h"
#define IN 2
#define OUT 3
typedef struct QUEUE {
int head;
int tail;
int capacity;
void **data;
boolean lastAction;
}QUEUE;
#endif
通过对IN
和OUT
的宏定义,我们来判断上一次操作到底是什么。
B. 初始化&销毁
a. 初始化
要使用队列,最开始当然要先初始化。代码如下👇
boolean initQueue(QUEUE **queue, int capacity) {
if (NULL == queue || NULL != *queue || capacity <= 0) {
return FALSE;
}
*queue = (QUEUE *) calloc(sizeof(QUEUE), 1);
if (NULL == queue) {
return FALSE;
}
(*queue)->data = (void **) calloc(sizeof(void *), capacity);
if (NULL == (*queue)->data) {
free(*queue);
*queue = NULL;
return FALSE;
}
(*queue)->head = 0;
(*queue)->tail = 0;
(*queue)->capacity = capacity;
(*queue)->lastAction = OUT;
return TRUE;
}
可能会有人对我的形参有点疑惑,为什么是指针的指针。QUEUE **queue
。我在下面列出调用这个函数的代码做为比较。
QUEUE *queue = NULL;;
initQueue(&queue, 30);
可以看到我们先定义了一个指针,指向控制头QUEUE
,根据一个著名的例子,用指针交换两个变量的值,如果你要改变实参的值,传递形参就应该把它的地址传过去,否则是无法改变它的值的。所以参考这里,我们需要初始化这个QUEUE *queue
,就需要传递它的地址,即&queue
。其对应的形参就是QUEUE **queue
了。
b. 销毁
申请了空间首先就要考虑释放空间,所以接下来要写的就是销毁队列。👇
void destoryQueue(QUEUE **queue) {
if (NULL == queue || NULL != *queue) {
return;
}
free((*queue)->data);
free(*queue);
*queue = NULL;
}
这里的原理是一样的,要传递(QUEUE **queue)
。因为要销毁指向控制头的那个指针。
C. 判断队列空&队列满
在之前的分析中,我们已经知道了如何判断空满,只需要满足两个条件,一个是head
和tail
值相等,一个是上一次操作是入队列还是出队列。
a. 判断队列满
boolean isQueneFull(const QUEUE *queue) {
if (NULL == queue) {
return TRUE;
}
return IN == queue->lastAction && queue->head == queue->tail;
}
如果传递进去的是一个无效的指针,那么我将其看做的满的。因为只有要入队列的时候,需要判断队列是不是满的,这时候无效的指针是无法将数据入队列的,所以我将其看做是满的,意思也是不能再入队列了。
b. 判断队列空
boolean isQueneEmpty(const QUEUE *queue) {
if (NULL == queue) {
return TRUE;
}
return OUT == queue->lastAction && queue->head == queue->tail;
}
遵从和上面一样的原则。
D. 入队列&出队列
a. 入队列
boolean in(QUEUE *queue, void *data) {
if (NULL == queue || isQueneFull(queue)) {
return FALSE;
}
queue->data[queue->tail] = data;
queue->tail = (queue->tail + 1) % queue->capacity;
queue->lastAction = IN;
return TRUE;
}
这个逻辑就是之前分析的,从尾指针的地方入队列,并将尾指针向前走一格,方式就是(queue->tail + 1) % queue->capacity
。
b. 出队列
boolean out(QUEUE *queue, void *data) {
if (NULL == queue || isQueneEmpty(queue)) {
return FALSE;
}
data = queue->data[queue->head];
queue->head = (queue->head + 1) % queue->capacity;
queue->lastAction = OUT;
}
其逻辑是一样的,从队首出去,然后将头指针向前挪动一格。
出队列和入队列都是很简单的,需要注意的就是要将lastAction
设置为对应的IN
或OUT
。
E. 读取队首数据
读取队首数据是和出队列差不多的,只是不需要将头指针挪动。
void readHead(const QUEUE *queue, void *data) {
if (NULL == queue || isQueneEmpty(queue)) {
return;
}
data = queue->data[queue->head];
}
这样我们就完成了一个队列的工具库,其基本功能就是入队列和出队列。
Ⅳ. 完整代码
首先是我的自己的头文件tyz.h👇
#ifndef _TYZ_H_
#define _TYZ_H_
#define TRUE 1
#define FALSE 0
#define NOT_FOUND -1
#define SET(value, index) (value |= 1 << (index ^ 7))
#define CLEAR(value, index) (value &= ~(1 << (index ^ 7)))
#define GET(value, index) ((value) & (1 << (index ^ 7)) != 0)
typedef unsigned char boolean;
typedef unsigned int u32;
typedef unsigned short u16;
typedef boolean u8;
int skipBlank(const char *str);
boolean isRealStart(int ch);
#endif
然后是队列的头文件,queue.h👇
#ifndef _TYZ_QUEUE_H_
#define _TYZ_QUEUE_H_
#include "tyz.h"
#define IN 2
#define OUT 3
typedef struct QUEUE {
int head;
int tail;
int capacity;
void **data;
boolean lastAction;
}QUEUE;
boolean initQueue(QUEUE **queue, int capacity);
void destoryQueue(QUEUE **queue);
boolean isQueneFull(const QUEUE *queue);
boolean isQueneEmpty(const QUEUE *queue);
boolean in(QUEUE *queue, void *data);
boolean out(QUEUE *queue, void *data);
void readHead(const QUEUE *queue, void *data);
#endif
最后是队列的函数部分代码👇
#include <stdio.h>
#include <malloc.h>
#include "tyz.h"
#include "queue.h"
void readHead(const QUEUE *queue, void *data) {
if (NULL == queue || isQueneEmpty(queue)) {
return;
}
data = queue->data[queue->head];
}
boolean out(QUEUE *queue, void *data) {
if (NULL == queue || isQueneEmpty(queue)) {
return FALSE;
}
data = queue->data[queue->head];
queue->head = (queue->head + 1) % queue->capacity;
queue->lastAction = OUT;
}
boolean in(QUEUE *queue, void *data) {
if (NULL == queue || isQueneFull(queue)) {
return FALSE;
}
queue->data[queue->tail] = data;
queue->tail = (queue->tail + 1) % queue->capacity;
queue->lastAction = IN;
return TRUE;
}
boolean isQueneEmpty(const QUEUE *queue) {
if (NULL == queue) {
return TRUE;
}
return OUT == queue->lastAction && queue->head == queue->tail;
}
boolean isQueneFull(const QUEUE *queue) {
if (NULL == queue) {
return TRUE;
}
return IN == queue->lastAction && queue->head == queue->tail;
}
void destoryQueue(QUEUE **queue) {
if (NULL == queue || NULL != *queue) {
return;
}
free((*queue)->data);
free(*queue);
*queue = NULL;
}
boolean initQueue(QUEUE **queue, int capacity) {
if (NULL == queue || NULL != *queue || capacity <= 0) {
return FALSE;
}
*queue = (QUEUE *) calloc(sizeof(QUEUE), 1);
if (NULL == queue) {
return FALSE;
}
(*queue)->data = (void **) calloc(sizeof(void *), capacity);
if (NULL == (*queue)->data) {
free(*queue);
*queue = NULL;
return FALSE;
}
(*queue)->head = 0;
(*queue)->tail = 0;
(*queue)->capacity = capacity;
(*queue)->lastAction = OUT;
return TRUE;
}
这样我们就完成了队列工具库的创建。
如果对指针的操作还有疑问的可以看我下面的文章👇
【C语言基础】->指针与二维数组->同一个人的换装游戏