三者都屬於數據結構,做爲專業的技術人員來講,理解數據結構是不可或缺的一部分。在平常的面試中,可能會遇到棧、堆、隊列等一系列問題。 html
在面試中,常常問遇到與之相關的一些列問題哈。java
棧 是一種遵循 後進先出(LIFO) 原則的有序集合。新添加和待刪除的數據都保存在棧的同一端棧頂,另外一端就是棧底。新元素靠近棧頂,舊元素靠近棧底。 棧由編譯器自動分配釋放。棧使用一級緩存。調用時處於存儲空間,調用完畢自動釋放。git
舉個栗子:乒乓球盒子/搭建積木程序員
javaScript中,數據類型分爲基本數據類型和引用數據類型,基本數據類型包含:string、number、boolean、undefined、null、symbol、bigint這幾種。在內存中這幾種數據類型存儲在棧空間,咱們按值訪問。原型類型都存儲在棧內存中,是大小固定而且有序的。es6
咱們知道了基本數據結構的存儲以後,咱們再來看看JavaScript中如何經過棧來管理多個執行上下文。面試
JavaScript中每個可執行代碼,在解釋執行前,都會建立一個可執行上下文。按照可執行代碼塊可分爲三種可執行上下文。 算法
由於JS執行中最早進入全局環境,因此處於"棧底的永遠是全局環境的執行上下文"。而處於"棧頂的是當前正在執行函數的執行上下文",當函數調用完成後,它就會從棧頂被推出(理想的狀況下,閉包會阻止該操做,閉包後續文章深刻詳解)。編程
"全局環境只有一個,對應的全局執行上下文也只有一個,只有當頁面被關閉以後它纔會從執行棧中被推出,不然一直存在於棧底"瀏覽器
看個例子:緩存
let name = '蝸牛'; function sayName(name) { sayNameStart(name); } function sayNameStart(name) { sayNameEnd(name); } function sayNameEnd(name) { console.log(name); } 複製代碼
當代碼進行時聲明:
咱們須要本身建立一個棧,而且這個棧包含一些方法。
function Stack() { let items = []; this.push = function(element) { items.push(element); }; this.pop = function() { let s = items.pop(); return s; }; this.peek = function() { return items[items.length - 1]; }; this.isEmpty = function() { return items.length == 0; }; this.size = function() { return items.length; }; this.clear = function() { items = []; } } 複製代碼
可是這樣的方式在建立多個實例的時候爲建立多個items的副本。就不太合適了。 用ES如何6實現Stack類了。能夠用WeakMap實現,並保證屬性是私有的。
let Stack = (function() { const items = new WeakMap(); class Stack { constructor() { items.set(this, []); } getItems() { let s = items.get(this); return s; } push(element) { this.getItems().push(element); } pop() { return this.getItems().pop(); } peek() { return this.getItems()[this.getItems.length - 1]; } isEmpty() { return this.getItems().length == 0; } size() { return this.getItems().length; } clear() { this.getItems() = []; } } return Stack; })(); 複製代碼
棧能夠解決十進制轉爲二進制的問題、任意進制轉換的問題、平衡園括號問題、漢羅塔問題。
// 例子十進制轉二進制問題 function divideBy2(decNumber) { var remStack = new Stack(), rem, binaryString = ''; while (decNumber > 0) { rem = Math.floor(decNumber % 2); remStack.push(rem); decNumber = Math.floor(decNumber / 2); } while(!remStack.isEmpty()) { binaryString += remStack.pop().toString(); } return binaryString; } // 任意進制轉換的算法 function baseConverter(decNumber, base) { var remStack = new Stack(), rem, binaryString = '', digits = '0123456789ABCDEF'; while (decNumber > 0) { rem = Math.floor(decNumber % base); remStack.push(rem); decNumber = Math.floor(decNumber / base); } while(!remStack.isEmpty()) { binaryString += digits[remStack.pop()].toString(); } return binaryString; } 複製代碼
不一樣瀏覽器對調用棧的大小是有限制,超過將出現棧溢出的問題。下面這段代碼能夠檢驗不用瀏覽器對調用棧的大小限制。
var i = 0; function recursiveFn () { i++; recursiveFn(); } try { recursiveFn(); } catch (ex) { console.log(`個人最大調用棧 i = ${i} errorMsg = ${ex}`); } 複製代碼
谷歌瀏覽器:
function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 超時 Fibonacci(500) // 超時 複製代碼
上面代碼是一個階乘函數,計算n的階乘,最多須要保存n個調用記錄,複雜度 O(n) 。若是超出限制,會出現棧溢出問題。
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity 複製代碼
因而可知,「尾調用優化」對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。ES6 亦是如此,第一次明確規定,全部 ECMAScript 的實現,都必須部署「尾調用優化」。這就是說,ES6 中只要使用尾遞歸,就不會發生棧溢出(或者層層遞歸形成的超時),相對節省內存。
列子來源:ECMAScript 6 入門
堆,通常由操做人員(程序員)分配釋放,若操做人員不分配釋放,將由OS回收釋放。分配方式相似鏈表。堆存儲在二級緩存中。
JavaScript 的數據類型除了原始類型,還有一類是 Object 類型,它包含:
Object 類型都存儲在堆內存中,是大小不定,複雜可變的。 Object 類型數據的 指針 存儲在棧內存空間, 指針實際指向的值存儲在堆內存空間。
一般與垃圾回收機制有關。爲了使程序運行時佔用的內存最小。
當一個方法執行時,每一個方法都會創建本身的內存棧,在這個方法內定義的變量將會逐個放入這塊棧內存裏,隨着方法的執行結束,這個方法的內存棧也將天然銷燬了。所以,全部在方法中定義的變量都是放在棧內存中的;
當咱們在程序中建立一個對象時,這個對象將被保存到運行時數據區中,以便反覆利用(由於對象的建立成本一般較大),這個運行時數據區就是堆內存。堆內存中的對象不會隨方法的結束而銷燬,即便方法結束後,這個對象還可能被另外一個引用變量所引用(方法的參數傳遞時很常見),則這個對象依然不會被銷燬,只有當一個對象沒有任何引用變量引用它時,系統的垃圾回收機制纔會在覈實的時候回收它。
隊列遵循FIFO,先進先出原則的一組有序集合。隊列在尾部添加元素,在頂部刪除元素。在現實中最多見的隊列就是排隊。先排隊的先服務。(請你們文明排隊,不要插隊。)
JavaScript是單線程,單線程任務被分爲同步任務和異步任務。同步任務在調用棧中等待主線程依次執行,異步任務會在有告終果以後,將回調函數註冊到任務隊列,等待主線程空閒(調用棧爲空),放入執行棧等待主線程執行。
Event loop執行以下圖,任務隊列只是其中的一部分。
執行棧在執行完同步任務以後,若是執行棧爲空,就會去檢查微任務(MicroTask)隊列是否爲空,若是爲空的話,就會去執行宏任務隊列(MacroTask)。不然就會一次性執行完全部的微任務隊列。 每次一個宏任務執行完成以後,都會去檢查微任務隊列是否爲空,若是不爲空就會按照先進先出的方式執行完微任務隊列。而後在執行下一個宏任務,如此循環執行。直到結束。
實現包含如下方法的Queue類
// 隊列Queue類簡單實現 function Queue() { let items = []; // 添加元素 this.enqueue = function(element) { items.push(element); }; // 刪除元素 this.dequeue = function() { return items.shift(); }; // 返回隊列第一個元素 this.front = function() { return items[0]; }; // 判斷隊列是否爲空 this.isEmpty = function() { return items.length === 0; }; // 返回隊列長度 this.size = function() { return items.length; }; } 複製代碼
ES6語法實現Queue隊列類,利用WeakMap來保存私有屬性items,並用外層函數(閉包)來封裝Queue類。
let Queue1 = (function() { const items = new WeakMap(); class Queue1 { constructor() { items.set(this, []); } // 獲取隊列 getQueue() { return items.get(this); } // 添加元素 enqueue (element) { this.getQueue().push(element); } // 刪除元素 dequeue() { return this.getQueue().shift(); } // 返回隊列第一個元素 front() { return this.getQueue()[0]; } // 判斷隊列是否爲空 isEmpty() { return this.getQueue().length === 0; } // 返回隊列長度 size() { return this.getQueue().length; } } return Queue1; })(); 複製代碼
元素的添加和刪除基於優先級。常見的就是機場的登機順序。頭等艙和商務艙的優先級高於經濟艙。實現優先隊列,設置優先級。
// 優先列隊 function PriorityQueue() { let items = []; // 建立元素和它的優先級(priority越大優先級越低) function QueueElement(element, priority) { this.element = element; this.priority = priority; } // 添加元素(根據優先級添加) this.enqueue = function(element, priority) { let queueElement = new QueueElement(element, priority); // 標記是否添加元素的優先級的值最大 let added = false; for (let i = 0; i < items.length; i++) { if (queueElement.priority < items[i].priority) { items.splice(i, 0, queueElement); added = true; break; } } if (!added) { items.push(queueElement); } }; // 刪除元素 this.dequeue = function() { return items.shift(); }; // 返回隊列第一個元素 this.front = function() { return items[0]; }; // 判斷隊列是否爲空 this.isEmpty = function() { return items.length === 0; }; // 返回隊列長度 this.size = function() { return items.length }; // 打印隊列 this.print = function() { for (let i = 0; i < items.length; i++) { console.log(`${items[i].element} - ${items[i].priority}`); } }; } 複製代碼
// 循環隊列(擊鼓傳花) function hotPotato(nameList, num) { let queue = new Queue(); //{1} // 構造函數爲4.3建立 for(let i =0; i< nameList.length; i++) { queue.enqueue(nameList[i]); // {2} } let eliminted = ''; while(queue.size() > 1) { // 把隊列num以前的項按照優先級添加到隊列的後面 for(let i = 0; i < num; i++) { queue.enqueue(queue.dequeue()); // {3} } eliminted = queue.dequeue(); // {4} console.log(eliminted + '在擊鼓傳花遊戲中被淘汰'); } return queue.dequeue(); // {5} } let names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl']; let winner = hotPotato(names, 7); console.log('獲勝者是:' + winner); 複製代碼
參考來源: