1. 程式人生 > >JavaScript資料結構之佇列

JavaScript資料結構之佇列

接上篇-資料結構之棧

資料結構之---對列

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.tail();
      }
      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("&nbsp;");
            }
            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,當大量客戶端向服務端發起連線,而服務端擁擠時,就會形成佇列,先來的先處理,後來的後處理,當佇列滿時,新來的請求直接拋棄掉。 資料結構在系統設計中的應用非常廣泛,只是我們水平達不到那個級別,知道的太少,但如果能理解並掌握這些資料結構,那麼就有機會在工作中使用它們並解決一些具體的問題,當我們手裡除了錘子還有電鋸時,那麼我們的眼裡就不只是釘子,解決問題的思路也會更加開闊。

5.參考

阮一峰-函式的擴充套件