今天稍微停下前進的腳步,來看下隊棧的左右互搏術。前兩天學習了佇列和棧以後,今天就可以試著來用兩個棧實現佇列的功能或者用兩個佇列來實現棧的功能。
1. 用兩個棧實現一個佇列
1.1 題目分析
棧是先進後出,佇列是先進先出,但可以用兩個棧來模擬一個佇列的功能,來實現佇列中主要的 enqueue,dequeue, head 方法。
1.2 思路分析
我們所學的每一種資料結構,本質上都是對資料如何儲存和使用的研究,這就必然涉及到增刪改查,那麼考慮實現這些方法時,我們優先考慮如何實現資料的增加,只有存在了資料,才能夠後續的操作。所以如何實現佇列中的增加資料方法 enqueue 呢?
- 給兩個棧分別命名為 stack1,stack2。那有了這兩個棧以後,可以選取其中一個來儲存資料,比如說 stack1,那麼佇列的 enqueue 方法就很容易了,直接利用棧的 push 方法就能夠新增資料。
接下來考慮佇列中的刪除 dequeue 方法
- 首先要注意下 dequeue 方法是刪除佇列中的頭部元素,而此時隊首是在 stack1 棧底的,目前來說還取不到。
- 這個時候 stack2 該上場了,可以把 stack1 中的元素都依次移除並壓入 stack2 中,這樣的話,stack2 的棧頂就變成了隊首,不就可以利用 stack2 的 pop 方法來移除元素了嘛。
那佇列的 head 方法呢
- 執行完 stack2 的 pop 方法後,還需要把資料再移回 stack1 裡嗎?其實不需要了,因為此時隊首正好是 stack2 的棧頂,而佇列的 head 方法就可以利用棧的 top 方法來實現了。
- 如果 stack2 是空的怎麼辦?那 stack1 的元素都移除到 stack2 就可以了。
- 如果 stack1 也是空的呢,那就說明佇列中沒有元素了,此時返回 null 就可以了。
注意到了嗎,這裡又用到了 分而治之 的思想,還記得之前在哪裡用過嗎?對,就是在給棧新增獲取最小值方法的時候用過,當時也是用了兩個棧來實現。這裡的話 enqueue 始終都操作 stack1,dequeue 和 head 方法始終都操作 stack2。
1.3 程式碼實現
{
class StackQueue {
constructor() {
this.stack1 = new Stack();
this.stack2 = new Stack();
} // 初始化stack,偽造私有方法 _initStack() {
if (this.stack1.isEmpty() &
&
this.stack2.isEmpty()) {
return null;
// 如果兩個棧都是空的,那麼佇列中就沒有元素
} if (this.stack2.isEmpty()) {
// 如果stack2是空的,那麼此時stack1一定不為空 while (!this.stack1.isEmpty()) {
this.stack2.push(this.stack1.pop());
// 把stack1的元素移除到stack2中
}
}
} // 向隊尾新增一個元素 enqueue(item) {
this.stack1.push(item);
// 把資料存入到stack1中
} // 刪除隊首的一個元素 dequeue() {
this._initStack();
return this.stack2.pop();
} // 返回隊首的元素 head() {
this._initStack();
return this.stack2.top();
}
} var stackQueue = new StackQueue();
stackQueue.enqueue(1);
stackQueue.enqueue(4);
stackQueue.enqueue(8);
console.log(stackQueue.head());
// 1 stackQueue.dequeue();
stackQueue.enqueue(9);
console.log(stackQueue.head());
// 4 stackQueue.dequeue();
console.log(stackQueue.head());
// 8 console.log(stackQueue.dequeue());
// 8 console.log(stackQueue.dequeue());
// 9
}複製程式碼
是不是覺得很簡單呢,梳理清楚佇列和棧的特性就OK啦。接下來讓我們繼續修煉,用佇列實現棧吧!
2. 用兩個佇列實現一個棧
2.1 題目分析
佇列是先進先出,棧是先進後出,(不斷重複這兩個知識點) 但可以用兩個佇列來模擬一個棧的功能,來實現棧中主要的 push,pop, top 方法。
你可能會想到利用上邊的套路來實現這個需求,但是最後的結果你會發現是不正確的。因為 把 stack1 的元素移除到 stack2 中,此時的兩個棧中的資料就首尾交換了,而如果此處換成佇列 this.queue2. enqueue(this.queue1. dequeue()),你會發現由於佇列的特性,此時的兩個佇列還是一樣的,首尾並沒有交換。
so 我們來換個思路
2.2 思路分析
和上邊一樣,我們先考慮如何實現棧的儲存資料 push 方法:
- 給兩個佇列分別命名為 queue1,queue2。實現 push 方法時,利用佇列的 enqueue 方法,如果兩個佇列都為空,那麼預設向 queue1 裡新增資料;如果有一個不為空,那麼就向這個不為空的佇列裡新增資料。
top 方法就簡單了:
- 利用佇列的 tail 方法,兩個佇列要麼都為空,要麼有一個不為空,那麼返回不為空佇列的尾部元素就是棧頂元素了.
接下來思考比較複雜的 pop 方法:
- pop 方法刪除的是棧頂,但此時棧頂元素是佇列的尾部元素,而隊尾元素是不能刪除的。
- 但每次執行 pop 時,可以將不為空的 a 佇列裡的元素迴圈刪除並放入到另一個 b 佇列中,直到 a 佇列中只剩下一個元素,此時 a 佇列的這個元素就是佇列的尾部元素,也就是棧頂元素了,那 pop 方法就簡單了,利用 a 佇列的 dequeue 方法就可以了。
在具體的實現中,需要額外定義兩個變數,dataQueue 和 emptyQueue:
- dataQueue 始終指向那個不為空的佇列
- emptyQueue 始終指向那個為空的佇列
2.3 程式碼實現
{
class QueueStack {
constructor() {
this.queue1 = new Queue();
this.queue2 = new Queue();
this.dataQueue = null;
// 存放資料的佇列 this.emptyQueue = null;
// 存放備份資料的佇列
} // 初始化佇列資料,模擬私有方法 確認哪個佇列存放資料,哪個佇列做備份 _initQueue() {
if (this.queue1.isEmpty()) {
this.dataQueue = this.queue2;
this.emptyQueue = this.queue1;
} else {
// 都為空的話 預設是 佇列1 this.dataQueue = this.queue1;
this.emptyQueue = this.queue2;
}
} // 往棧裡壓入一個元素 push(item) {
this._initQueue();
this.dataQueue.enqueue(item);
} // 返回棧頂的元素 top() {
this._initQueue();
return this.dataQueue.tail();
} // 把棧頂的元素移除 pop() {
this._initQueue();
while (this.dataQueue.size() >
1) {
// 利用備份佇列轉移資料, this.emptyQueue.enqueue(this.dataQueue.dequeue());
// 資料佇列和備份佇列交換了身份
} return this.dataQueue.dequeue();
// 移除資料佇列的頭部元素
}
} var queueStack = new QueueStack();
queueStack.push(1);
queueStack.push(2);
queueStack.push(4);
console.log(queueStack.top());
// 棧頂是 4 console.log(queueStack.pop());
// 移除 4 queueStack.push(5);
console.log(queueStack.top());
// 棧頂變成 5 queueStack.push(6);
console.log(queueStack.pop());
// 移除 6 console.log(queueStack.pop());
// 移除5 console.log(queueStack.top());
// 棧頂是 2
}複製程式碼
如果你有其他好的方法,歡迎留言
總結
我們利用基礎的陣列 api 實現了佇列和棧的功能,再來回顧下(敲黑板,劃重點了)
- 佇列最大的特點就是先進先出,主要的兩個操作是入隊和出隊,和棧一樣,是個受限制的資料結構。
- 佇列既可以用陣列實現,也可以用連結串列實現,用陣列實現的叫順序佇列,用連結串列實現的叫鏈式佇列(後續更新)
- 棧最大的特點就是先進後出,主要的兩個操作是出棧和入棧,也是一個受限制的資料結構。
- 和佇列一樣,既可以用陣列實現,也可以用連結串列實現。不管基於陣列還是連結串列,入棧、出棧的時間複雜度都為 O(1),佇列也是如此。
當然還有其他的佇列和棧,我這裡只介紹到了基礎的實現。這個硬骨頭,還需慢慢啃。
重點
如果有錯誤或者錯別字,還請給我留言指出,謝謝。