天天看點

【前端算法系列】隊列queue

933.最近的請求次數

  • 有新請求就入隊,3000ms前發出的請求出隊
  • 隊列的長度就是最近請求次數
/** 時間複雜度:O(n) while循環體 n為踢出對的請求個數
 *  空間複雜度:O(n) 定義了一個數組,n為隊列的長度
 */
var RecentCounter = function() {
    this.q = []
};
RecentCounter.prototype.ping = function(t) {
    // 新請求入隊
    this.q.push(t)
    // 把不在時間範圍内的老請求出隊
    while(this.q[0]<t-3000){
        this.q.shift()
    }
    return this.q.length
};
           

資料流中的第K大元素

int k = 3;

int[] arr = [4,5,8,2];

KthLargest kthLargest = new KthLargest(3, arr);

kthLargest.add(3); // returns 4

kthLargest.add(5); // returns 5

思路:

方法1:儲存前K個最大的值;全部排序

方法2:優先隊列,每次把最大的放在最上面,或把最小的放在最上面

小頂堆:把最小的放根結點,是以要保證元素個數等于K(意思是這個堆的個數為K個,最頂的是最小的,周遊數組的元素,如果有比它小的就不用進來排隊了,如果比它大,就把最小的踢掉,剩下的元素重新調整log2^k)

239.滑動視窗最大值

輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3

輸出: [3,3,5,5,6,7]

解釋:

滑動視窗的位置 最大值

--------------- -----

[1 3 -1] -3 5 3 6 7 ------> 3

1 [3 -1 -3] 5 3 6 7 ------> 3

1 3 [-1 -3 5] 3 6 7 ------> 5

1 3 -1 [-3 5 3] 6 7 ------> 5

1 3 -1 -3 [5 3 6] 7 ------> 6

1 3 -1 -3 5 [3 6 7] ------> 7

所有的滑動視窗題用隊列去處理

【前端算法系列】隊列queue
  • 方法1:暴力求解,枚舉視窗的起點位置,起點開始到len-k的最大值,寫兩個嵌套循環O(n*K)
const maxSlidingWindow = function (nums, k) {
    const len = nums.length
    const res = []
    let left = 0, right = k-1
    // 當數組沒有被周遊完時,執行循環體内的邏輯
    while(right < len){
        const max = calMax(nums, left, right)
        res.push(max)
        // 左右指針前進一步
        left++
        right++
    }
    return res
}
function calMax(arr, left, right){
    if (!arr || !arr.length) return
    // 初始化為第一個元素
    let maxNum = arr[left] 
    for(let i = left; i<=right; i++){
        // 誰比我大,我就變成誰
        if(arr[i] > maxNum) maxNum = arr[i]
    }
    return maxNum
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3))
           

上述通過周遊來更新最大值産生了K

  • 方法2:線性隊列,O(n+k)即O(n)一次解決

    1)設定一個雙端隊列,存儲nums的下标

    2)周遊每個元素,拿元素跟雙端隊列裡的隊尾做比較(雙端隊列:比我小的都踢出去)

    目前元素小于隊尾,就直接入隊,否則,将隊尾元素逐個出隊,直到隊尾元素大于等于目前元素為止(比如,視窗移到2 3 4位置,5進來的時候,要跟視窗其他數對比,隻要比5小,都可以從這個棧彈出)

    3)檢查隊頭元素,看隊頭元素是否已經被排除在滑動視窗的範圍之外了(即隊頭小于i-k)。如果是,則将隊頭元素出隊

    4)判斷滑動視窗狀态,被周遊的元素個數小于K,表示還不能動結果數組,隻能繼續更新隊列;如果大于等于k,表示最大值已經出現,隊頭元素就是最大值

