目录
一、队列的定义
1.1 相关概念和特点
(1)基本概念
队列(queue)是一种先进先出(First In First Out, FIFO )的线性表。它只允许在表的一端进行插入,而在另一端删除元素。
这和日常生活中的排队是一致的,最早进入队列的元素最早 离开。在队列中,允许插入的一端称为队尾(rear),允许删除的一端则称为队头(front)
(2)队列特点
先入先出(FIFO)
(3)案例引入
队列在程序设计中也经常出现。一个最典型的例子就是操作系统中的作业排队。在允许多道程序运行的计算机系统中,同时有几个作业运行。如果运行的结果都需要通过通道输出,那就要按请求输入的先后次序排队。每当通道传输完毕可以接受新的输出任务时,队头的作业先从队列中退出做输出操作。凡是申请输出的作业都从队尾进入队列。
图1.1-1 队列示意图
2.1 队列分类
图1.2-1 不同队列结构示意图
二、队列的表示和实现
2.1 顺序队列的表示和实现
(1)结构体定义
#define MAX 1000
typedef struct Queue//队列构造
{
void *p[MAX];//数据域,数组存储指针(地址)
int size;//队列大小
} Queue;
(2)初始化操作
图2.1-1 顺序队列初始化
Queue *initQueue()//队列初始化
{
Queue *queue = (Queue *)malloc(sizeof(Queue));//在堆区申请内存
if (queue == NULL)
return NULL; //内存分配失败
memset(queue->p, 0, sizeof(void *) * MAX);//为数组p初始化
queue->size = 0;
return queue;
}
(3)入队操作
int push(void *data, Queue *queue) //入队列,本质是尾插
{
if (data == NULL || queue == NULL)
return 0; //空指针判断,入队列失败
queue->p[queue->size] = data; //把data插入在队列末位
queue->size++; //队列大小+1
return 1;
}
(4)出队操作
void *pop(Queue *queue) //出队列,即pop出首个元素
{
if (queue == NULL || queue->size == 0)
return NULL;
void *data = queue->p[0]; //需要出队列的元素
for (int i = 0; i < queue->size - 1; i++)
queue->p[i] = queue->p[i + 1]; //更新队列,剩余的数据地址前移一位
free(queue->p[queue->size-1]);
queue->p[queue->size-1] = NULL;//将原队列末位数据置空
queue->size--; //队列大小减1
return data;
}
(5)队列大小与判断队列是否为空
int isEmptyQueue(Queue *queue)//判断队列是否为空
{
if (queue == NULL)
return -1;
return queue->size == 0; //空时返回1
}
int sizeOfQueue(Queue *queue)//返回队列的大小
{
if (queue == NULL)
return -1;
return queue->size;
}
2.2 链式队列的表示和实现
(1)结构体定义
图2.2-1 链式队列结构
//定义队列
typedef struct qNode{
int data;//整型数据(可以根据需要修改数据类型)
struct qNode *next;
}qNode,*queue;
typedef struct{
queue front;
queue rear;
}Queue;
(2)初始化操作
图 2.2-2 链式队列初始化
//初始化队列
void initQueue(Queue &Q){
Q.front=Q.rear=(queue)malloc(sizeof(qNode));//队头指针、队尾指针指向同一空结点
Q.front->next=NULL;
}
(3)入队操作
图2.2-3 入队操作
//入队列
void inQueue(Queue &Q,int data){
queue p;
p=(queue)malloc(sizeof(qNode));//分配一个结点空间
p->data=data;
p->next=NULL;
Q.rear->next=p;//队尾指针指向p
Q.rear=p;//队尾指针后移一位
printf("数据 %d 已经入队列\n\n",data);
}
(4)出队操作
图2.2-4 出队操作
//出队列
void outQueue(Queue &Q,int &data){
queue p;
p=(queue)malloc(sizeof(qNode));//分配一个结点空间
p=Q.front->next;//指定一个新的结点指向队头指针指向的结点
data=p->data;//返回结点数据值
Q.front->next=p->next;//将p指向的下一数据给队头指针,令下一数据前移到队头
printf("数据 %d 已经出队列\n\n",data);
if(Q.rear==p){
Q.rear=Q.front;//使队尾指针回到初始位置
printf("请注意:队列中已经没有数据!\n\n");//提示用户队列无数据
}
delete(p);//释放p所指结点空间
}
(5)输出队列全部字符与判断队列是否为空
//判断队列是否为空
bool emptyQueue(Queue &Q){
if(Q.front==Q.rear){
return false;
}else{
return true;
}
}
//输出链队列中储存的全部字符
void outQ(Queue Q){
queue q;//辅助变量
q=Q.front->next;
//如果链队列不为空,输出其全部字符
while(q){
printf("%d ",q->data);
q=q->next;
}
printf("\n");//换行
}
2.3 双向队列的表示和实现
这里可以采用双向链表实现,本人并没有实现。因此就暂时不放源码了
如果后续有补充会回来更新。
2.4 循环队列的表示和实现
(1)结构体定义
图2.4-1 循环队列结构
代码实现如下:
// 队列的顺序存储结构(循环队列)
#define MAX_QSIZE 5 // 最大队列长度+1
typedef struct {
int *base; // 初始化的动态分配存储空间
int front; // 头指针,若队列不空,指向队列头元素
int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
} SqQueue;
(2)初始化操作
// 构造一个空队列Q
SqQueue* Q_Init() {
SqQueue *Q = (SqQueue*)malloc(sizeof(SqQueue));
// 存储分配失败
if (!Q){
exit(OVERFLOW);
}
Q->base = (int *)malloc(MAX_QSIZE * sizeof(int));
// 存储分配失败
if (!Q->base){
exit(OVERFLOW);
}
Q->front = Q->rear = 0;
return Q;
}
(3)入队操作
图2.4-2 入队操作
代码实现如下:
// 插入元素e为Q的新的队尾元素
int Q_Put(SqQueue *Q, int e) {
if ((Q->rear + 1) % MAX_QSIZE == Q->front) // 队列满
return -1;
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAX_QSIZE;
return 1;
}
(4)出队操作
图2.4-3 出队操作
// 若队列不空,则删除Q的队头元素,用e返回其值,并返回1;否则返回-1
int Q_Poll(SqQueue *Q, int &e) {
if (Q->front == Q->rear) // 队列空
return -1;
e = Q->base[Q->front];
Q->front = (Q->front + 1) % MAX_QSIZE;
return 1;
}
(5)全部源码
注:这里我太懒了没有敲、源码来自于百度
// 队列的顺序存储结构(循环队列)
#define MAX_QSIZE 5 // 最大队列长度+1
typedef struct {
int *base; // 初始化的动态分配存储空间
int front; // 头指针,若队列不空,指向队列头元素
int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
} SqQueue;
// 构造一个空队列Q
SqQueue* Q_Init() {
SqQueue *Q = (SqQueue*)malloc(sizeof(SqQueue));
// 存储分配失败
if (!Q){
exit(OVERFLOW);
}
Q->base = (int *)malloc(MAX_QSIZE * sizeof(int));
// 存储分配失败
if (!Q->base){
exit(OVERFLOW);
}
Q->front = Q->rear = 0;
return Q;
}
// 销毁队列Q,Q不再存在
void Q_Destroy(SqQueue *Q) {
if (Q->base)
free(Q->base);
Q->base = NULL;
Q->front = Q->rear = 0;
free(Q);
}
// 将Q清为空队列
void Q_Clear(SqQueue *Q) {
Q->front = Q->rear = 0;
}
// 若队列Q为空队列,则返回1;否则返回-1
int Q_Empty(SqQueue Q) {
if (Q.front == Q.rear) // 队列空的标志
return 1;
else
return -1;
}
// 返回Q的元素个数,即队列的长度
int Q_Length(SqQueue Q) {
return (Q.rear - Q.front + MAX_QSIZE) % MAX_QSIZE;
}
// 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR
int Q_GetHead(SqQueue Q, int &e) {
if (Q.front == Q.rear) // 队列空
return -1;
e = Q.base[Q.front];
return 1;
}
// 打印队列中的内容
void Q_Print(SqQueue Q) {
int p = Q.front;
while (Q.rear != p) {
cout << Q.base[p] << endl;
p = (p + 1) % MAX_QSIZE;
}
}
// 插入元素e为Q的新的队尾元素
int Q_Put(SqQueue *Q, int e) {
if ((Q->rear + 1) % MAX_QSIZE == Q->front) // 队列满
return -1;
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAX_QSIZE;
return 1;
}
// 若队列不空,则删除Q的队头元素,用e返回其值,并返回1;否则返回-1
int Q_Poll(SqQueue *Q, int &e) {
if (Q->front == Q->rear) // 队列空
return -1;
e = Q->base[Q->front];
Q->front = (Q->front + 1) % MAX_QSIZE;
return 1;
}
三、经典实例
3.1 层次遍历
在信息处理中有一大类问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层 或当前行时就对下一层或下一行做预处理,把处理顺序安排好,等到当前层或当前行处理完毕,就可以处理下一层或下一行。
使用队列是为了保存下一一步的处理顺序。下面举一个二叉树层次遍历的例子
图 3.1-1 二叉树
图 3.1-2 二叉树遍历过程
该过程的简单描述如下:
1. 根节点入队
2. 若队空(所有节点均处理完毕),结束。否则重复3.
3. 队列中第一个结点出队,并访问之。若其有左孩子,若其有右孩子,则将右孩子入队,返回2.
3.2 队列在计算机系统中的应用
队列在计算机系统中的应用非常广泛,以下仅从两个方面来简述队列在计算机系统中的作 用:
1. 第一个方面是解决主机与外部设备之间速度不匹配的问题,
2. 第二个方面是解决由多用户引起 的资源竞争问题。
对于第一个方面,仅以主机和打印机之间速度不匹配的问题为例做简要说明。主机输出数据 给打印机打印,输出数据的速度比打印数据的速度要快得多,解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区写入打印数据。打印数据缓冲区中所存储的数据就是一个队列。
对于第二个方面,CPU 资源的竞争就是一个典型的例子。在一个带有多终端的计算机系统上,有多个用户需要CPU各自运行自己的程序,它们分别通过各自的终端向操作系统提出占用CPU的请求。操作系统通常按照每个请求在时间上的先后 顺序,把它们排成一个队列,每次把CPU分配给队首请求的用户使用。当相应的程序运行结束或 用完规定的时间间隔后,令其出队,再把CPU分配给新的队首请 个用户的请求,又使CPU能够正常运行。
3.3 其他实例
其他的实例还包含:划分子集问题、农夫过河问题、离散事件模拟、作业调度、迷宫问题。
在这里不再列举,大家可以自行搜索。
四、小结:栈与队列
本章介绍了两种特殊的线性表:栈和队列,主要内容如下。
(1) 栈是限定仅在表尾进行插入或删除的线性表,又称为后进先出的线性表。栈有两种存储表示,顺序表示(顺序栈)和链式表示(链栈)。栈的主要操作是进栈和出栈,对于顺序栈的进栈 和出栈操作要注意判断栈满或栈空。
(2) 队列是一种先进先出的线性表。它只允许在表的一端进行插入,而在另一端删除元素。 队列也有两种存储表示,顺序表示(循环队列)和链式表示(链队)。队列的主要操作是进队和出 队,对于顺序的循环队列的进队和出队操作要注意判断队满或队空。凡是涉及队头或队尾指针的 修改都要将其对MAXQSIZE求模。
(3) 栈和队列是在程序设计中被广泛使用的两种数据结构,其具体的应用场景都是与其表示 方法和运算规则相互联系的。下表分别从逻辑结构、存储结构和运算规则三方面对二者进行了比较。
队列 比较项目 | 栈 | 队列 |
逻辑结构 | 和线性表一样,元素之间存在一对一的关系 | 和线性表一样,元素之间存在一对一的关系 |
存储结构 | 顺序存储: 存储空间预先分配,可能会导致空间闲置或 栈满溢出现象;数据元素个数不能自由扩充 | 顺序存储(常设计成循环队列形式): 存储空间预先分配,可能会导致空间闲置或 队满溢出现象;数据元素个数不能自由扩充 |
链式存储: 动态分配,不会出现闲置或栈满溢出现象; 数据元素个数可以自由扩充 | 链式存储: 动态分配,不会出现闲置或队满溢出现象; 数据元素个数可以自由扩充 | |
运算规则 | 插入和删除在表的一端(栈顶)完成,后进先出 | 插入运算在表的一端(队尾)进行,删除运算在表的另一端(队头),先进先出 |
(4)栈有一个重要应用是在程序设计语言中实现递归。递归是程序设计中最为重要的方 法之一,递归程序结构清晰,形式简洁。但递归程序在执行时需要系统提供隐式的工作栈来 保存调用过程中的参数、局部变量和返回地址,因此递归程序占用内存空间较多,运行效率较低。