• LeetCode精选200道--栈与队列篇


    栈与队列理论基础

    队列是先进先出,栈是先进后出。

    image-20220810103008927

    来说一说栈,栈先进后出,如图所示:

    image-20220810103124069

    栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。

    栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)

    从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。

    vector是一个能够存放任意类型的动态数组,能够增加和压缩数据

    deque 是 double-ended queue 的缩写,又称双端队列容器。

    image-20220810103433277

    队列

    我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。

    deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。

    SGI STL中 队列底层实现缺省情况下一样使用deque实现的。

    我们也可以指定vector为栈的底层实现,初始化语句如下:

    std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
    
    • 1

    刚刚讲过栈的特性,对应的队列的情况是一样的。

    队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构

    用栈实现队列

    使用栈实现队列的下列操作:

    push(x) – 将一个元素放入队列的尾部。
    pop() – 从队列首部移除元素。
    peek() – 返回队列首部的元素。
    empty() – 返回队列是否为空。

    示例:

    MyQueue queue = new MyQueue();
    queue.push(1);
    queue.push(2);
    queue.peek();  // 返回 1
    queue.pop();   // 返回 1
    queue.empty(); // 返回 false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    说明:

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

    思路

    这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。

    使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。

    下面动画模拟以下队列的执行过程如下:

    执行语句:
    queue.push(1);
    queue.push(2);
    queue.pop(); 注意此时的输出栈的操作
    queue.push(3);
    queue.push(4);
    queue.pop();
    queue.pop();注意此时的输出栈的操作
    queue.pop();
    queue.empty();

    232.用栈实现队列版本2

    上面代码的操作就是这个动图的执行过程…

    在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。

    最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

    在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。

    用队列实现栈

    使用队列实现栈的下列操作:

    • push(x) – 元素 x 入栈
    • pop() – 移除栈顶元素
    • top() – 获取栈顶元素
    • empty() – 返回栈是否为空

    注意:

    • 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
    • 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
    • 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。

    思路

    队列模拟栈,其实一个队列就够了,那么我们先说一说两个队列来实现栈的思路。

    队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。

    但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的!

    如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

    模拟的队列执行语句如下:

    queue.push(1);        
    queue.push(2);        
    queue.pop();   // 注意弹出的操作       
    queue.push(3);        
    queue.push(4);       
    queue.pop();  // 注意弹出的操作    
    queue.pop();    
    queue.pop();    
    queue.empty();    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    225.用队列实现栈

    class MyStack {
    
        Queue<Integer> queue1; // 和栈中保持一样元素的队列
        Queue<Integer> queue2; // 辅助队列
    
        /** Initialize your data structure here. */
        public MyStack() {
            queue1 = new LinkedList<>();
            queue2 = new LinkedList<>();
        }
        
        /** Push element x onto stack. */
        public void push(int x) {
            queue2.offer(x); // 先放在辅助队列中
            while (!queue1.isEmpty()){
                queue2.offer(queue1.poll());
            }
            Queue<Integer> queueTemp;
            queueTemp = queue1;
            queue1 = queue2;
            queue2 = queueTemp; // 最后交换queue1和queue2,将元素都放到queue1中
        }
        
        /** Removes the element on top of the stack and returns that element. */
        public int pop() {
            return queue1.poll(); // 因为queue1中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
        }
        
        /** Get the top element. */
        public int top() {
            return queue1.peek();
        }
        
        /** Returns whether the stack is empty. */
        public boolean empty() {
            return queue1.isEmpty();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    优化

    其实这道题目就是用一个队列就够了。

    一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

    class MyStack {
        // Deque 接口继承了 Queue 接口
        // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
        Deque<Integer> que1;
        /** Initialize your data structure here. */
        public MyStack() {
            que1 = new ArrayDeque<>();
        }
        
        /** Push element x onto stack. */
        public void push(int x) {
            que1.addLast(x);
        }
        
        /** Removes the element on top of the stack and returns that element. */
        public int pop() {
            int size = que1.size();
            size--;
            // 将 que1 导入 que2 ,但留下最后一个值
            while (size-- > 0) {
                que1.addLast(que1.peekFirst());
                que1.pollFirst();
            }
    
            int res = que1.pollFirst();
            return res;
        }
        
        /** Get the top element. */
        public int top() {
            return que1.peekLast();
        }
        
        /** Returns whether the stack is empty. */
        public boolean empty() {
            return que1.isEmpty();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    有效的括号

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

    有效字符串需满足:

    • 左括号必须用相同类型的右括号闭合。
    • 左括号必须以正确的顺序闭合。
    • 注意空字符串可被认为是有效字符串。

    示例 1:

    • 输入: “()”
    • 输出: true

    示例 2:

    • 输入: “()[]{}”
    • 输出: true

    示例 3:

    • 输入: “(]”
    • 输出: false

    示例 4:

    • 输入: “([)]”
    • 输出: false

    示例 5:

    • 输入: “{[]}”
    • 输出: true

    思路

    括号匹配是使用栈解决的经典问题。

    题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。

    先来分析一下 这里有三种不匹配的情况,

    第一种情况,字符串里左方向的括号多余了 ,所以不匹配。

    括号匹配1

    第二种情况,括号没有多余,但是 括号的类型没有匹配上。

    括号匹配2

    第三种情况,字符串里右方向的括号多余了,所以不匹配。

    括号匹配3

    我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。

    至此,代码如下:

    class Solution {
        public boolean isValid(String s) {
            Deque<Character> deque = new LinkedList<>();
            char ch;
            for (int i = 0; i < s.length(); i++) {
                ch = s.charAt(i);
                //碰到左括号,就把相应的右括号入栈
                if (ch == '(') {
                    deque.push(')');
                }else if (ch == '{') {
                    deque.push('}');
                }else if (ch == '[') {
                    deque.push(']');
                } else if (deque.isEmpty() || deque.peek() != ch) {
                    //没有匹配的,直接退出
                    return false;
                }else {//如果是右括号判断是否和栈顶元素匹配
                    deque.pop();
                }
            }
            //最后判断栈中元素是否匹配
            return deque.isEmpty();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

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

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

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

    示例:

    • 输入:“abbaca”
    • 输出:“ca”
    • 解释:例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。

    提示:

    • 1 <= S.length <= 20000
    • S 仅由小写英文字母组成。

    思路

    这道题目就像是我们玩过的游戏对对碰,如果相同的元素放在挨在一起就要消除。

    可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如果消除呢,特别是消除之后又有新的元素可能挨在一起。

    此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。

    递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

    相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是Segmentation fault(当然不是所有的Segmentation fault 都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。

    字符串作为栈

    class Solution {
        public String removeDuplicates(String s) {
            // 将 res 当做栈
            StringBuffer res = new StringBuffer();
            // top为 res 的长度
            int top = res.length();
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                // 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
                if (top >= 0 && res.charAt(top) == c) {
                    res.deleteCharAt(top);
                    top--;
                // 否则,将该字符 入栈,同时top++
                } else {
                    res.append(c);
                    top++;
                }
            }
            return res.toString();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    使用 Deque 作为堆栈(推荐)

    ArrayDeque会比LinkedList在除了删除元素这一点外会快一点
    参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist

     public String removeDuplicates(String S) {
    
            ArrayDeque<Character> deque = new ArrayDeque<>();
            char ch;
            for (int i = 0; i < S.length(); i++) {
                //1.遍历字符串
                ch = S.charAt(i);
                //2.如果字符串为空,或者不等于上一次放进来的字符
                if (deque.isEmpty() || deque.peek() != ch) {
                    //3.那就放入栈
                    deque.push(ch);
                } else {
                    //4.否则弹出
                    deque.pop();
                }
            }
         //5.定义空字符串
            String str = "";
            //剩余的元素即为不重复的元素
         //6.弹出栈中元素并加入字符串
            while (!deque.isEmpty()) {
                str = deque.pop() + str;
            }
         //7.返回字符串
            return str;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    逆波兰表达式求值

    根据 逆波兰表示法,求表达式的值。

    有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

    注意 两个整数之间的除法只保留整数部分。

    可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

    示例 1:

    输入:tokens = [“2”,“1”,“+”,“3”,“*”]
    输出:9
    解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
    示例 2:

    输入:tokens = [“4”,“13”,“5”,“/”,“+”]
    输出:6
    解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
    示例 3:

    输入:tokens = [“10”,“6”,“9”,“3”,“+”,“-11”,““,”/“,””,“17”,“+”,“5”,“+”]
    输出:22
    解释:该算式转化为常见的中缀算术表达式为:
    ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
    = ((10 * (6 / (12 * -11))) + 17) + 5
    = ((10 * (6 / -132)) + 17) + 5
    = ((10 * 0) + 17) + 5
    = (0 + 17) + 5
    = 17 + 5
    = 22

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

    思路

    递归就是用栈来实现的。

    所以栈与递归之间在某种程度上是可以转换的! 这一点我们在后续讲解二叉树的时候,会更详细的讲解到。

    那么来看一下本题,其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。

    但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。

    在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,和1047.删除字符串中的所有相邻重复项 (opens new window)中的对对碰游戏是不是就非常像了。

    150.逆波兰表达式求值

        public int evalRPN(String[] tokens) {
    
            //1.声明栈
            Deque<Integer> stack = new LinkedList();
            //2.遍历数组
            for (int i = 0; i < tokens.length; ++i) {
                //3.如果碰见+号了,那么弹出两个元素,把它们相加的和放到栈中
                if ("+".equals(tokens[i])) {        // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
                    stack.push(stack.pop() + stack.pop());      // 注意 - 和/ 需要特殊处理、
                    //4.碰见-号了,那么弹出两个元素,相减(第二个减第一个)把它们相减的和放到栈中
                } else if ("-".equals(tokens[i])) {
                    stack.push(-stack.pop() + stack.pop());
                    //5.碰见*号了,弹出两个元素,把它们相乘的积放到栈中
                } else if ("*".equals(tokens[i])) {
                    stack.push(stack.pop() * stack.pop());
                    //6.遇见/号,弹出两个元素
                } else if ("/".equals(tokens[i])) {
                    //为什么要写成这样的形式,因为第二个数除第一个数没办法区分谁是第一第二,所以通过变量temp来区分
                    int temp1 = stack.pop();
                    int temp2 = stack.pop();
                    stack.push(temp2 / temp1);
                } else {
                    //遇见正常的数,那么就把它转换为Integer类型放到栈中
                    stack.push(Integer.valueOf(tokens[i]));
                }
            }
            return stack.pop();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    题外话

    我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。

    例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算法,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦!

    那么将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/”, “+”] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。

    可以说本题不仅仅是一道好题,也展现出计算机的思考方式。

    在1970年代和1980年代,惠普在其所有台式和手持式计算器中都使用了RPN(后缀表达式),直到2020年代仍在某些模型中使用了RPN。

    这些人真聪明!!!

    滑动窗口最大值

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

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

    进阶:

    你能在线性时间复杂度内解决此题吗?

    image-20220814145835141

    提示:

    • 1 <= nums.length <= 10^5
    • -10^4 <= nums[i] <= 10^4
    • 1 <= k <= nums.length

    思路

    这是使用单调队列的经典题目。(递减)

    这个队列应该长这个样子:

    class MyQueue {
    public:
        void pop(int value) {
        }
        void push(int value) {
        }
        int front() {
            return que.front();
        }
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。

    没有这种数据结构,我们需要自己进行设计!

    其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。

    那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列

    不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。

    设计单调队列的时候,pop,和push操作要保持如下规则:

    1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作(保证不重复,也是保证递减)
    2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止(保证队列元素递减)

    保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

    总之要保证队列里单调递减或递增的原则,所以叫做单调队列

    为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下

    239.滑动窗口最大值-2

    那么我们用什么数据结构来实现这个单调队列呢?

    使用deque最为合适

    我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。

    基于刚刚说过的单调队列pop和push的规则,代码不难实现,如下:

    class Solution {
        public int[] maxSlidingWindow(int[] nums, int k) {
            
            ArrayDeque<Integer> deque = new ArrayDeque<>();
            int n = nums.length;
            //存放结果集
            int[] res = new int[n - k + 1];
            int idx = 0;
            for(int i = 0;i<n;i++){
                //根据题意:i为nums下标,是要在[i - k + 1, i]中选到最大值,只需要保证两点
                //1.队列头节点需要在[i - k + 1, i]范围内,不符合则要弹出
                //peek方法是返回队列头部元素
                while(!deque.isEmpty() && deque.peek() < i-k+1){
                    // pop() 一般指弹栈 poll()指出队
                    deque.poll();
                }
                //既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
                while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]){
                    deque.pollLast();
                }
    
                deque.offer(i);
    
                //因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将该队列头节点放入结果就行了
                if(i >= k -1){
                    res[idx++] = nums[deque.peek()];
                }
            }
            return res;
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    前 K 个高频元素

    给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

    示例 1:

    • 输入: nums = [1,1,1,2,2,3], k = 2
    • 输出: [1,2]

    示例 2:

    • 输入: nums = [1], k = 1
    • 输出: [1]

    提示:

    • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
    • 你的算法的时间复杂度必须优于 O ( n log ⁡ n ) O(n \log n) O(nlogn) , n 是数组的大小。
    • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
    • 你可以按任意顺序返回答案。

    堆的特点

    1. 堆是一棵完全二叉树;
    2. 堆序性 (heap order): 任意节点都优于它的所有孩子
      a. 如果是任意节点都大于它的所有孩子,这样的堆叫大顶堆,Max Heap;
      b. 如果是任意节点都小于它的所有孩子,这样的堆叫小顶堆,Min Heap;

    image-20220814163253675

    左图是小顶堆,可以看出对于每个节点来说,都是小于它的所有孩子的,注意是所有孩子,包括孙子,曾孙…

    1. 既然堆是用数组来实现的,那么我们可以找到每个节点和它的父母/孩子之间的关系,从而可以直接访问到它们。

    img

    比如对于节点 3 来说,

    • 它的 Index = 1,
    • 它的 parent index = 0,
    • 左孩子 left child index = 3,
    • 右孩子 right child index = 4.

    可以归纳出如下规律:

    • 设当前节点的 index = x,
    • 那么 parent index = (x-1)/2,
    • 左孩子 left child index = 2*x + 1,
    • 右孩子 right child index = 2*x + 2.

    有些书上可能写法稍有不同,是因为它们的数组是从 1 开始的,而我这里数组的下标是从 0 开始的,都是可以的。

    这样就可以从任意一个点,一步找到它的孙子、曾孙子,真的太方便了

    思路

    这道题目主要涉及到如下三块内容:

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

    首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

    然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列

    什么是优先级队列呢?

    其实就是一个披着队列外衣的堆

    347.前K个高频元素

    class Solution {
        public int[] topKFrequent(int[] nums, int k) {
            int[] result = new int[k];
            HashMap<Integer, Integer> map = new HashMap<>();
            for (int num : nums) {
                map.put(num, map.getOrDefault(num, 0) + 1);
            }
    
            Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
            // 根据map的value值,构建于一个大顶堆(o1 - o2: 小顶堆, o2 - o1 : 大顶堆)
            PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
            for (Map.Entry<Integer, Integer> entry : entries) {
                queue.offer(entry);
            }
            for (int i = k - 1; i >= 0; i--) {
                result[i] = queue.poll().getKey();
            }
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    总结

    文章来自https://www.programmercarl.com/

    栈与队列的理论基础

    1. C++中stack,queue 是容器么?
    2. 我们使用的stack,queue是属于那个版本的STL?
    3. 我们使用的STL中stack,queue是如何实现的?
    4. stack,queue 提供迭代器来遍历空间么?

    相信不仅仅是C++中有这些问题,那么大家使用其他编程语言,也可以考虑一下这四个问题,栈和队列是如何实现的。

    栈与队列是我们熟悉的不能再熟悉的数据结构,但它们的底层实现,很多同学都比较模糊,这其实就是基础所在。

    可以出一道面试题:栈里面的元素在内存中是连续分布的么?

    这个问题有两个陷阱:

    • 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
    • 陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。

    所以这就是考察候选者基础知识扎不扎实的好问题。

    大家还是要多多重视起来!

    了解了栈与队列基础之后,那么可以用栈与队列:栈实现队列 (opens new window)栈与队列:队列实现栈 (opens new window)来练习一下栈与队列的基本操作。

    值得一提的是,用栈与队列:用队列实现栈还有点别扭 (opens new window)中,其实只用一个队列就够了。

    一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

    如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构。

    再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。

    cd a/b/c/../../
    
    • 1

    这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用。这在leetcode上也是一道题目,编号:71. 简化路径,大家有空可以做一下。

    递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

    所以栈在计算机领域中应用是非常广泛的。

    有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。

    所以数据结构与算法的应用往往隐藏在我们看不到的地方

  • 相关阅读:
    Unity 导航寻路快速上手
    SLAM的本质就是六个字
    操作系统程序作业
    scwgcna官网教程中英文实战高维wgcna分析 单细胞wgcna分析
    Ansible循环与判断
    C#中从一个路径复制SQLite数据库并将其粘贴到另一路径
    ios小程序蓝牙发送信息失败,报10004
    开咖啡店生意不好怎么办?这些注意事项一件不能少
    ASP.NET Core Web API Swagger 按标签Tags分组排序显示
    JAVA的@EXCEL导出导入常用注解汇总
  • 原文地址:https://blog.csdn.net/qq_45714272/article/details/126333632