const maxSlidingWindow = function (nums, k) {
  // 緩存數組的長度
  const len = nums.length;
  // 初始化結果數組
  const res = [];
  // 初始化雙端隊列
  const deque = [];
  // 開始周遊數組
  for (let i = 0; i < len; i++) {
    // 當隊尾元素小于目前元素時
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      // 将隊尾元素(索引)不斷出隊,直至隊尾元素大于等于目前元素
      deque.pop();
    }
    // 入隊目前元素索引(注意是索引)
    deque.push(i);
    // 當隊頭元素的索引已經被排除在滑動視窗之外時
    while (deque.length && deque[0] <= i - k) {
      // 将隊頭元素索引出隊
      deque.shift();
    }
    // 判斷滑動視窗的狀态,隻有在被周遊的元素個數大于 k 的時候,才更新結果數組
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  // 傳回結果數組
  return res;
}
           

設計循環隊列

循環隊列是一種線性資料結構,其操作表現基于 FIFO(先進先出)原則并且隊尾被連接配接在隊首之後以形成一個循環。它也被稱為“環形緩沖器”

循環隊列的一個好處是我們可以利用這個隊列之前用過的空間。在一個普通隊列裡,一旦一個隊列滿了,我們就不能插入下一個元素,即使在隊列前面仍有空間。但是使用循環隊列,我們能使用這些空間去存儲新的值。

循環隊列可以高效利用存儲空間,普通隊列需要很大的存儲空間,存儲空間會往上跑,循環隊列可以避免這個問題。

你的實作應該支援如下操作:

MyCircularQueue(k): 構造器,設定隊列長度為 k 。

Front: 從隊首擷取元素。如果隊列為空,傳回 -1 。

Rear: 擷取隊尾元素。如果隊列為空,傳回 -1 。

enQueue(value): 向循環隊列插入一個元素。如果成功插入則傳回真。

deQueue(): 從循環隊列中删除一個元素。如果成功删除則傳回真。

isEmpty(): 檢查循環隊列是否為空。

isFull(): 檢查循環隊列是否已滿。

【前端算法系列】隊列queue
class MyCircularQueue{
    constructor(k) {
        // 用來儲存資料長度為k的資料結構
        this.list = Array(k)
        // 隊首指針
        this.front = 0
        // 隊尾的指針
        this.rear = 0
        // 隊列的長度
        this.max = k
    }
    // 入隊:rear指針指向哪裡,哪裡就可以入隊,且rear指針+1
    enQueue(num) {
        if (this.isFull()) {
            return false
        } else {
            this.list[this.rear] = num
            // 指針向後移動
            this.rear = (this.rear + 1) % this.max
            return true
        }
    }
    // 出隊:front指針指向誰,誰就出隊,且front指針+1
    deQueue() {
        let v = this.list[this.front]
        this.list[this.front] = ''
        console.log('front----->'+this.front)
        // front指針往後移動
        this.front = (this.front + 1) % this.max
        console.log('front222----->'+this.front)
        return v
    }
    isEmpty() {
        return this.front === this.rear && !this.list[this.front]
    }
    isFull() {
        return this.front === this.rear && !!this.list[this.front]
    }
    Front() {
        return this.list[this.front]
    }
    rear() {
        let rear = this.rear - 1
        return this.list[rear < 0 ? this.max - 1 : rear]
    }
}
const cQueue = new MyCircularQueue(5); // 設定長度為 3
cQueue.enQueue(1);  // 傳回 true
cQueue.enQueue(2);  // 傳回 true
cQueue.enQueue(3);  // 傳回 true
cQueue.enQueue(4);  // 傳回 false,隊列已滿
// cQueue.rear();  // 傳回 3
// cQueue.isFull();  // 傳回 true
cQueue.deQueue();  // 傳回 true
cQueue.deQueue();  // 傳回 true
cQueue.deQueue();  // 傳回 true
// cQueue.enQueue(4);  // 傳回 true
// cQueue.rear();  // 傳回 4
console.log(cQueue)
           

任務排程器

給定一個用字元數組表示的 CPU 需要執行的任務清單。其中包含使用大寫的 A - Z 字母表示的26 種不同種類的任務。任務可以以任意順序執行,并且每個任務都可以在 1 個機關時間内執行完。CPU 在任何一個機關時間内都可以執行一個任務,或者在待命狀态。

