LeetCode--栈与队列

LeetCode--栈与队列

基础知识

是一种LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。ECMAScript为数组专门提供了 push()pop() 方法,以便实现类似栈的行为。 push() 方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而 pop() 方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项。
队列数据结构的访问规则是FIFO(Fist-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项。ECMAScript为数组专门提供了 shift() 和 unshift() 方法,以便实现类似队列的行为。由于 push() 是向数组末端添加数组项的方法,因此要模拟队列只需一个从数组前端取得数组项的方法。实现这一操作的数组方法就是 shift() ,它能够移除数组中的第一个项并返回该项,同时将数组长度减1。unshift() 与 shift() 的用途相反:它能在数组前端添加任意个数组项并返回新数组的长度。因此,同时使用 unshift() 和 pop() 方法,可以从相反的方向来模拟队列,即在数组的前端添加数组项,从数组末端移除数组项。
将push()和pop()结合在一起,我们就可以实现类似栈的行为。将shift()和push()方法结合在一起,可以像使用队列一样使用数组。

技巧

1.由于栈结构的特殊性,非常适合做对称匹配类的题目。
2.递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
3.一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
4.单调队列。
5.优先级队列。

题目

1.用栈实现队列
简单
在这里插入图片描述
思路:
两个栈一个输入栈,一个输出栈。在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

var MyQueue = function() {
    this.stackIn = [];
    this.stackOut = [];
};

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

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

/**
 * @return {number}
 */
MyQueue.prototype.peek = function() {
    const x = this.pop();
    this.stackOut.push(x);
    return x;
};

/**
 * @return {boolean}
 */
MyQueue.prototype.empty = function() {
    if(!this.stackIn.length&&!this.stackOut.length)return true;
    return false;
};

/**
 * 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.用队列实现栈
简单
在这里插入图片描述
思路:
1.两个队。用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
2.一个队。一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

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.queue2.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() {
    const x = this.pop();
    this.queue1.push(x);
    return x;
};

/**
 * @return {boolean}
 */
MyStack.prototype.empty = function() {
    if(!this.queue1.length&&!this.queue2.length){
        return true;
    }
    return false;
};

/**
 * 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()
 */
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 len = this.queue.length;
    while(len>1){
        this.queue.push(this.queue.shift());
        len--;
    }
    return this.queue.shift();
};

/**
 * @return {number}
 */
MyStack.prototype.top = function() {
    let x = this.pop();
    this.push(x);
    return x;
};

/**
 * @return {boolean}
 */
MyStack.prototype.empty = function() {
    return !this.queue.length;
};

3.有效的括号
简单
在这里插入图片描述
思路:
这里有三种不匹配的情况,1.字符串里左方向的括号多余了 ,所以不匹配。2.括号没有多余,但是 括号的类型没有匹配上。3.第三种情况,字符串里右方向的括号多余了,所以不匹配。第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false。第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false。第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false。字符串遍历完之后,栈是空的,就说明全都匹配了。

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    let stack = [];
    for (let i = 0; i <s.length; i++){
        switch (s[i]){
            case '(':
                stack.push(')');
                break;
            case '[':
                stack.push(']');
                break;
            case '{':
                stack.push('}');
                break;
            default:
                if(stack.pop()!=s[i]) return false;
        }
    }
    return !stack.length;
};

4.删除字符串中的所有相邻重复项
简单
在这里插入图片描述
思路:
1.双指针。
2.栈。在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素。用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。然后再去做对应的消除操作。从栈中弹出剩余元素,因为从栈里弹出的元素是倒序的,所以在对字符串进行反转一下,就得到了最终的结果。

/**
 * @param {string} s
 * @return {string}
 */
var removeDuplicates = function(s) {
    let stack = [];
    for(let i = 0;i<s.length;i++){
        let x = stack.pop();
        if( x == s[i])continue;
        stack.push(x);
        stack.push(s[i]);
    }
    return stack.join('');
};

5.删除字符串中的所有相邻重复项
简单
在这里插入图片描述
思路:
遇到数字压入栈中,遇到符号将栈顶两个元素弹出并运算的结果再压入栈。

/**
 * @param {string[]} tokens
 * @return {number}
 */
var evalRPN = function(tokens) {
    let calculate = new Map([
        //如果不加number()会变成字符串相加 '2'+'1'=‘21’
        ['+',(a,b)=>Number(b)+Number(a)],
        ['-',(a,b)=>b-a],//注意第二个运算数会被先弹出来 所以是b-a 除法同理
        ['*',(a,b)=>b*a],
        //不能用 Math.floor, 因为 Math.floor 是向下取整,parseInt 可以去掉小数部分。如果是计算结果是负数 Math.floor(-1.5) = -2 。 parseInt(-1.5) = -1。
        //位运算中的 | 也可以做到。这里用到了位运算中的或。整数与0的位或运算,都是本身。浮点数不支持位运算,过程中会自动转化成整数,利用这一点,可以将浮点数与0进行位或运算即可达到取整目的。
        ['/',(a,b)=>parseInt(b/a)]//["/", (a, b) => (b / a) | 0]
    ]);
    let stack = [];
    for(let i = 0; i<tokens.length; i++){
        //数字压入栈
        if(!calculate.has(tokens[i])){
            stack.push(tokens[i]);
            continue;
        }
        stack.push(calculate.get(tokens[i])(stack.pop(),stack.pop()));
    }
    return stack.pop();
};

