常用資料結構——佇列及其應用
阿新 • • 發佈:2019-01-31
佇列和棧作為一種最簡單最基本的常用資料結構,可以說在許多方面都應用廣泛。在程式執行時,他們可以儲存程式執行路徑中各個點的資訊,以便用於回溯操作或其他需要訪問已經訪問過的節點資訊的操作。這裡對佇列的特點、作用做出描述、並簡單地用不同途徑實現了佇列的基本功能。本文的程式碼實現均為類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; }
環形佇列
環形佇列即將陣列的頭和尾連線起來構成環形。為了使隊滿的條件不與隊空的條件(front == rear)衝突。可以捨棄一個元素的儲存空間,隊頭front指向隊頭的上一個位置,隊尾rear指向隊尾元素。這樣隊滿條件變為(rear+1)%MaxSize == front. 增加一個判滿函式如下:插入函式修改為: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;
}
佇列的應用非常廣泛,比如在圖的廣度優先遍歷中。作為一種最簡單的資料結構,限制性的線性表,當然用線性表也可以實現佇列的所有功能,但正是由於棧和佇列太常用,才單獨抽象成一種資料結構。後續會有關於棧的文章。