第十一天|栈与队列| 150. 逆波兰表达式求值, 239. 滑动窗口最大值,347.前 K 个高频元素,总结

目录

150. 逆波兰表达式求值

 239. 滑动窗口最大值(有点难)

方法1:自定义数组

方法2:双端队列实现

347.前 K 个高频元素

方法1:基于大顶堆实现

方法2:基于小顶堆实现

方法3:基于小顶堆的简化实现

总结


150栈操作,239队列操作,347大顶堆小顶堆

150. 逆波兰表达式求值

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

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。

该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

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

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。

  • 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈

逆波兰表达式相当于是二叉树中的后序遍历。

(中缀表达式对于计算机来说就不是很友好,因为计算机判断出运算符后不能直接运算,还要比较优先级,后缀表达式不需要考虑优先级了,直接利用栈来顺序处理。)

Integer.valueOf(s) 功能:将一个字符串解析为一个 Integer 对象。

    class Solution {
        public int evalRPN(String[] tokens) {
            Deque<Integer> stack = new LinkedList<>();
            for (String s : tokens) {
                if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
                    stack.push(stack.pop() + stack.pop());
                } else if ("-".equals(s)) {  // 注意 - 和/ 需要特殊处理
                    stack.push(-stack.pop() + stack.pop());
                } else if ("*".equals(s)) {
                    stack.push(stack.pop() * stack.pop());
                } else if ("/".equals(s)) {
                    int temp1 = stack.pop();
                    int temp2 = stack.pop();
                    stack.push(temp2 / temp1);
                } else {
                    stack.push(Integer.valueOf(s));
                }
            }
            return stack.pop();
        }
    }

 239. 滑动窗口最大值(有点难)

感觉自己独立很难code出来,虽然思路是理解了。

这是使用单调队列的经典题目。需要一个队列,将元素放进窗口,随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值即可。

需要我们自己来实现一个单调队列。单调队列,即单调递减或单调递增的队列。因为队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

例如 {2, 3, 5, 1 ,4} ,单调队列里只维护 {5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。

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

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

方法1:自定义数组

重点理解在add(),主要作用是向双端队列中添加元素,同时确保队列中的元素按降序排列。每次添加新元素时,移除所有小于新元素的末尾元素,从而保持队列的顺序。

    class Solution {
        public int[] maxSlidingWindow(int[] nums, int k) {
            //方法1:自定义数组
            if (nums.length == 1) {
                return nums;
            }
            int len = nums.length - k + 1;
            //存放结果元素的数组
            int[] res = new int[len];
            int num = 0;
            //自定义队列
            MyQueue myQueue = new MyQueue();
            //先将前k的元素放入队列
            for (int i = 0; i < k; i++) {
                myQueue.add(nums[i]);
            }
            res[num++] = myQueue.peek();
            for (int i = k; i < nums.length; i++) {
                //滑动窗口移除最前面的元素,移除是判断该元素是否放入队列
                myQueue.poll(nums[i - k]);
                //滑动窗口加入最后面的元素
                myQueue.add(nums[i]);
                //记录对应的最大值
                res[num++] = myQueue.peek();
            }
            return res;
        }
    }

    class MyQueue {
        Deque<Integer> deque = new LinkedList<>();

        //弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
        //同时判断队列当前是否为空
        void poll(int val) {
            if (!deque.isEmpty() && val == deque.peek()) {
                deque.poll();
            }
        }

        //添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
        //保证队列元素单调递减
        //比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
        void add(int val) {
            while (!deque.isEmpty() && val > deque.getLast()) {
                deque.removeLast();
            }
            deque.add(val);
        }

        //队列队顶元素始终为最大值
        int peek() {
            return deque.peek();
        }
    }

方法2:双端队列实现

理解一下。

    class Solution {
        public int[] maxSlidingWindow(int[] nums, int k) {
            //方法2:利用双端队列手动实现单调队列
            /**
             * 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
             * 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
             */
            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]范围内,不符合则要弹出
                while (!deque.isEmpty() && deque.peek() < i - k + 1) {
                    deque.poll();
                }
                // 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
                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;
        }
    }

347.前 K 个高频元素

思路主要涉及到如下三块内容:

  1. 统计元素出现频率(使用map(key,value))
  2. 对频率排序(优先级队列-->小顶堆)
  3. 找出前K个高频元素

优先级队列:

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

堆是一棵完全二叉树,大顶堆(堆头是最大元素),小顶堆(堆头是最小元素)。优先级队列从小到大排就是小顶堆,从大到小排就是大顶堆。

(为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。)

· 是使用小顶堆呢,还是大顶堆?

要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。(pop的是堆顶的元素

总体流程如图所示:

Code注释详解:

在Java中,PriorityQueue 是一个基于堆的数据结构,它的排序是在元素插入和删除时自动进行的。

实现小顶堆:PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]);

这段代码创建了一个 PriorityQueue,其元素为 int[] 数组,并使用自定义的比较器来决定队列中元素的顺序。具体来说,队列中元素按每个数组的第二个元素(索引为1)进行升序排序。

pair1[1] - pair2[1] 的结果决定了 PriorityQueue 的排序规则。如果结果为负数,表示 pair1 的第二个元素小于 pair2 的第二个元素,pair1 应排在 pair2 之前。

实现大顶堆:PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]);

这段代码创建了一个 PriorityQueue,其元素为 int[] 数组,并使用自定义的比较器来决定队列中元素的顺序。具体来说,队列中元素按每个数组的第二个元素(索引为1)进行降序排序。