6.滑动窗口最大值
困难
在这里插入图片描述
思路:
主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
单调队列,即单调递减或单调递增的队列。对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。设计单调队列的时候,pop,和push操作要保持如下规则:
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
使用单调队列的时间复杂度是 O(n)。nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。空间复杂度因为我们定义一个辅助队列,所以是O(k)。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function(nums, k) {
    //定义一个单调递减队列
    class maxQueue{
        queue;
        constructor(){
            this.queue = [];
        }
        enqueue(value){
            //向单调递减队列中添加新元素 将比该元素小的值弹出 队列一直保持单调递减 
            let back = this.queue.length-1;
            //此处注意队列为空时取值为undefined 判断时一定要写成this.queue[back]!=undefined而不是!this.queue[back]
            while(this.queue[back]!=undefined&&value>this.queue[back]){
                this.queue.pop();
                back = this.queue.length-1;
            }
            this.queue.push(value);
        }
        dequeue(value){
            //窗口向右滑动后队列中元素已经被记录过,位于窗口左侧,则进行删除
            //如果前面的元素仍在队列中 则一定是最大值的位置 因为enqueue操作一定会将比它小的元素弹出
            if(value==this.queue[0]){
                this.queue.shift();
            }
        }
        front(){
            return this.queue[0]
        }
    }
    let res = [];
    let myqueue = new maxQueue();
    let i = j = 0;
    //双指针表示滑动窗口 先将右指针滑到右边
    while(j<k){
        myqueue.enqueue(nums[j]);
        j++;
    }
    //添加第一个窗口最大值
    res.push(myqueue.front());
    //窗口滑动
    while(j<nums.length){
        //新加入元素到窗口 队列应该如何改动
        myqueue.enqueue(nums[j]);
        //从窗口删除遍历过的元素 队列应该如何改动
        myqueue.dequeue(nums[i]);
        //记录每个窗口的最大值
        res.push(myqueue.front());
        i++;
        j++;
    }
    return res;
};

7.前 K 个高频元素
中等
在这里插入图片描述
思路:
统计元素出现的频率,这一类的问题可以使用map来进行统计。
优先级队列是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!所以排序的过程的时间复杂度是 O ( log ⁡ k ) O(\log k) O(logk),整个算法的时间复杂度是 O ( n log ⁡ k ) O(n\log k) O(nlogk)

#总结

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    //优先级队列是一个完全二叉树
    class Heap{
        constructor(compareFn){
            //实现一个小顶堆 
            this.compareFn = compareFn;
            this.heap = [];
        }
        //添加元素
        push(x){
            //将元素加入堆底
            this.heap.push(x);
            //记录插入元素的索引
            let index = this.heap.length-1;
            //父节点的索引
            let parent = Math.floor((index-1)/2);
            //上浮 父节点比插入家电大 则将插入节点上浮
            while(parent>=0&&this.compare(parent,index)>0){
                [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
                //更新索引
                index = parent;
                parent = Math.floor((index-1)/2);
            }   
        }
        //弹出栈顶元素并返回值
        pop(){
            let top = this.heap[0];
            //弹出堆低元素并赋值到栈顶 相当于将栈顶元素删除,栈低元素移动到栈顶
            this.heap[0] = this.heap.pop();
            //index记录索引
            let index = 0;
            //左子节点索引为left 则右节点为left+1
            let left = 2*index+1;
            //下沉操作只需考虑子结点中最小的那个 如果更小则该节点下沉 与子节点交换位置
            let smallerChild = this.compare(left,left+1)>0?left+1:left;
            while(this.heap[smallerChild]!=undefined&&this.compare(index,smallerChild)>0){
                [this.heap[index],this.heap[smallerChild]] = [this.heap[smallerChild],this.heap[index]];
                //更新索引
                index= smallerChild;
                left = 2*index+1;
                smallerChild = this.compare(left,left+1)>0?left+1:left;
            }
            return top;
        }
        //元素排序方式 决定是小顶堆还是大顶堆
        compare(index1,index2){
            if(this.heap[index1]==undefined)return 1;
            if(this.heap[index2]==undefined)return -1;
            return this.compareFn(this.heap[index1],this.heap[index2]);
        }
        size(){
            return this.heap.length;
        }
    }
    //使用map记录整数及其出现次数
    let itemToCount = new Map();
    for(let i =  0; i<nums.length;i++){
        //get用来获取一个Map对象指定的元素,返回的是键所对应的值,如果不存在则会返回undefined
        itemToCount.set(nums[i],(itemToCount.get(nums[i])||0)+1);
    }
    let minHeap = new Heap((a,b)=>a[1]-b[1]);
    //entries()方法返回一个新的包含 [key, value]的 Iterator对象,返回的迭代器的迭代顺序与 Map对象的插入顺序相同。
    for(let entry of itemToCount.entries()){
        minHeap.push(entry);
        if(minHeap.size()>k){
            minHeap.pop();
        }
    }
    let ret = [];
    for(let i = minHeap.size()-1;i>=0;i--){
        ret[i] = minHeap.pop()[0];
    }
    return ret;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值