然而,兩個相同種類的任務之間必須有長度為 n 的冷卻時間,是以至少有連續 n 個機關時間内 CPU 在執行不同的任務,或者在待命狀态。

你需要計算完成所有任務所需要的最短時間。

示例 :

輸入:tasks = [“A”,“A”,“A”,“B”,“B”,“B”], n = 2

輸出:8

解釋:A -> B -> (待命) -> A -> B -> (待命) -> A -> B.

在本示例中,兩個相同類型任務之間必須間隔長度為 n = 2 的冷卻時間,而執行一個任務隻需要一個機關時間,是以中間出現了(待命)狀态。

思路:任務清單keys [‘A’,‘B’] 是用來周遊循環,拿到目前數量最多的任務,然後個push到臨時隊列tmp中,臨時隊列隻能放到n+1個(3個)任務,不夠放的就用’-'來補齊(做到兩個相同類型任務之間必須長度為n)。執行完一個臨時隊列tmp後,再循環判斷還有沒有任務清單keys,有的話繼續前面的動作,直到keys[0]=undefined為止

步驟:

1)先記錄每類任務的數量 Q:{A:3, B:3}

2)max記錄最多數量的先執行,先被push到臨時任務隊列tmp中,這個tmp就是AB-AB-AB(不夠n+1個的往裡面補"-")

  • 擷取任務清單keys([‘A’,‘B’]),周遊任務清單找到數量最多的key,拿到數量最多的A先push到臨時隊列tmp中,表示執行這個隊列,是以得把Q中A的數量減一,從任務清單keys清除A
  • 這個臨時隊列容量為n+1,是以能被循環小于n次,執行完一個臨時隊列後,把臨時隊列累加給p, 再去循環執行下一個臨時隊列
    【前端算法系列】隊列queue
const leastInterval = (tasks, n) => {
    // 表示最終隊列執行的結果
    let q = ''
    // 對歸類進行存儲
    let Q = {}
    tasks.forEach(item => {
        if (Q[item]) {
            Q[item]++
        } else {
            Q[item] = 1
        }
    });
    // 隊列中還有任務就要循環,為什麼為1,是因為效率問題,不要一直判斷對象Q是否為空,會重複計算
    while (1) {
        // 處理邊界問題
        // 任務清單:隻要有key值,表示任務隊列中還有,key值沒有就表示已經都處理掉了,就不用每次去判斷Q是否為空
        let keys = Object.keys(Q)
        if (!keys[0]) {
            break
        }
        // 處理任務:找到哪個數量最多,數量最多的就優先處理
        // 聲明一個隊列用來存儲1+n任務單元
        let tmp = []
        for (let i = 0; i <= n; i++) {
            // 記錄最大值
            let max=0
            // 最大值名稱
            let key
            // 最大值索引位置
            let pos
            // 任務清單中找目前任務還剩下多少
            keys.forEach((item, idx)=>{
                // 找最大值
                if(Q[item]>max){
                    max = Q[item]
                    key = item
                    pos = idx
                    // console.log(item)
                    // console.log(idx)
                }
            })
            // 判斷是否找到最大值key,如果是就push到臨時隊列
            if(key){
                // console.log('11--->'+key)
                tmp.push(key)
                // 在任務清單中清除
                keys.splice(pos, 1) //  ["A", "B"] ===> ["B"]
                Q[key]--
                if(Q[key]<1){
                    delete Q[key]
                }
                // console.log('222--->'+keys)
            }else{
                break
            }
        }
        // 如果不夠,後面補什麼
        q +=tmp.join('').padEnd(n+1,'-')
    }
    // 邊界處理,最後不要出現冷卻時間
    q = q.replace(/-+$/g, '')
    return q.length
}
const tasks = ["A", "A", "A", "B", "B", "B"], n = 2
console.log(leastInterval(tasks, n)) 
           

繼續閱讀