算法Day10|150-逆波兰表达式求值;239-滑动窗口最大值;前k个高频元素

58 阅读6分钟

1、题目1:150-逆波兰表达式求值

题目:leetcode.cn/problems/ev…

类型:中等

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

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

注意:

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

思路:

1、什么是逆波兰表达式?

也叫后缀表达式,我们常用的是中缀表达式,例如(1+2)*(3+4)

换成二叉树长这样,而二叉树后缀表达式就是我们表达式:12+34+x

暂时无法在飞书文档外展示此内容

后缀表达式顺序处理字符串,遇到计算符号从栈出取出来计算

编码比较容易

public static int evalRPN(String[] tokens) {

    Stack<Integer> stack = new Stack<>();
    for (String token : tokens) {
        if(token.equals("+")){
            Integer pop = stack.pop();
            Integer pop1 = stack.pop();
            stack.add(pop1+pop);
        }else if(token.equals("-")){
            Integer pop = stack.pop();
            Integer pop1 = stack.pop();
            stack.add(pop1 -pop);
        }else if(token.equals("*")){
            Integer pop = stack.pop();
            Integer pop1 = stack.pop();
            stack.add(pop*pop1);
        }else if(token.equals("/")){
            Integer pop = stack.pop();
            Integer pop1 = stack.pop();
            stack.add(pop1/pop);
        }else{
            stack.add(Integer.valueOf(token));
        }
    }
    return stack.pop();
}

2、题目2:239-滑动窗口最大值

题目:leetcode.cn/problems/sl…

类型:困难

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

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

思路:

滑动窗口的移动过程,很像一个队列,队列里始终维护窗口里的值。

难点是如何求一个区间的最大值,暴力方法,遍历一遍的过程每次从窗口中再找到最大的数值,这样很明显是O(n*k)的算法。

而想到排序,可能会想到大顶堆来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了,但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了,所以不能用大顶堆。

这个队列应该有三个方法:

  • Pop
  • Push
  • getMax

每次窗口移动的时候,调用pop移除元素,push 放入元素,然后通过 getMax返回最大值。

然后再分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。

但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。

那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。

大家此时应该陷入深思.....

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

单调队列,即单调递减或单调递增的队列.

    public static int[] maxSlidingWindow(int[] nums, int k){
        if(nums.length ==1){
            return nums;
        }
        int len = nums.length -k+1;
        // 存放结果元素的数组,看要移动多少次
int[] res = new int[len];
        int num=0;
        MyQueue myQueue = new MyQueue();
        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<Integer>();

    // 弹出元素,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
 // 同时判断队列当前是否为空
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();
    }

}

3、题目3:前k个高频元素

题目:leetcode.cn/problems/to…

类型:中等

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

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

大/小顶堆的应用, 在C++中就是优先级队列,本题是 大数据中取前k值 的经典思路堆的数据结构来处理。

什么是堆呢?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

使用小顶堆,不断把最小值排出去。key 是元素,value 是频率。

本题我们就要使用优先级队列来对部分频率进行排序。

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

public int[] topKFrequent(int[] nums,int k){
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        map.put(num,map.getOrDefault(num,0) +1);
    }
    // 在优先队列中存储二元组(num,cnt),cnt 表示元素值 num 在数组中的出现次数
 // 出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(小顶堆)
 // 即使用第二个频率进行排序
PriorityQueue<int[]> queue = new PriorityQueue<>(((o1, o2) -> o1[1]-o2[1]));
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
        if(queue.size()<k){
            queue.add(new int[]{entry.getKey(),entry.getValue()});
        }else{
            //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
if(entry.getValue() > queue.peek()[1]){
                queue.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
queue.add(new int[]{entry.getKey(), entry.getValue()});
            }
        }
    }
    int[] res = new int[k];
    for (int i = k-1; i >=0 ; i--) {
        res[i] = queue.poll()[0];
    }
    return res;
}