队列和栈作为一种最简单最基本的常用数据结构,可以说在许多方面都应用广泛。在程序运行时,他们可以保存程序运行路径中各个点的信息,以便用于回溯操作或其他需要访问已经访问过的节点信息的操作。这里对队列的特点、作用做出描述、并简单地用不同途径实现了队列的基本功能。本文的代码实现均为类C语言(节点用结构体封装,部分语法为C++,比如引用),或者纯C++。
什么是队列?
队列以一种先入先出(FIFO)的线性表,还有一种先入后出的线性表(FILO)叫做栈。
教科书上有明确的定义与描述。类似于现实中排队时的队列(队尾进,队头出),队列只在线性表两端进行操作,插入元素的一端称为表尾,删除(取出)元素的一端称为表头。分别对应于 入队和出队操作。
存储结构
对应于线性存储结构,称为顺序队列,链式存储结构称为链队。实现分别用数组和链表。
顺序队列的实现非常简单。
#include<iostream>
#include<stdlib.h>
#define MaxSize 100
using namespace std;
typedef struct
{
int data[MaxSize];
int front,rear; //队首、队尾 指针
}SqQueue; //队列中两端都会发生变化,所以用头尾指针表示两端的变化
void InitQueue(SqQueue *&q);
void DestroyQueue(SqQueue *&q);
bool QueueEmpty(SqQueue *q);
bool InQueue(SqQueue *&q,int e); //入队insert
bool DeQueue(SqQueue *&q,int &e);//出队delete
int main(void)
{
system("pause");
return 0;
}
void InitQueue(SqQueue *&q)
{
q = (SqQueue *)malloc(sizeof(SqQueue));
q->front = q->rear = -1;
}
void DestroyQueue(SqQueue *&q)
{
free(q);
q = NULL;
}
bool QueueEmpty(SqQueue *q)
{
return q->front == q->rear;//为空
}
bool InQueue(SqQueue *&q,int e)
{
if(q->rear == MaxSize-1)//队满上溢出
//环形队列--------if((p->rear+1)%MaxSize == front)
return false;
q->rear ++;
//环形队列--------q->rear = (q->rear+1)%MaxSize;
q->data[q->rear] = e;
return true;
}
bool DeQueue(SqQueue *&q,int &e)
{
if(q->front == q->rear)//队空下溢出
return false;
q->front ++;
//环形队列---------p->front = (p->front+1)%MaxSize;
e = q->data[q->front];
return true;
}
而线性队列在使用中会出现假溢出。即判断队列已满,但实际上并非所有位置都存放了元素。可以通过每次出队后将队列中所有元素前移一个位置解决,但这样会造成很高的额外时间消耗。采用环形队列可以解决这一问题。
环形队列
增加一个判满函数如下:
bool QueueFull(SqQueue *q)
{
if((q->rear+1)%MaxSize == q->front || (q->front == -1 && q->rear == MaxSize-1))
{
return true;
}
return false;
}
插入函数修改为:
bool InQueue(SqQueue *&q,int e)
{
if(QueueFull(q))
return false;
q->rear = (q->rear+1)%MaxSize;
q->data[q->rear] = e;
return true;
}
删除函数修改为:
bool DeQueue(SqQueue *&q,int &e)
{
if(QueueEmpty(q))
return false;
q->front = (q->front+1)%MaxSize;
e = q->data[q->front];
}
遍历函数为:
void TraverseQueue(SqQueue *q)//从表头到表尾
{
for(int i=0;i<QueueLength(q);i++)
{
cout << q->data[(i+q->front+1)%MaxSize] << endl;
}
cout << endl;
}
其余函数均与顺序队列一致。
环形队列在C++中采用类实现如下:
#include<iostream>
#include<stdlib.h>
using namespace std;
/*******************实现环形队列*************/
class MyQueue
{
public:
MyQueue(int queueCapacity);//创建队列
virtual ~MyQueue();//销毁队列
void ClearQueue();//清空队列
bool QueueEmpty() const;//判断队列是否为空
bool QueueFull() const;//判满队列
int QueueLength() const;//队列长度
bool InQueue(int element);//新元素入队
bool DeQueue(int &element);//首元素出队
void QueueTraverse();//遍历队列
private:
int *m_pQueue; //队列数组指针
int m_iQueueLen; //队列元素个数
int m_iQueueCapacity; //队列数组容量
int m_iHead;//队头,实质是数组下标
int m_iTail; //队尾
};
//构造函数,创建队列
MyQueue::MyQueue(int queueCapacity)
{
m_iQueueCapacity = queueCapacity;
m_iHead = 0;
m_iTail = 0;
m_iQueueLen = 0;//ClearQueue();
m_pQueue = new int[m_iQueueCapacity];
}
// 析构函数,销毁队列
MyQueue::~MyQueue()
{
delete []m_pQueue;
m_pQueue = NULL;
}
//清空队列
void MyQueue::ClearQueue()
{
m_iHead = 0 ;
m_iTail = 0 ;
m_iQueueLen = 0;
}
//判空队列
bool MyQueue::QueueEmpty() const
{
return m_iQueueLen == 0;
//m_iQueueLen == 0 ? true : false;
}
//判满
bool MyQueue::QueueFull() const
{
if(m_iQueueCapacity == m_iQueueLen)
{
return true;
}
else
{
return false;
}
}
//获取队列长度
int MyQueue::QueueLength() const
{
return m_iQueueLen;
}
//新元素入队
bool MyQueue::InQueue(int element)
{
if(QueueFull())
{
return false;
}
else
{
m_pQueue[m_iTail] = element;
m_iTail ++;
m_iTail %= m_iQueueCapacity;
m_iQueueLen ++;
return true;
}
}
//首元素出队
bool MyQueue::DeQueue(int &element)
{
if(QueueEmpty())
{
return false;
}
else
{
element = m_pQueue[m_iHead];
m_iHead ++ ;
m_iHead %= m_iQueueCapacity;
m_iQueueLen --;
return true;
}
}
//遍历队列
void MyQueue::QueueTraverse()
{
for(int i=m_iHead; i < m_iHead + m_iQueueLen; i++)
{
cout << m_pQueue[i%m_iQueueCapacity] << endl;
}
}
int main(void)
{
//检测一下环形队列是否写正确了
MyQueue *p = new MyQueue(4);
p->InQueue(10);
p->InQueue(20);
p->InQueue(23);
p->InQueue(78);
p->QueueTraverse();
int e = 0;
p->DeQueue(e);
cout << endl;
cout << e << endl;
cout << endl;
p->QueueTraverse();
p->ClearQueue();
if(p->QueueEmpty())
{
cout << "The queue is empty!" << endl;
}
p->InQueue(238);
p->InQueue(34);
p->QueueTraverse();
cout << "The length of the queue is " << p->QueueLength() << endl;
p->InQueue(100);
p->InQueue(299);
if(p->QueueFull())
{
cout << "The queue is full!" << endl;
}
p->QueueTraverse();
p->~MyQueue();
cout << "You have destroyed your queue successfully!" << endl;
system("pause");
return 0;
}
如果数据元素是多个数据项组成,在C语言中可采用结构体将多个数据项封装在节点中,C++中可以用类将锁哥数据元素封装为一个数据对象。如果在不同应用场景下数据元素数据类型(封装类型或原有类型)不同,可以采用类模板设计实现代码重用。读者可以自行完成。
链队
在队列的链式存储结构中,可用不含头节点的链表表示。定义队列包含两个节点,其中一个为front指针指向表头,另一个为rear指针指向表尾。链队不存在满队的情况。
实现如下:
#include<iostream>
#include<stdlib.h>
using namespace std;
//链队
//用不含头节点的链表实现
//数据节点定义
typedef struct qnode
{
int data;
qnode *next;
}QNode;
//链队定义
typedef struct Queue
{
QNode *front;
QNode *rear;
}LiQueue;
void InitQueue(LiQueue *&q)//初始化
{
q = (LiQueue *)malloc(sizeof(Queue));
q->front = NULL;
q->rear = NULL;
}
void DestroyQueue(LiQueue *&q)//销毁
{
QNode *p = q->front,*r;
while(p != NULL)
{
r = p;
p = p->next;
free(r);
}
free(q);
}
bool QueueEmpty(LiQueue *q)
{
return NULL == q->rear;
}
void InQueue(LiQueue *&q,int e)//入队不会失败
{
QNode *p = (QNode *)malloc(sizeof(QNode));
p->data = e;
p->next = NULL;
if(QueueEmpty(q))
{
q->front = p;
q->rear = p;
}
else
{
q->rear->next = p;
q->rear = p;
}
}
bool DeQueue(LiQueue *&q,int &e)//出队
{
if(QueueEmpty(q))//队为空
{
return false;
}
QNode *t = q->front;
if(q->front == q->rear)//队中只含一个数据元素
{
q->front = q->rear = NULL;
}
else//队中含有两个及以上数据元素
{
q->front = t->next;
}
e = t->data;
free(t);
t = NULL;
return true;
}
bool TraverseQueue(LiQueue *q)
{
if(QueueEmpty(q))
{
return false;
}
QNode *p = q->front;
while(p != NULL)
{
cout << p->data << endl;
p = p->next;
}
cout << endl;
return true;
}
int main(void)
{
LiQueue *q;
InitQueue(q);
InQueue(q,1);
InQueue(q,2);
InQueue(q,3);
InQueue(q,4);
TraverseQueue(q);
int elem = 0;
DeQueue(q,elem);
cout << "The element you deleted is :" << elem << endl;
TraverseQueue(q);
DeQueue(q,elem);
DeQueue(q,elem);
DeQueue(q,elem);
if(QueueEmpty(q))
{
cout << "The List Queue is empty!" << endl;
}
TraverseQueue(q);
DestroyQueue(q);
system("pause");
return 0;
}
典型应用
在具体的程序设计中,只要涉及到先进先出的设计,即采用了队列的思想。
队列的一个典型应用就是求解——迷宫问题。
迷宫问题是指:给定给定一个M×N的迷宫图、入口与出口、行走规则。求一条从指定入口到出口的路径。
所求路径必须是简单路径,即路径不重复。
所求路径必须是简单路径,即路径不重复。
迷宫问题可以用栈或者队列来求解。其中使用队列求解出的路径是最短路径。
迷宫采用二维数组来表示,其中路用0表示,墙用1表示。为了求解问题的方便,通常在数组的周围加上围墙,即在周围加上两行和两列。形成M+2行,N+2列的迷宫数组。
求解思路:使用顺序队列(使用顺序队列的原因是:出队入队操作并不会删除结点,只是改变了队首队尾指针的值,最终还要通过队列中已出队节点来回溯得到路径),队列中的数据元素类型为格点坐标(i,j)和路径中上一格点在队列中的位置pre的封装。pre的设置是为了找到终点后由终点通过pre回溯到起点从而逆序打印出路径(采用递归实现)。在将一个能走的格点入队后,循环搜索它周围的四个格点,并将其中能走的入队,所以必须制定四个方向的搜索顺序(最后若有多条最短路径,则打印出哪一条由搜索顺序决定)。由于路径不重复,所以在在入队后将一个迷宫格点的值赋为-1,避免重复搜索。整体思路类似于广度优先搜索。
实现如下:
由于队列操作简单,其中并没有定义出出队,入队等函数。
注意最后打印出的坐标为(行号,列号),并不是惯用的横纵坐标。
#include<iostream>
#include<stdlib.h>
using namespace std;
const int MaxSize = 100;
typedef struct
{
int i,j;//迷宫块坐标
int pre;//当前路径中前一方块在队列中的位置
}Box;
typedef struct Queue
{
Box data[MaxSize];
int rear,front;//front指向当前队头的前一元素,rear指向队尾
}SqQueue;
//全局数组maze表示迷宫
const int M=4,N=4;
int maze[M+2][N+2] = { {1, 1, 1, 1, 1, 1}, //迷宫示例
{1, 0, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1} };
bool MazePath(int xi,int yi,int xe,int ye);
void print(SqQueue q,int n);
int main(void)
{
if(MazePath(1,1,4,4))
{
cout << "有路径,如上~" << endl;
}
else
{
cout << "没有路径~" << endl;
}
system("pause");
return 0;
}
//求迷宫路径算法,xi,yi入口坐标,xe,ye出口坐标
bool MazePath(int xi,int yi,int xe,int ye)//x表行号,y表列号
//搜索路径(xi,yi)->(xe,ye)
{
int i,j;
bool find = false;//找到出口置1
SqQueue qu;//在栈中分配内存
qu.rear = qu.front = -1;
qu.rear ++;
qu.data[qu.rear].i = xi;
qu.data[qu.rear].j = yi;//(xi,yi)入队
qu.data[qu.rear].pre = -1;//表示在队列中没有位于它之前的元素,作为搜索路径时的结束条件
maze[xi][yi] = -1;//将0置为-1,避免重复搜索
while(qu.front != qu.rear && !find)//当队列不空且没有找到出口时循环
{
qu.front ++;
i = qu.data[qu.front].i;
j = qu.data[qu.front].j;//i表行,j表列
if(i==xe && j==ye)
{
find = true;
print(qu,qu.front);//打印路径,从当前格点(终点)开始追溯递归打印路径
return true;//找到出口
}
//将(i,j)周围四个格点中为路且没有走过的格点进队
for(int di=0;di<4;di++)//di表示查找方向,0->3顺时针旋转,分别为上右下左
{
switch(di)
{
case 0: i=qu.data[qu.front].i-1;
j=qu.data[qu.front].j;
break;
case 1: i=qu.data[qu.front].i;
j=qu.data[qu.front].j+1;
break;
case 2: i=qu.data[qu.front].i+1;
j=qu.data[qu.front].j;
break;
case 3: i=qu.data[qu.front].i;
j=qu.data[qu.front].j-1;
break;
}
if(maze[i][j] == 0)
{
qu.rear ++;
qu.data[qu.rear].i = i;
qu.data[qu.rear].j = j;
qu.data[qu.rear].pre = qu.front;//上一个出队元素在队列中的标号
maze[i][j] = -1;
}
}
}
return false;//未找到路径返回false
}
//递归打印路径
void print(SqQueue q,int n)
{
if(q.data[n].pre == -1)
{
cout << "(" << q.data[n].i << "," << q.data[n].j << ")" << endl;
return;//return 必须写
}
print(q,q.data[n].pre);
cout << "(" << q.data[n].i << "," << q.data[n].j << ")" << endl;
}
队列的应用非常广泛,比如在图的广度优先遍历中。作为一种最简单的数据结构,限制性的线性表,当然用线性表也可以实现队列的所有功能,但正是由于栈和队列太常用,才单独抽象成一种数据结构。后续会有关于栈的文章。