1. 程式人生 > >常用資料結構——佇列及其應用

常用資料結構——佇列及其應用

佇列和棧作為一種最簡單最基本的常用資料結構,可以說在許多方面都應用廣泛。在程式執行時他們可以儲存程式執行路徑中各個點的資訊,以便用於回溯操作或其他需要訪問已經訪問過的節點資訊的操作。這裡對佇列的特點、作用做出描述、並簡單地用不同途徑實現了佇列的基本功能。本文的程式碼實現均為類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;
}


佇列的應用非常廣泛,比如在圖的廣度優先遍歷中。作為一種最簡單的資料結構,限制性的線性表,當然用線性表也可以實現佇列的所有功能,但正是由於棧和佇列太常用,才單獨抽象成一種資料結構。後續會有關於棧的文章。