資料結構之——佇列
阿新 • • 發佈:2018-11-10
1. 佇列的特點?
-
佇列是一種先進先出的資料結構。類似於我們日常排隊購物,先到的人排在前面,自然優先被服務。棧有出棧和入棧兩個基本操作,佇列也只有兩個基本操作,從隊尾入隊 enqueue,從隊頭出隊 dequene。
-
佇列可以用陣列或者連結串列實現,用陣列實現的佇列叫作順序佇列,用連結串列實現的佇列叫作鏈式佇列。
2. 順序佇列?
- C++ 實現簡易順序佇列。首先,定義一個固定長度的陣列,然後用兩個陣列的索引值分別表徵佇列的隊頭和隊尾,入隊則隊尾索引值增 1,出隊則隊頭索引值增 1。
- 有一種特殊情況,隊尾元素一直進隊到達了陣列上限,但前面有元素出隊,這時候,佇列其實還有儲存空間沒有利用,我們就將佇列的資料都往前平移,騰出一些空間來。
class ArrayQueue
{
private:
int *data; // 順序佇列
int head; // 佇列頭索引值
int tail; // 佇列尾索引值
int len; // 佇列的大小
public:
// 初始化佇列,申請一個大小為 n 的陣列
ArrayQueue(int n)
{
data = new int[n];
head = 0;
tail = 0;
len = n;
}
bool enqueue(int item)
{
// 佇列頭索引值為零且佇列尾索引值等於陣列長度,佇列空間已滿,返回 false,入隊失敗
if (tail == len && head == 0)
{
cout << "queue is full" << endl;
return false;
}
else
{// 若只有佇列尾索引值等於陣列長度,向前平移資料
if (tail == len && head != 0)
{
int num = tail - head;
for (int i = 0; i < num; i++)
{
data[i] = data[len - num + i];
}
head = 0;
tail = num;
}
// 佇列空間未滿,入隊,佇列尾索引值加 1
data[tail] = item;
tail++;
return true;
}
}
int dequeue()
{
// 佇列頭等於佇列尾,佇列為空,返回 -1,出隊失敗
if (head == tail)
{
cout << "queue is empty" << endl;
return -1;
}
// 佇列非空,隊頭元素出隊,佇列頭索引值加 1
else
{
int item = data[head];
head++;
return item;
}
}
};
2. 鏈式佇列?
- C++ 實現簡易鏈式佇列。定義頭指標指向哨兵結點,尾指標指向最後一個結點,入隊則在鏈尾插入一個新結點,出隊則刪除哨兵結點後的第一個結點。
// 單向連結串列
struct linked_list
{
int data;
linked_list *next;
};
class ListQueue
{
private:
linked_list *head; // 連結串列佇列的頭指標
linked_list *tail; // 連結串列佇列的尾指標
int num; // 佇列中元素個數
public:
// 初始化佇列,增加一個哨兵結點,方便連結串列操作
ListQueue()
{
head = new linked_list;
head->data = -1;
head->next = NULL;
tail = head; // 頭指標和尾指標都指向哨兵
num = 0;
}
// 入隊,在鏈尾插入新結點
bool enqueue(int item)
{
linked_list *node = new linked_list;
if (node)
{
node->data = item;
node->next = NULL;
tail->next = node; // 新增新結點
tail = node; // 更新尾指標
num++;
return true;
}
else // 記憶體不足,無法插入新結點,返回 false
{
return false;
}
}
int dequeue()
{
// 佇列為空,返回 -1,出隊失敗
//if (num == 0)
if (head == tail)
{
cout << "queue is empty" << endl;
return -1;
}
// 出隊,彈出哨兵結點後第一個結點的值,並刪除結點
else
{
if (head->next == tail) // 當佇列只有一個結點時,需要更改尾節點指向哨兵
{
tail = head;
}
int item = head->next->data;
num--;
linked_list * node = head->next;
head->next = node->next;
delete node;
return item;
}
}
};
4. 迴圈佇列?
- 用陣列來實現佇列,當隊尾到達陣列上限時,我們需要進行資料搬移來利用實際上處於空閒的記憶體空間,但這樣入隊操作的效能就會受到影響。
- 為了避免進行資料搬移,我們引入迴圈佇列來解決這個問題。
-
迴圈佇列實質上就是將陣列首尾相連組成一個環,當隊尾到達陣列上限但陣列頭還有空間時,我們就可以繼續沿著陣列頭繼續進隊。
-
實現迴圈佇列的關鍵就是確定佇列空和佇列滿的條件。類似順序佇列,當 head = tail 時,佇列為空;當 (head+1) % len = head 時,佇列為滿。而且,當佇列滿時, tail 指向的位置是沒有資料的。
class CircularQueue
{
private:
int *data; // 順序佇列
int head; // 佇列頭索引值
int tail; // 佇列尾索引值
int len; // 佇列的大小
public:
// 初始化佇列,申請一個大小為 n 的陣列
CircularQueue(int n)
{
data = new int[n];
head = 0;
tail = 0;
len = n;
}
bool enqueue(int item)
{
// 佇列空間已滿,返回 false,入隊失敗
if ((tail + 1) % len == head)
{
cout << "queue is full" << endl;
return false;
}
else
{
// 佇列空間未滿,入隊,佇列尾索引值加 1
data[tail] = item;
tail++;
// 到達陣列上限,索引值迴圈到 0
if (tail == len)
{
tail = 0;
}
return true;
}
}
int dequeue()
{
// 佇列頭等於佇列尾,佇列為空,返回 -1,出隊失敗
if (head == tail)
{
cout << "queue is empty" << endl;
return -1;
}
// 佇列非空,隊頭元素出隊,佇列頭索引值加 1
else
{
int item = data[head];
head++;
// 到達陣列上限,索引值迴圈到 0
if (head == len)
{
head = 0;
}
return item;
}
}
void print()
{
cout << head << " " << tail << endl;
}
};
3. 阻塞佇列和併發佇列?
- 阻塞佇列就是在佇列基礎上增加了阻塞操作。佇列為空時,出隊操作會被阻塞,直到佇列中有了資料才能返回;佇列滿時,入隊操作會被阻塞,直到佇列中有空閒位置時才能插入新資料。
- 使用阻塞佇列,我們可以輕鬆實現一個“生產者 - 消費者“模型。此外,我們還可以通過協調”生產者“和”消費者“的數量,來提高資料的處理效率。
- 在多執行緒情況下,會有多個執行緒同時操作佇列,要想實現一個執行緒安全的佇列,我麼就需要一個併發佇列。一種是在 enqueue、dequeue方法中加鎖,同一時刻只允許一個存或取操作;另一種則是基於迴圈佇列,利用 CAS 原子操作,可以實現非常高效的併發佇列。
3. 佇列線上程池等有限資源池中的應用?
執行緒池中沒有空閒執行緒時,新的任務請求執行緒資源時,執行緒池該如何處理?各種處理策略又是如何實現的呢?
-
一般有兩種策略。非阻塞的處理方式直接拒絕任務請求;阻塞的處理將請求進行排隊,等到有空閒執行緒時,取出請求進行處理。
-
基於連結串列,可以實現支援無限排隊的無界佇列(unbounded queue),但是可能會導致過多的請求排隊,會導致請求的響應時間過長。而基於陣列的有界佇列(bounded queue),佇列大小有限,超出的請求就會被拒絕。
獲取更多精彩,請關注「seniusen」!