三者都屬於數據結構,做爲專業的技術人員來講,理解數據結構是不可或缺的一部分。在平常的面試中,可能會遇到棧、堆、隊列等一系列問題。 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);
複製代碼
參考來源: