「算法」JS 中的栈和队列

455 阅读3分钟

数据结构

JavaScript 中,栈和队列的实现依赖于数组,也就是说,栈和队列就是特殊的数组。

栈的特点是先进后出,用到数组的两个方法:

stack.push(item); // 进栈
stack.pop(); // 出栈

队列

队列的特点是先进先出,用到数组的两个方法:

stack.push(item); // 进队
stack.shift(); // 出队

算法

栈的应用

关键词:对称性、递减栈

20. 有效的括号

括号的成立意味着对称性,所以我们使用栈来解决。当遇到左括号时,压入对应的右括号,遇到右括号时,弹出栈顶元素并进行校验,得到结果:

const l2r = {
    "(":")",
    "[":"]",
    "{":"}"
}

var isValid = function(s) {
    const stack=[];
    for(let i =0;i<s.length;i++){
        const value=s[i];
        if(value==="("||value==='['||value==='{'){
            stack.push(l2r[value]);
        } else if(!stack.length|| stack.pop()!==value){
            return false;
        }       
    }
    return !stack.length;
};

剑指 Offer II 038. 每日温度

可以使用两层遍历,更好的方法是使用栈来避免重复操作,就是要及时将不必要的元素出栈

维护一个递减栈,保持温度单调递减。具体做法是,遍历温度数组,将新读取的温度值与栈顶温度比较,若读取的温度值较低或栈为空,则将它的下标压入栈中;若读取的温度值较高,则将读取的温度值下标与栈顶元素的值求差,填入结果数组栈顶下标对应的元素中,重复这一步直到读取的温度值比栈顶温度值低或栈为空。

相较于两层遍历,递减栈的时间复杂度由 O(n^2) 变为了 O(n)。

var dailyTemperatures = function(temperatures) {
    const len = temperatures.length;
    const stack=[];
    const res= (new Array(len)).fill(0);
    for(let i=0;i<len;i++){
        while(stack.length&&temperatures[i]>temperatures[stack[stack.length-1]]){
          const index=stack.pop(); 
          res[index]= i-index;
        }
        stack.push(i);
    }
    return res;
};

155. 最小栈

这道题的关键在于 getMin 函数。普通的想法是使用遍历来找最小元素,时间复杂度是 O(n):

var MinStack = function() {
    this.stack=[];
};

MinStack.prototype.push = function(val) {
    this.stack.push(val)
};

MinStack.prototype.pop = function() {
    this.stack.pop();
};

MinStack.prototype.top = function() {
    return this.stack[this.stack.length-1];
};

MinStack.prototype.getMin = function() {
    let min = Infinity;
    for(let i =0;i<this.stack.length;i++){
        const cur=this.stack[i];
        if(cur<min){
            min=cur;
        }
    }
    return min;
};

这么写也能通过测试,但是我们可以利用栈来将时间复杂度降到 O(1)。

为了降低时间复杂度,我们使用空间换时间。使用一个辅助栈作为递减栈,用于存储当前的最小元素序列,调用 getMin 时,只需要取出栈顶的元素:

var MinStack = function() {
    this.stack=[];
    // 用空间换时间,定义最小数的栈,使 getMin 方法时间复杂度为 O(1)
    this.stack2=[];
};

MinStack.prototype.push = function(val) {
    this.stack.push(val);
    // 若当前元素比递减栈的栈顶还小,压栈
    if(!this.stack2.length||this.stack2[this.stack2.length-1]>=val){
        this.stack2.push(val);
    }
};

MinStack.prototype.pop = function() {
    const value = this.stack.pop();
    // 若弹出的元素也存在于递减栈,将递减栈的栈顶一并弹出
    if(value===this.stack2[this.stack2.length-1]){
        this.stack2.pop();
    }
    return value;
};

MinStack.prototype.top = function() {
    return this.stack[this.stack.length-1];
};

MinStack.prototype.getMin = function() {
    // 直接获取递减栈的栈顶
    return this.stack2[this.stack2.length-1]
};

队列的应用

关键词:逆序、双指针、双端队列

232. 用栈实现队列

本题的关键是让一个栈逆序输出,这里用两个栈来解决:一个栈用于入队操作,一个栈用于出队操作。当想要出队或者查看队首元素时,若出队的栈为空,则将入队栈的元素逐个弹出并压入出队栈,再执行相关操作,这样就实现了已输入的元素的逆序:

var MyQueue = function() {
    this.stack1=[];
    this.stack2=[];
};

MyQueue.prototype.push = function(x) {
    this.stack1.push(x);
};

MyQueue.prototype.pop = function() {
    if(!this.stack2.length){
        while(this.stack1.length){
            this.stack2.push(this.stack1.pop())
        }
    }
    return this.stack2.pop();
};

MyQueue.prototype.peek = function() {
    if(!this.stack2.length){
        while(this.stack1.length){
            this.stack2.push(this.stack1.pop())
        }
    }
    return this.stack2[this.stack2.length-1];
};

MyQueue.prototype.empty = function() {
    return !this.stack1.length&&!this.stack2.length;
};

剑指 Offer 59 - I. 滑动窗口的最大值

为了约束窗口的范围,我们使用双指针,两个指针分别指向左和右,遍历指针包含的区域得到结果,这种方法的时间复杂度为 O(kn):

var maxSlidingWindow = function(nums, k) {
    const res =[];
    let i =0,j=k-1;
    if(!nums||!nums.length){
        return res;
    }
    while(j<nums.length){
        let max=calMax(nums,i,j);
        res.push(max);
        i++;
        j++;
    }
    return res;
};

function calMax(nums,i,j){
    let max=nums[i];
    for(let k=i+1;k<=j;k++){
        if(nums[k]>max) {
            max=nums[k];
        }
    }
    return max;
}

O(kn) 中 k 的产生来源于我们需要通过遍历来得到最大值,可以使用双端队列将时间复杂度降到 O(n)。

本题中的双端队列是一个有效的递减队列,只在窗口移动时更新元素的最大值。遍历数组时,若当前元素大于队尾元素,则将队尾小于当前元素的元素依次出队(双端队列可从队尾出队);若小于队尾元素,则将其下标入队。

当遍历到第 k 个值时,第一个滑动窗口的最大值就是当前的队尾元素所对应的值。将它压入结果数组中,重复此步骤直到读完最后一个元素。

同时,每次得到结果时需要检查窗口之前的元素是否在队列中,若在则出队。这一步是为了维持队列的有效性

var maxSlidingWindow = function(nums, k) {
    // 双端队列,时间复杂度O(n)
    const deque=[];
    const res=[];
    for(let i=0;i<nums.length;i++){
        // 若遍历元素大于队尾元素对应的值且队不空,队尾元素出队
        while(deque.length&&nums[deque[deque.length-1]]<nums[i]){
            deque.pop();
        }
        // 直到遍历元素小于队尾元素或队空,遍历元素的下标入队
        deque.push(i);
        // 及时移除窗口之前的元素
        while(deque.length&&deque[0]<=i-k){
            deque.shift();
        }
        // 若遍历到 k,则证明第一个窗口的结果已经产生
        if(i>=k-1){
            res.push(nums[deque[0]]);
        }
    }
    return res;
};

(使用 API 来解决:

var maxSlidingWindow = function(nums, k) {
    const res=[];
    let i=0;
    while(nums[i+k-1]!=null){
        const temp = nums.slice(i,i+k);
        res.push(Math.max(...temp))
        i++;
    }
    return res;
};