資料結構之---佇列
1.佇列的定義
佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(end)進行插入操作,和棧一樣,佇列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊首。
佇列的資料元素又稱為佇列元素。在佇列中插入一個佇列元素稱為入隊,從佇列中刪除一個佇列元素稱為出隊。因為佇列只允許在一端插入,在另一端刪除,所以只有最早進入佇列的元素才能最先從佇列中刪除,故佇列的特性為 先進先出 (First-In-First-Out,FIFO)
請看下面的圖解
佇列新增新的元素,左側是佇列的頭部,右側是佇列的尾部,新的元素如果想進入佇列,只能從尾部進入。 佇列移除元素,左側是佇列的頭部,右側是佇列的尾部, 如果想要出佇列,只能從佇列的頭部出去 日常生活中,排隊就是典型的佇列結構,先到的先被服務,後來的在隊尾等著,直到輪到他為止(當然,特殊情況除外)。比如說其他場景 提交作業系統執行的一系列程式、列印任務池等,一些模擬系統用佇列來模擬銀行或雜貨 店裡排隊的顧客。2.佇列的實現
從資料儲存的角度看,實現佇列有兩種方式,一種是以陣列做基礎,一種是以連結串列做基礎,陣列是最簡單的實現方式,本文以基礎的陣列來實現佇列。
佇列的操作包括建立佇列、銷燬佇列、入隊、出隊、清空佇列、獲取隊頭元素、獲取佇列的長度。
我們定義以下幾個佇列的方法:
- enqueue 從隊尾新增一個元素(新來一個辦業務的人,排在了隊尾)
- dequeue 從隊首刪除一個元素(隊伍最前面的人,辦完了業務,離開了)
- head 返回隊首的元素(後邊的人好奇看一下,隊伍最前面的人是誰)
- tail 返回隊尾的元素(前邊的人好奇看一下,隊伍最後面的人是誰)
- size 返回佇列的大小(營業員數一下隊伍有多少人)
- isEmpty 返回佇列是否為空(營業員檢視當前是不是有人在排隊)
- clear 清空佇列(此視窗暫停營業,大家撤了吧)
然後我們利用es6的class的實現以上的方法 新建一個queue.js檔案
class Queue {
constructor() {
this.items = []; // 儲存資料
}
enqueue(item) { // 向隊尾新增一個元素
this.items.push(item);
}
dequeue() { // 刪除隊首的一個元素
return this.items.shift();
}
head() { // 返回隊首的元素
return this.items[0];
}
tail() { // 返回隊尾的元素
return this.items[this.items.length - 1];
}
size() { // 返回佇列的元素
return this.items.length;
}
isEmpty() { // 返回佇列是否為空
return this.items.length === 0;
}
clear() { // 清空佇列
this.items = [];
}
}
複製程式碼
3.佇列的應用
記住兩點:
- 棧的特性是先進後出(聯想:羽毛球桶)
- 佇列的特性是先進先出(聯想:排隊)
3.1 約瑟夫環問題
有一個陣列存放了100個資料0-99,要求每隔兩個數刪除一個數,到末尾時再迴圈至開頭繼續進行,求最後一個被刪除的數字。
比如說:有十個數字:0,1,2,3,4,5,6,8,9,每隔兩個數刪除一個數,就是2 5 8 刪除,如果只是從0到99每兩個數刪除一個數,其實挺簡單的,但是我們還得考慮到末尾的時候還有再重頭開始,還得考慮刪除掉的元素從陣列中刪除。那我們如果佇列的話,就比較簡單了
3.1.2 思路分析
- 先將這100個資料放入佇列,用while迴圈,終止的條件是佇列裡只有一個元素。
- 定義index變數從0開始計數,從佇列頭部刪除一個元素,index + 1
- 如果index%3 === 0 ,說明這個元素需要被移除佇列,否則的話就把它新增到佇列的尾部
經過while迴圈後,不斷的有元素出佇列,最後隊伍中只會剩下一個被刪除的元素
3.1.3 看程式碼實現
// 每隔兩個數刪除一個數
{
var arr = []; // 準備0-99 100個資料
for (var i = 0; i < 100; i++) {
arr.push(i);
}
function delRang(arr) {
var queue = new Queue(); // 呼叫之前實現Queue類
var len = arr.length;
for (var i = 0; i < len; i++) {
queue.enqueue(i); // 將資料存入佇列
}
var index = 0;
while (queue.size() !== 1) { // 迴圈判斷佇列裡大小否為還剩下1個
var item = queue.dequeue(); // 出隊一個元素,根據當前的index來判斷是否需要移除
index += 1;
if (index % 3 !== 0) {
queue.enqueue(item); // 不是的話,則新增到隊尾,繼續迴圈
}
}
console.log(queue.head()); // 90
return queue.head(); // 返回最後一個元素
}
delRang(arr);
}
複製程式碼
是不是感覺使用佇列很簡單呢,接下來再看幾個小練習
3.2 斐波那契數列
3.2.1 題目介紹
什麼是斐波那契數列: 斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……這個數列從第3項開始,每一項都等於前兩項之和。在數學上,斐波納契數列以如下被以遞迴的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)來源:斐波那契數列——百度百科
3.2.2 我們先考慮使用普通的方法實現 -- 遞迴 遞迴版 程式碼實現
function Fibonacci (n) {
if ( n <= 2 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 55
Fibonacci(100) // 堆疊溢位
Fibonacci(500) // 堆疊溢位
複製程式碼
由上可見,遞迴非常消耗記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤。但是也有解決的辦法,採用尾遞迴優化。
函式呼叫自身,稱為遞迴;如果尾呼叫自身,就稱為尾遞迴。 對於尾遞迴來說,由於只存在一個呼叫棧,所以永遠不會發生“棧溢位”錯誤。
尾遞迴版 程式碼實現
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 2 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 354224848179262000000
Fibonacci2(1000) // 4.346655768693743e+208
複製程式碼
上面程式碼雖然簡潔,但是不易想到
3.2.3 那接下來我們用佇列實現一遍 思路分析
- 需要先將兩個1 新增到佇列中
- 定義index來計數,採用while迴圈,終止條件是 index < n - 2(因為每次遍歷我們只保留2個元素在佇列中)
- 使用dequeue方法移除佇列頭部的元素,標記為 numDel;
- 使用head方法獲取此時頭部的元素,標記為numHead;
- 使用enqueue方法將前兩者的和從尾部放入佇列中
- index + 1
當迴圈結束後,佇列裡面只有兩個元素,用dequeue方法移除頭部元素後,再用head方法獲取的頭部元素就是最終的結果,而且此方法不會產生“棧溢位”錯誤。
佇列版 程式碼實現
{
function fibonacci(n) {
if (n <= 2) return 1;
var queue = new Queue();
// 先存入序列的前兩個值
queue.enqueue(1);
queue.enqueue(1);
var index = 0;
while (index < n - 2) {
var delItem = queue.dequeue(); // 移除佇列的頭部元素
var headItem = queue.head(); // 獲取佇列頭部元素(因為上一步已經將頭部元素移除)
var resNum = delItem + headItem;
queue.enqueue(resNum); // 將兩者之和存入佇列
index += 1;
}
queue.dequeue();
return queue.head();
}
console.log("fibonacci", fibonacci(10)); // 55
console.log("fibonacci", fibonacci(100)); // 354224848179262000000
}
複製程式碼
3.3 列印楊輝三角
3.3.1 題目分析 所謂楊輝三角,大家肯定都不會陌生,如下圖所示 楊輝三角——百度百科介紹 計算的方式:f[i][j] = f[i-1][j-1] + f[i-1][j], i 代表行數,j代表一行的第幾個數,如果j= 0 或者 j = i ,則 f[i][j] = 1。
3.3.2 思路分析
- 楊輝三角中的每一行,都依賴於上一行,假設現在佇列裡已經儲存了第n-1行的資料,那麼輸出第n行時,只需要將佇列裡的資料依次出佇列,進行計算得到下一行的數值並講計算所得儲存到佇列中
- 然後我們需要兩層for迴圈,將n-1行和n行的資料分開列印;有上圖可以得出規律,n行只有n個數,所以我們就可以使用for迴圈控制enqueue的次數,n次結束後,佇列裡儲存的就是計算好的第n+1行的資料
3.3.3 程式碼實現
<!DOCTYPE html>
<html lang="en">
<head>
<title>列印楊輝三角</title>
</head>
<body>
<script src="./queue.js"></script>
<script>
// 楊輝三角
{
function yangHui(n) {
var queue = new Queue();
queue.enqueue(1); // 先在佇列中儲存第一行的資料
for (var i = 1; i <= n; i++) { // 第一層迴圈控制層數
var line = "";
var pre = 0;
for (var j = n; j > i; j--) { // 列印空格
document.write(" ");
}
for (var j = 0; j < i; j++) { // 第二層控制當前層的資料
var item = queue.dequeue();
var value = item + pre; // 計算下一行的值
pre = item;
line += item + " ";
queue.enqueue(value);
}
queue.enqueue(1); // 將每層的最後一個數值 1 存入佇列中
document.write(line + "<br />");
}
}
yangHui(10);
}
</script>
</body>
</html>
複製程式碼
4.佇列總結
使用佇列的例子還有很多,比如逐層列印一顆樹上的節點,還有訊息通訊使用的socket,當大量客戶端向服務端發起連線,而服務端擁擠時,就會形成佇列,先來的先處理,後來的後處理,當佇列滿時,新來的請求直接拋棄掉。 資料結構在系統設計中的應用非常廣泛,只是我們水平達不到那個級別,知道的太少,但如果能理解並掌握這些資料結構,那麼就有機會在工作中使用它們並解決一些具體的問題,當我們手裡除了錘子還有電鋸時,那麼我們的眼裡就不只是釘子,解決問題的思路也會更加開闊。