队列和栈
本文描述了队列和栈的基本概念,并给出了几道常见的算法题来帮助读者理解队列和栈。
基本概念
队列和栈都是线性数据结构。当使用数组实现队列和栈时,这样的数组可以被称为访问和读取受限的数组,即只能访问和读取数组某一端的端点,而无法对中间节点进行操作。其中,队列遵循先进先出原则,队尾进,队首出;而栈则遵循后进先出原则,队尾进队尾出(栈顶进栈顶出)。
实战练习
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
if(nums.length == 0){
return nums;
}
let queue = [], max_nums = [], index = 0;
for(let num of nums){
index++;
if(queue.length == 0 || queue[queue.length - 1] >= num){
queue.push(num);
}else if(queue[queue.length - 1] < num){
while(queue.length > 0 && queue[queue.length - 1] < num){
queue.pop();
}
queue.push(num);
}
if(index >= k){
max_nums.push(queue[0]);
if(queue[0] == nums[index - k]){
queue.shift();
}
}
}
return max_nums;
};
/**题解
* 解这道题的一个直观方法是:创建队列,遍历数组并将进入窗口内的值入队,当队列长度达到窗口最大限制时,
* 遍历该窗口队列,获取最大值,并将队首节点出对,继续遍历输入数组,从而得到滑动窗口的最大值数组。
* 但这种方法实际上引入了不必要的时间开支,基于队列的特点,实际上有更好的方法来做这道题。
*
* 上述方法的时间复杂度主要浪费在寻找窗口的最大值上,那么我们可以思考,是否存在一种方法,能存储可能的最大值,以避免窗口遍历操作。这时我们需要明确一点,队列是一种先进先出的结构。也就是说,在同一个窗口中,如果后入队的值,比先入队的值大,那么先入队的值就不可能是最大值。基于这一思想,我们可以创建一个非递减单调队列,该队列队尾元素入队时,如果前面的元素比它小,就先把前面比它小的所有元素出队再入队。
* 对于该队列,队首是当前窗口的最大值,而当队首元素不在位于当前窗口时,队首元素出队。通过这种方法,时间复杂度大大减少了。
*
* 实际上这里用左右双指针写会快很多(js数组头插入删除要移动元素,费时间,所以可以直接用两个移动的游标指针来表示队首队尾),我这里为了演示就直接调函数了。
*/
var MaxQueue = function() {
this.queue = [];
this.orderQueue = [];
this.leftQ = -1;
this.leftOrder = 0;
};
/**
* @return {number}
*/
MaxQueue.prototype.max_value = function() {
if(this.queue.length == 0 || this.leftQ == this.queue.length - 1){
return -1;
}else{
return this.orderQueue[this.leftOrder];
}
};
/**
* @param {number} value
* @return {void}
*/
MaxQueue.prototype.push_back = function(value) {
this.queue.push(value);
while(this.orderQueue.length - 1> this.leftOrder && this.orderQueue[this.orderQueue.length - 1] < value){
this.orderQueue.pop();
}
this.orderQueue.push(value);
if(this.orderQueue[this.leftOrder] < value){
this.leftOrder = this.orderQueue.length - 1;
}
};
/**
* @return {number}
*/
MaxQueue.prototype.pop_front = function() {
if(this.queue.length == 0 || this.leftQ == this.queue.length - 1){
return -1;
}else{
let popNum = this.queue[++this.leftQ];
if(popNum == this.orderQueue[this.leftOrder]){
this.leftOrder++;
}
return popNum;
}
};
/**
* Your MaxQueue object will be instantiated and called as such:
* var obj = new MaxQueue()
* var param_1 = obj.max_value()
* obj.push_back(value)
* var param_3 = obj.pop_front()
*/
/**题解
* 和滑动窗口一样的套路,除了引入一个基本的队列外,还需要创建一个辅助队列(单调队列),辅助队列里面的队首,就是当前队列里的最大值。
*
* 为什么这样有效?是因为队列是先进先出的结构,这意味着一旦后面进来一个更大的元素,那么在他前面比它大的元素都出队,且它没有出队时,队列中的最大元素值总等于该元素的值。
*/
var CQueue = function() {
this.stack1 = [];
this.stack2 = [];
};
/**
* @param {number} value
* @return {void}
*/
CQueue.prototype.appendTail = function(value) {
this.stack1.push(value);
};
/**
* @return {number}
*/
CQueue.prototype.deleteHead = function() {
if(this.stack2.length > 0){
return this.stack2.pop();
}
while(this.stack1.length > 0){
this.stack2.push(this.stack1.pop());
}
return this.stack2.pop() || -1;
};
/**
* Your CQueue object will be instantiated and called as such:
* var obj = new CQueue()
* obj.appendTail(value)
* var param_2 = obj.deleteHead()
*/
/**题解
* 队列是先进先出,栈是后进先出。而两次后进先出则为先进先出(负负得正),所以可以用两个栈来模拟一个队列。具体来说,一个栈模拟入队,另一个栈模拟出队。
*/
/**
* initialize your data structure here.
*/
var MinStack = function() {
this.stack = [];
this.orderStack = [];
};
/**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
this.stack.push(x);
if(this.orderStack.length == 0){
this.orderStack.push(x);
}else if(this.orderStack[this.orderStack.length - 1] >= x){
this.orderStack.push(x);
}
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
let popNum = this.stack.pop();
if(popNum == this.orderStack[this.orderStack.length - 1]){
this.orderStack.pop();
}
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
};
/**
* @return {number}
*/
MinStack.prototype.min = function() {
return this.orderStack[this.orderStack.length - 1];
};
/**
* Your MinStack object will be instantiated and called as such:
* var obj = new MinStack()
* obj.push(x)
* obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.min()
*/
/**题解
* 和单调队列类似,添加一个辅助的单调栈来在栈顶显示最小值。
*
* 这是基于栈的特征,后进先出,如果后面进来的元素不是最小值,则它出去之前,最小值永远不会是栈顶元素。即我们可以维护一个从栈顶到栈底的非严格单调递增序列,来快速查询最小值。
*/
总结
栈和队列都是仅支持端操作的线性结构,这意味着对其的访问和读取受到限制。基于这种特性,使用辅助栈/队列通常能够增强原始的栈\队列。上面的几道题,实际上都是通过额外创建辅助的单调端读写结构来帮助降低额外操作的时间复杂度。
参考
[1] github-javascript-algorithms,队列的基本概念
[2] github-javascript-algorithms,栈的基本概念