day 11 | 逆波兰表达式求值、滑动窗口最大值、前 K 个高频元素

  1. 逆波兰表达式求值

本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题

题目链接/文章讲解/视频讲解:https://programmercarl.com/0150.%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC.html

  1. 滑动窗口最大值 (有点难度,可能代码写不出来,但一刷至少需要理解思路)

之前讲的都是栈的应用,这次该是队列的应用了。

本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html

347.前 K 个高频元素 (有点难度,可能代码写不出来,一刷至少需要理解思路)

大/小顶堆的应用, 在C++中就是优先级队列

本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.html

逆波兰表达式求值

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();
    }
}

你提到的 波兰表达式(Polish Notation),也叫做 前缀表达式(Prefix Notation),是一种数学表达式的写法,在这种写法中,运算符放在操作数之前。

波兰表达式的基本规则

  1. 运算符在操作数之前:例如,+ 1 2 表示 1 + 2,而 1 + 2 是普通的中缀表达式。
  2. 没有括号:运算符的顺序决定了运算的先后,不需要使用括号来明确优先级。
  3. 每个运算符都需要两个操作数

波兰表达式的例子

  • 中缀表达式:3 + 4

  • 波兰表达式:+ 3 4

  • 中缀表达式:(3 + 4) * 5

  • 波兰表达式:* + 3 4 5

如何逆波兰表达式(Reverse Polish Notation, RPN)进行求值

逆波兰表达式(RPN)是波兰表达式的变种,其中运算符跟随在操作数之后。例如,3 4 + 表示 3 + 4

逆波兰表达式求值的算法

  1. 使用栈:我们从左到右遍历表达式的每个元素:
    • 如果是操作数(数字),就将其压入栈中。
    • 如果是运算符(+, -, *, / 等),则从栈中弹出两个操作数,进行相应的计算,然后将结果压回栈中。
  2. 最终栈中会剩下一个元素,即为表达式的计算结果。

代码解析:

public int evalRPN(String[] tokens) {
    // 使用双端队列作为栈来存储数字
    Deque<Integer> stack = new LinkedList<>();
    
    // 遍历 tokens 数组中的每个元素
    for (String s : tokens) {
        // 如果是加法运算符
        if ("+".equals(s)) {
            // 弹出两个数字并计算结果
            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();
}

代码逐行解释:

  1. Deque<Integer> stack = new LinkedList<>();:使用双端队列(Deque)来模拟栈。LinkedListDeque 的一个实现,它支持高效的头尾操作。
  2. 遍历输入的每个元素 s
    • 如果 s 是加法、减法、乘法或除法运算符,就从栈中弹出两个元素进行计算,并将计算结果压回栈。
    • 如果 s 是数字,将它转换为整数并压入栈中。
  3. stack.push():将元素压入栈。
  4. stack.pop():将栈顶元素弹出,用于运算操作。
  5. stack.pop()stack.push() 操作实现了数字和运算符的交换:每个操作符都会用两个栈中的数字进行计算,然后将结果压回栈中。
  6. 最终,栈中只剩下一个元素,它就是逆波兰表达式的计算结果

时间复杂度

  • 时间复杂度:O(n),其中 ntokens 数组的长度。每个元素最多进栈和出栈一次,所以时间复杂度是 O(n)。
  • 空间复杂度:O(n),栈最多需要存储 n 个数字。

例子

假设输入是:

String[] tokens = {"2", "1", "+", "3", "*"};
  1. 初始栈为空。
  2. 第一个元素 2 入栈:[2]
  3. 第二个元素 1 入栈:[2, 1]
  4. 遇到加法运算符 "+",弹出 12,计算 2 + 1 = 3,结果入栈:[3]
  5. 第三个元素 3 入栈:[3, 3]
  6. 遇到乘法运算符 "*",弹出 33,计算 3 * 3 = 9,结果入栈:[9]

最终栈中只有一个元素 9,即为结果。

总结:

  • 波兰表达式(前缀表达式)是运算符在操作数之前的一种写法,而 逆波兰表达式(后缀表达式)是操作数在运算符之后的一种写法。
  • 通过栈来模拟逆波兰表达式的求值非常高效,利用栈的后进先出特性来处理运算符和操作数。

滑动窗口最大值

//解法二
//利用双端队列手动实现单调队列
/**
 * 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
 * 单调递减队列类似 (head -->3 --> 2 --> 1 --> 0 (--> tail) (左边为头结点,元素存的是下标)
 */
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]范围内,不符合则要弹出
            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;
    }
}

前 K 个高频元素

    //解法2:基于小顶堆实现
    public int[] topKFrequent2(int[] nums, int k) {
        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;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值