PriorityQueue 是 Java 中的一个类,实现了优先队列的数据结构。优先队列是一种基于堆的数据结构,支持在对元素进行排序的基础上进行快速插入和删除操作。

(pair1, pair2) -> pair2[1] - pair1[1] 是一个自定义的比较器,使用 lambda 表达式来定义。pair2[1] - pair1[1] 的结果决定了 PriorityQueue 的排序规则:如果结果为负数,表示 pair2 的第二个元素小于 pair1 的第二个元素,pair2 应排在 pair1 之后。

遍历 Map 集合:for(Map.Entry<Integer, Integer> entry : map.entrySet())

  • Map.EntryMap 接口的一个内部接口,用于表示 Map 中的一个键值对(entry)。
  • map.entrySet() 方法返回一个 Set 集合,这个集合包含了 map 中的所有键值对(Map.Entry 对象)。
  • entry.getKey() 获取当前键值对的键,entry.getValue() 获取当前键值对的值。

var 关键字:for (var x : map.entrySet())

var 关键字:这里的 var 关键字使得编译器自动推断变量 x 的类型。在这个例子中,var entry 的类型会被推断为 Map.Entry<Integer, Integer>

  • var 是 Java 10 引入的一个关键字,用于局部变量类型推断。它可以根据变量的初始值来自动推断变量的类型。
  • var 只能用于局部变量声明,不能用于字段、方法参数、返回类型等。

在Java中,PriorityQueue 是一个基于堆的数据结构,它的排序是在元素插入和删除时自动进行的。

计算频次:

map.put(num, map.getOrDefault(num, 0) + 1);

方法1:基于大顶堆实现

大顶堆需要对所有元素进行排序,将所有数据加入大顶堆中,最终取出前k个元素,即就是出现频率前k高的元素。

    class Solution {
        public int[] topKFrequent(int[] nums, int k) {
//        方法1:基于大顶堆实现
            Map<Integer, Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
            for (int num : nums) {
                map.put(num, map.getOrDefault(num, 0) + 1);
            }
            //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
            //出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆)
            PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]);
            for (Map.Entry<Integer, Integer> entry : map.entrySet()) { //大顶堆需要对所有元素进行排序
                pq.add(new int[]{entry.getKey(), entry.getValue()});
            }
            int[] ans = new int[k];
            for (int i = 0; i < k; i++) { //依次从队头弹出k个,就是出现频率前k高的元素
                ans[i] = pq.poll()[0];
            }
            return ans;
        }
    }

方法2:基于小顶堆实现

不再需要在队列中添加所有元素,而是只需要维护k个元素。元素大于k的时候做比较,弹出堆头即最小元素。最终做了一个倒序输出,因为队列中是从小到大排列的。

创建数组的方法:

  • 静态初始化

int[] arr = {1, 2, 3, 4, 5};

  • 动态初始化

int[] arr = new int[5]; // 创建一个包含5个元素的数组

arr[0] = 1;

arr[1] = 2;

  • 创建并立即初始化

int[] arr = new int[]{1, 2, 3, 4, 5};

String[] strArr = new String[]{"Hello", "World"};

    class Solution {
        public int[] topKFrequent(int[] nums, int k) {
//            方法2:基于小顶堆实现
            Map<Integer, Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
            for (int num : nums) {
                map.put(num, map.getOrDefault(num, 0) + 1);
            }
            //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
            //出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
            PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
            for (Map.Entry<Integer, Integer> entry : map.entrySet()) { //小顶堆只需要维持k个元素有序
                if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
                    pq.add(new int[]{entry.getKey(), entry.getValue()});
                } else {
                    if (entry.getValue() > pq.peek()[1]) { //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                        pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                        pq.add(new int[]{entry.getKey(), entry.getValue()});
                    }
                }
            }
            int[] ans = new int[k];
//            倒叙弹出
            for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
                ans[i] = pq.poll()[0];
            }
            return ans;
        }
    }

方法3:基于小顶堆的简化实现

最后倒不倒序都可以,因为本题目中不限制输出顺序,若需要求,则可考虑。

    class Solution {
        public int[] topKFrequent(int[] nums, int k) {
//            方法3:基于小顶堆的简化实现
            // 优先级队列,为了避免复杂 api 操作,pq 存储数组
            // lambda 表达式设置优先级队列从大到小存储 o1 - o2 为从小到大,o2 - o1 反之
            PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
            int[] res = new int[k]; // 答案数组为 k 个元素
            Map<Integer, Integer> map = new HashMap<>(); // 记录元素出现次数
            for (int num : nums) {
                map.put(num, map.getOrDefault(num, 0) + 1);
            }
            for (var x : map.entrySet()) {  // entrySet 获取 k-v Set 集合
                // 将 kv 转化成数组
                int[] tmp = new int[2];
                tmp[0] = x.getKey();
                tmp[1] = x.getValue();
                pq.offer(tmp);
                // 下面的代码是根据小根堆实现的,我只保留优先队列的最后的k个,只要超出了k我就将最小的弹出,剩余的k个就是答案
                if (pq.size()>k){
                    pq.poll();
                }
            }
            for (int i = 0; i <k; i++) {
                res[i] = pq.poll()[0];
            }
            return res;
        }
    }

总结

1. 栈里面的元素在内存中是连续分布的么?

    这个问题有两个陷阱:

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

2. 有空可以做一下 71. 简化路径

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

栈:匹配问题

队列:滑动窗口(单调队列);优先级队列(大顶堆,小顶堆)

第十一天的总算是结束了,直冲Day13!(12是休息Day✌)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值