栈与队列模块算法题

162 阅读10分钟

栈与队列

数组表示栈或队列( js ),

栈:push()pop()方法分别用来入栈、出栈

队列:push()shift()方法分别用来入队、出队

1. 用栈实现队列

题目

​ 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明

  • 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

思路

用数组表示栈,其push()pop()方法分别用来入栈、出栈( js

  1. 两个栈,一个为进栈——队列元素入口栈,一个为出栈——队列元素出口栈
  2. 队时,元素直接进入进栈即可
  3. 队时,若出栈不空,则栈顶元素直接出出栈并返回其值;若空,则将进栈中的元素全部出栈并进入出栈中,然后栈顶元素出出栈并返回其值
  4. 返回队列开头的元素——将开头元素出队并保存其值,然后又让该元素进入出栈中(因为出队最终都是从出栈中出),最后返回其值
  5. 队列判空——判断两个栈是否都空,若都空,则返回true;否则,返回false

动态效果:

代码


var MyQueue = function () {
    this.inStack = []
    this.outStack = []
};

/** 
 * @param {number} x
 * @return {void}
 */
MyQueue.prototype.push = function (x) {
    this.inStack.push(x)
};

/**
 * @return {number}
 */
MyQueue.prototype.pop = function () {
    if (!this.outStack.length) {
        while (this.inStack.length) {
            this.outStack.push(this.inStack.pop())
        }
    }
    return this.outStack.pop()
};

/**
 * @return {number}
 */
MyQueue.prototype.peek = function () {
    let val = this.pop()
    this.outStack.push(val)
    return val
};

/**
 * @return {boolean}
 */
MyQueue.prototype.empty = function () {
    return !this.inStack.length && !this.outStack.length
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * var obj = new MyQueue()
 * obj.push(x)
 * var param_2 = obj.pop()
 * var param_3 = obj.peek()
 * var param_4 = obj.empty()
 */

2. 用队列实现栈

题目

​ 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意

  • 你只能使用队列的基本操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

思路

用数组表示队列,其push()shift()方法分别用来入队、出队( js

  1. 双队列

    1. 两个队列,一个用来实现元素出入栈(出入队列),一个用来辅助实现元素出栈(辅助队列
    2. 入栈时,元素直接进入出入队列
    3. 出栈时,若出入队列为空,则将辅助队列元素出队并进入出入队列中;若不空,则不必操作。然后,若出入队列元素个数等于1,则直接出队并返回该元素;若出入队列元素个数大于1,则将出入队列元素出队并进入辅助队列,直至出入队列元素个数等于1,然后让该元素出队并返回其值
    4. 返回栈顶元素——将栈顶元素出栈并保存其值,然后又让该元素进入出入队列中或入栈(因为出栈最终都是从出入队列中出),最后返回其值
    5. 栈判空——判断两个队列是否都空,若都空,则返回true;否则,返回false

    动态效果:

  2. 单队列

    即,双队列的优化。

    一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部即可。移动次数为队列元素个数减1。

代码

  1. 双队列

    
    var MyStack = function () {
        this.queue1 = []
        this.queue2 = []
    };
    
    /** 
     * @param {number} x
     * @return {void}
     */
    MyStack.prototype.push = function (x) {
        this.queue1.push(x)
    };
    
    /**
     * @return {number}
     */
    MyStack.prototype.pop = function () {
        if (!this.queue1.length) {
            [this.queue1, this.queue2] = [this.queue2, this.queue1]
        }
        while (this.queue1.length > 1)
            this.queue2.push(this.queue1.shift())
        return this.queue1.shift()
    };
    
    /**
     * @return {number}
     */
    MyStack.prototype.top = function () {
        let val = this.pop();
        this.queue1.push(val);
        return val;
    };
    
    /**
     * @return {boolean}
     */
    MyStack.prototype.empty = function () {
        return !this.queue1.length && !this.queue2.length
    };
    
    /**
     * Your MyStack object will be instantiated and called as such:
     * var obj = new MyStack()
     * obj.push(x)
     * var param_2 = obj.pop()
     * var param_3 = obj.top()
     * var param_4 = obj.empty()
     */
    
  2. 单队列

    
    var MyStack = function () {
        this.queue = []
    };
    
    /** 
     * @param {number} x
     * @return {void}
     */
    MyStack.prototype.push = function (x) {
        this.queue.push(x)
    };
    
    /**
     * @return {number}
     */
    MyStack.prototype.pop = function () {
        let n = this.queue.length - 1
        while (n--)
            this.queue.push(this.queue.shift())
        return this.queue.shift()
    };
    
    /**
     * @return {number}
     */
    MyStack.prototype.top = function () {
        let val = this.pop();
        this.queue.push(val);
        return val;
    };
    
    /**
     * @return {boolean}
     */
    MyStack.prototype.empty = function () {
        return !this.queue.length
    };
    
    /**
     * Your MyStack object will be instantiated and called as such:
     * var obj = new MyStack()
     * obj.push(x)
     * var param_2 = obj.pop()
     * var param_3 = obj.top()
     * var param_4 = obj.empty()
     */
    

3. 有效的括号

题目

​ 给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

考察点:栈的应用(括号匹配是使用栈解决的经典问题

思路

  1. 分析括号不匹配的情况有哪些?

    1. 左括号多余

    2. 右括号多余

    3. 括号类型不匹配

  2. 处理不匹配或匹配的情况

    • 若字符为左括号,则直接入栈
    • 若字符为右括号,则判断栈顶元素是否与之匹配(类型或方向),若匹配,则栈顶元素出栈(相消操作);若不匹配,则直接返回false
    • 最终,判断栈是否为空,若空,则返回true;若不空,则说明有多余的括号,返回false

代码

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
    let stack = []
    for (const i of s) {
        if (i === '(' || i === '[' || i === '{')
            stack.push(i)
        else if (i === ')' && stack[stack.length - 1] === '(')
            stack.pop()
        else if (i === ']' && stack[stack.length - 1] === '[')
            stack.pop()
        else if (i === '}' && stack[stack.length - 1] === '{')
            stack.pop()
        else
            return false
    }
    return !stack.length
};

4. 删除字符串中的所有相邻重复项

题目

​ 给出由小写字母组成的字符串 S重复项删除操作会选择两个相邻且相同的字母,并删除它们。

​ 在 S 上反复执行重复项删除操作,直到无法继续删除。

​ 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

考察点:栈的应用(此题仍为匹配问题

思路

  1. 将字符串的首字符加入栈中
  2. 利用循环遍历字符串中剩余字符
  3. 若当前字符与栈顶元素相同,则弹出栈顶元素(相消操作);若不同,则将该字符入栈
  4. 遍历结束后,将栈内元素出栈,连接成字符串并返回(注意出栈的字符顺序为目标值的逆序,所以还需要颠倒顺序再返回

代码

/**
 * @param {string} s
 * @return {string}
 */
var removeDuplicates = function (s) {
    let stack = [], r1 = "", r2 = ""
    stack.push(s[0])
    for (let i = 1; i < s.length; i++) {
        if (s[i] === stack[stack.length - 1])
            stack.pop()
        else
            stack.push(s[i])
    }
    while (stack.length)
        r1 += stack.pop()
    for (let i = r1.length - 1; i >= 0; i--)
        r2 += r1[i]
    return r2
};

5. 逆波兰表达式求值

题目

​ 给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式(后缀表达式)。

​ 请你计算该表达式。返回一个表示表达式值的整数。

注意

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

考察点:栈的应用(和上题做法类似,不过是把相消操作换成算数操作以及判断逻辑的变化)

思路

首先,对逆波兰表达式要有一定的了解:

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

故,依照上述内容,利用循环遍历字符串数组,用栈操作运算,最终返回栈顶元素。

注意点

  1. '+' 运算需要将字符串转换成数字;

    转换方法参考:js字符串转数字

  2. '-''/'运算需要注意参与运算数字的顺序,出栈的应为减数除数

  3. 两个整数之间的除法总是 向零截断,即当商为小数时,结果应保留较靠近零的整数。所以,当商为小数时,则需要向取整;但当商为小数时,则需要向取整,即所得商无论正负直接截取整数部分即可。

代码

  1. 普通版

    /**
     * @param {string[]} tokens
     * @return {number}
     */
    var evalRPN = function (tokens) {
        let stack = [], n
        for (const i of tokens) {
            switch (i) {
                case '+':
                    stack.push(stack.pop() + stack.pop())
                    break;
                case '-':
                    n = stack.pop()
                    stack.push(stack.pop() - n)
                    break;
                case '*':
                    stack.push(stack.pop() * stack.pop())
                    break;
                case '/':
                    n = stack.pop()
                    stack.push(Math.trunc(stack.pop() / n))
                    break;
                default:
                    stack.push(i * 1)
            }
        }
        return stack.pop()
    };
    
  2. 优化版

    /**
     * @param {string[]} tokens
     * @return {number}
     */
    var evalRPN = function (tokens) {
        let stack = []
        let s = new Map([
            ['+', (a, b) => a * 1 + b * 1],
            ['-', (a, b) => b - a],
            ['*', (a, b) => b * a],
            // | 0 是一个位运算,用于将浮点数结果截断为整数
            ['/', (a, b) => (b / a) | 0],
        ])
        for (const i of tokens) {
            if (!s.has(i))
                stack.push(i)
            else
                stack.push(s.get(i)(stack.pop(), stack.pop()))
        }
        return stack.pop()
    };
    

6. 滑动窗口最大值

题目

​ 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

​ 返回 滑动窗口中的最大值

思路

用队列存放元素——队首元素应为当前队列中所有元素的最大值,且元素顺序不能变,故需要自定义一个符合需求的队列。

  1. 设计队列(关键):
    1. constructor():添加一个双队列
    2. push(val):若双队列非空且队尾元素相比要加入队列的元素较小,则将队尾元素出队,直至队尾元素相对较大或队空,则加入元素到队尾;否则,说明队列中无比要加入队列的元素更小的元素,故直接加入元素到队尾(维护:队首元素为当前队列中所有元素的最大值,且元素顺序不变
    3. pop(val):若要出队的元素还在队列中,即队首元素等于val,则队首元素出队
    4. getMax():队首元素为当前队列中所有元素的最大值,故直接返回队首元素
  2. 利用循环将最初始的滑动窗口中的元素放入队列中,并获取最大值放入保存结果的数组中
  3. 利用循环移动滑动窗口,即元素出队、入队。每移动一次,则收集一次最大值
  4. 最终返回保存结果的数组

动态效果:

代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
    class deque {
        constructor() {
            this.queue = []
        }
        push(val) {
            while (this.queue.length && this.queue[this.queue.length - 1] < val)
                this.queue.pop()
            this.queue.push(val)
        }
        pop(val) {
            if (this.queue.length && this.queue[0] === val)
                this.queue.shift()
        }
        getMax() {
            return this.queue[0]
        }
    }

    let myDeque = new deque()
    let i = 0, j = 0
    let res = []
    while (i < k)
        myDeque.push(nums[i++])
    res.push(myDeque.getMax())
    while (i < nums.length) {
        myDeque.pop(nums[j++])
        myDeque.push(nums[i++])
        res.push(myDeque.getMax())
    }
    return res
};

7. 前 K 个高频元素

题目

​ 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

解决步骤

  1. 统计各元素出现频率
  2. 对频率进行排序
  3. 找出前K个高频元素

思路

  1. 用哈希法(Map)统计各元素出现频率
  2. 用优先级队列(小 顶/根 堆)对部分频率进行排序(时间复杂度较低)
  3. 最后将优先队列中的元素对应的 key 值返回

堆相关内容可以参考:数据结构——【堆】详解

代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function (nums, k) {
    let map = {}
    for (const i of nums) {
        map[i] = (map[i] || 0) + 1
    }

    let pq = new priorityQueue((a, b) => a[1] - b[1])
    for (const i of Object.entries(map)) {
        pq.push(i)
        if (pq.size() > k)
            pq.pop()
    }

    let res = []
    for (let i = pq.size() - 1; i >= 0; i--)
        res[i] = pq.pop()[0]

    return res
};
class priorityQueue {
    constructor(compareFn) {
        this.queue = []
        this.compareFn = compareFn
    }
    push(item) {
        this.queue.push(item)
        let index = this.queue.length - 1
        let parent = Math.floor((index - 1) / 2)
        while (parent >= 0 && this.compare(parent, index) > 0) {
            [this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]]
            index = parent
            parent = Math.floor((index - 1) / 2)
        }
    }
    pop() {
        let r = this.queue[0]
        this.queue[0] = this.queue.pop()
        let index = 0, left = 1
        let child = this.compare(left, left + 1) > 0 ? left + 1 : left
        while (child != undefined && this.compare(index, child) > 0) {
            [this.queue[index], this.queue[child]] = [this.queue[child], this.queue[index]]
            index = child
            left = 2 * index + 1
            child = this.compare(left, left + 1) > 0 ? left + 1 : left
        }
        return r
    }
    size() {
        return this.queue.length
    }
    compare(i, j) {
        if (this.queue[i] === undefined)
            return 1
        if (this.queue[j] === undefined)
            return -1

        return this.compareFn(this.queue[i], this.queue[j])
    }
}