Hello, 各位勇敢的小夥伴, 你們好, 我是大家的嘴強王者小五, 身體健康, 腦子沒病.git
本人有豐富的脫髮技巧, 能讓你一躍成爲資深大咖.github
一看就會一寫就廢是本人的主旨, 菜到摳腳是本人的特色, 卑微中透着一絲絲剛強, 傻人有傻福是對我最大的安慰.算法
歡迎來到
小五
的算法系列
之棧與隊列
.segmentfault
此係列文章以《算法圖解》和《學習JavaScript算法》兩書爲核心,其他資料爲輔助,並佐以筆者愚見所成。力求以簡單、趣味的語言帶你們領略這算法世界的奇妙。數組
本文內容爲棧和隊列。筆者將帶領你們從 js 模擬其實現出發,探索棧與隊列在實際場景中的應用。其間會穿插講解 程序調用棧 及 任務隊列,使理解程序執行、遞歸、異步調用等易如破竹。最後,附上幾道習題以加深理解及鞏固所學。異步
👺 特色:<後進先出>函數
👇 下圖羽毛球筒就是對棧的形象比喻oop
👺 建立棧post
👇 咱們接下來用js模擬一個棧,併爲其添加以下方法:學習
push(element(s))
:入棧 -> 添加一個/多個元素至棧頂pop()
:出棧 -> 移除棧頂元素,並返回被移除的元素peek()
:返回棧頂元素isEmpty()
:該棧是否存在元素clear()
:移除棧中全部元素size()
:棧中元素個數🤔 思考: ❓ 如何將十進制轉換爲二進制
<除2取餘,逆序排列>:將十進制數字與2進行累除運算,取餘後將餘數逆序排列,獲得的結果即爲該數的二進制.
上述分析可知,這個是典型的後進先出場景,即棧的場景,代碼以下:
import Stack from 'stack'; export const binaryConversion = (num: number) => { const stack = new Stack<number>(); let binaryStr = ''; while (num > 0) { stack.push(num % 2); num = Math.floor(num / 2); } while (!stack.isEmpty()) { binaryStr += stack.pop(); } return binaryStr; } // 輸入 10 -> 輸出: '1010'
👺 擴展:十進制轉其它進制
思路分析:咱們追加一個入參base表明進制;二進制餘數爲0 ~ 1,八進制餘數爲0 ~ 7,十六進制餘數爲0 ~ 15,搭配字母表示即爲0 ~ 9,A ~ F。
👉 添加一組對數字的映射就行了, 大功告成!
import Stack from 'stack'; export const binaryConversion = (num: number, base: number) => { const stack = new Stack<number>(); let binaryStr = ''; + let baseMap = '0123456789ABCDEF'; while (num > 0) { - stack.push(num % 2); - num = Math.floor(num / 2); + stack.push(num % base); + num = Math.floor(num / base); } while (!stack.isEmpty()) { - binaryStr += stack.pop(); + binaryStr += baseMap[stack.pop() as number]; } return binaryStr; } // binaryConversion(100345, 2) -> 11000011111111001 // binaryConversion(100345, 8) -> 303771 // binaryConversion(100345, 16) -> 187F9
提到遞歸,你們確定會說,這個我會呀,不就是不斷循環自身的過程嗎?
可你真的理解遞歸了嗎?快排、歸併、樹的前中後序遍歷等是否能夠輕鬆駕馭?如若不能,則遞歸仍沒有理解透徹,即沒有吃透函數的執行順序。
咱們來看下面一段函數:
const greet = (name) => { console.log(`hello, ${name}!`); newGreet(name); console.log('getting ready to say bye...'); bye(); } const newGreet = (name) => { console.log(`how are you, ${name}?`); } const bye = () => { console.log('ok, bye!'); } greet('xiaowu');
它的執行過程是怎樣的呢?咱們一塊兒來看下吧👇
咱們再來寫個簡單的遞歸, 求5的階乘 ($5!$)
const fact = num => { if (num === 1) return 1; return num * fact(num - 1); } fact(5);
翠花, 上執行圖
👺 擴展:執行上下文
分爲 「全局執行上下文」 和 「函數執行上下文」
在開始執行任何代碼前,JavaScript會建立全局上下文壓入棧底,即 window 對象;每當咱們調用一個函數時,一個新的函數執行上下文就會被建立,如上文過程圖所示;整個JavaScript執行過程便是一個 「調用棧」。
👺 特色:<先進先出>
👇 先來先服務即對隊列形象的比喻
👺 建立隊列
👇 咱們接下來用js模擬一個隊列,併爲其添加以下方法:
enqueue(element(s))
:入隊 -> 添加一個/多個元素至隊尾dequeue()
:出隊 -> 移除隊首元素, 並返回被移除的元素peek()
:返回隊首元素isEmpty()
:該隊列是否存在元素size()
:隊列中元素個數實際生活中,並不會時刻都遵循着先來先服務的原則;好比醫院,醫生會優先處理病情較爲嚴重的患者;這便引出了隊列的第一個變種 - 優先隊列;
優先隊列 - 在插入數據時,按其權重將數據插入到正確的位置。
👺 代碼分析
enqueue
方法,使其能按照權重插入正確位置Queue
類中的private
改成protected
,使繼承後的類可重寫enqueue
方法push
元素push
,不然使用splice
插入👺 代碼實現
「擊鼓傳花」 講的是一種遊戲規則,人們在擊鼓聲中傳花,鼓聲中止時,花傳到誰手上,誰就被淘汰,直到剩餘一我的〈勝者〉。
此遊戲一直處於循環狀態,由此引出了隊列的第二個變種 - 循環隊列。
咱們擬定,每循環n次淘汰一人,n爲入參。
👺 代碼分析
👺 代碼實現
書接上文,咱們講到了程序調用棧,而程序並不會均同步執行,當其執行異步操做時又會發生什麼呢?
🤔 思考: ❓ $setTimeout(() => {執行函數}, 0)$ 爲什麼沒有當即執行
【圖片來源 - JavaScript Event Loop 機制詳解與 Vue.js 中實踐應用】
程序順序執行,依次入棧;當遇到異步任務時,將其推入任務隊列;待程序棧清空後,讀取任務隊列,將其分別入棧;如此反覆的過程,即是 Event Loop - 事件循環。
🦅 這裏推薦一篇講解JS執行機制的文章 這一次,完全弄懂 JavaScript 執行機制
如下題目均來自 LeetCode,筆者會爲每道題目提供一種解題思路;此思路絕非最佳,歡迎各位看官積極思考,並留下本身的獨特看法。
👺 題目描述
請你僅使用兩個棧實現先入先出隊列。隊列應當支持通常隊列支持的全部操做(enqueue
、dequeue
、peek
、size
、isEmpty
);
你只能使用標準的棧操做 ~ 也就是隻有 push
, pop
, peek
, size
和 isEmpty
操做是合法的。
👺 題目分析
棧實現隊列,有兩種思路方向,分別爲從入棧入手和從出棧入手,筆者選擇從入棧入手;
核心思路便是如何讓後入棧的元素在棧底,先入棧的元素在棧頂;
用輔助棧倒換下順序,便可模擬隊列,過程以下 👇
👺 代碼實現
class Queue<T> { private items = new Stack<T>(); enqueue(item: T) { if (this.items.isEmpty()) { this.items.push(item); return; } const _stack = new Stack<T>(); while (!this.items.isEmpty()) { _stack.push(this.items.pop() as T); } _stack.push(item); while (!_stack.isEmpty()) { this.items.push(_stack.pop() as T); } } // 其他方法無變化 }
👺 題目描述
請你僅使用兩個隊列實現一個後入先出(LIFO)的棧,並支持普通棧的全部操做(push
、 pop
、peek
、size
、isEmpty
);
你只能使用隊列的基本操做 ~ 也就是 enqueue
, dequeue
, peek
, size
和 isEmpty
這些操做。
👺 題目分析
隊列實現棧,一樣有兩種思路方向,分別爲從入隊入手和從出隊入手,筆者選擇從入隊入手;
核心思路便是如何讓後入隊的元素在隊首,先入隊的元素在隊尾;
每次入隊新元素時,將隊列其它元素先出隊後入隊便可,過程以下 👇
👺 代碼實現
class Stack<T> { private items = new Queue<T>(); push(item: T) { this.items.enqueue(item); for (let i = 1; i < this.items.size(); i++) { this.items.enqueue(this.items.dequeue() as T); } } // 其他方法無變化 }
👺 題目描述
👺 題目分析
咱們看下面這個 🌰 例子
{[()][]}
給定字符串是否閉合,就是在找最小閉合單元,剔除後繼續尋找直至字符串爲空則知足條件,過程以下👇
👺 代碼實現
const isValid = (str: string) => { const stack = new Stack(); const strMap = { ')': '(', ']': '[', '}': '{', }; for (let i of str) { if (['(', '[', '{'].includes(i)) { stack.push(i); } if (strMap[i]) { if (strMap[i] === stack.peek()) { stack.pop(); } else { return false; } } } return stack.size() === 0; }
👺 題目描述
👺 題目分析
此題爲上一題的擴展,由上題分析,咱們可知,此題應繼續使用棧;
❓ 那怎麼求長度呢 🤔 與數字掛鉤的話藉助下數組的下標唄,下標差即爲長度
上圖可看出長度爲 出棧索引減棧頂值;而實際操做時,右括號不入棧,故咱們記錄一個bottom值;若棧爲空,則減bottom,棧不爲空,則減棧頂值。
👺 代碼實現
const longestValidParentheses = (str: string) => { const stack = new Stack<number>(); let maxLen = 0; let bottom: number | undefined = undefined; for (let i = 0; i < str.length; i++) { if (stack.isEmpty() && str[i] === ')') bottom = undefined; if (str[i] === '(') { if (bottom === undefined) bottom = i - 1; stack.push(i); } if (!stack.isEmpty() && str[i] === ')') { stack.pop(); let len = i - (stack.peek() ?? bottom); if (len > maxLen) maxLen = len; } } return maxLen; }
👺 題目描述
👺 題目分析
乍一看題幹,這不就是一個行走的隊列嗎,[1, 3, -1, -3, 5, 3, 6, 7]
執行順序以下👇
因而筆者實現了以下代碼:
const maxSlidingWindow = (nums: number[], k: number) => { let queue: number[] = []; let maxArr: number[] = []; nums.forEach(item => { if (queue.length <= k) { queue.push(item); } if (queue.length === k) { let max = queue[0]; for (let i = 1; i < queue.length; i++) { if (queue[i] > max) max = queue[i]; } maxArr.push(max); queue.shift(); } }) return maxArr; }
當我興致沖沖的提交到 LeetCode 上時
無奈,咱們繼續優化,看看怎麼能減小循環;觀察上圖,咱們發現,窗口中在最大值左側的數字沒有意義,咱們無需關心,可將其出隊,這樣窗口最大值爲隊首元素;
❓ 隊首何時出隊呢 🤔 隊列中存索引值不就行了,當前元素索引和隊首元素的差值與窗口大小比對,若大於窗口大小,則隊首元素已不在此窗口中,出隊。
👺 代碼實現
其本質是一個雙端隊列,擴展下咱們的 Queue 類,增長 pop、tail 兩個方法,表明從隊尾移除元素和獲取隊尾值。
pop() { return this.items.pop(); } tail() { return this.items[this.items.length - 1]; }
const maxSlidingWindow = (nums: number[], k: number) => { const queue = new Queue<number>(); let maxArr: number[] = []; nums.forEach((item, index) => { while (!queue.isEmpty() && item >= nums[queue.tail()]) { queue.pop(); } queue.enqueue(index); if (queue.peek() <= index - k) queue.dequeue(); if (index >= k - 1) maxArr.push(nums[queue.peek()]); }) return maxArr; }
🔗 本系列其它文章連接:起航篇 - 排序算法