Bootstrap

24暑假算法刷题 | Day11 | LeetCode 150. 逆波兰表达式求值,239. 滑动窗口最大值,347. 前K个高频元素


150. 逆波兰表达式求值

点此跳转题目链接

题目描述

给你一个字符串数组 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 <= tokens.length <= 104
  • tokens[i] 是一个算符("+""-""*""/"),或是在范围 [-200, 200] 内的一个整数

题解

栈的经典题目 ✨

由于逆波兰表达式的特点,每个运算符总是要处理其前面的两个操作数。因此,可以采用栈存储表达式中的操作数,当遇到运算符时就弹出栈顶两个元素、进行运算,并把结果入栈。最后,这般处理完整个表达式,栈中剩余的唯一元素就是计算结果。

代码(C++)

int evalRPN(vector<string> &tokens)
{
    stack<int> numSt; // 存储数字的栈
    for (const string &token : tokens) { 
        if (token == "+") {
            int num1 = numSt.top();
            numSt.pop();
            int num2 = numSt.top();
            numSt.pop();
            numSt.push(num1 + num2);
        } else if (token == "-") {
            int num1 = numSt.top();
            numSt.pop();
            int num2 = numSt.top();
            numSt.pop();
            numSt.push(num2 - num1);
        } else if (token == "*") {
            int num1 = numSt.top();
            numSt.pop();
            int num2 = numSt.top();
            numSt.pop();
            numSt.push(num1 * num2);
        } else if (token == "/") {
            int num1 = numSt.top();
            numSt.pop();
            int num2 = numSt.top();
            numSt.pop();
            numSt.push(num2 / num1);
        } else 
            numSt.push(stoi(token)); // 数字无脑入栈即可
    }
    return numSt.top();
}

239. 滑动窗口最大值

点此跳转题目链接

题目描述

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

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

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

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

题解

LeetCode标记 困难 \textcolor{red}{\text{困难}} 困难 题目

题目不难理解,可以先无脑写出暴力解法:

vector<int> maxSlidingWindowViolence(vector<int> &nums, int k)
{
    vector<int> res;
    int left = 0, right = k - 1;
    while (right < nums.size())
    {
        int maxNum = nums[left];
        for (int i = left + 1; i <= right; i++)
            maxNum = max(maxNum, nums[i]);
        res.push_back(maxNum);
        left++, right++;
    }
    return res;
}

不出意外地,在数据量大时超时了 🙊

进一步研究不难发现,每次窗口滑动,其实只有最左最右的值改变了——这容易让人联想到队列数据结构(出队、入队只改变队头、队尾)。如果用队列存储一个窗口中的值,每次只需要

  • 将上一次的左边界值出队列
  • 将这一次的右边界入队列

但是有一个问题:如何维护窗口最大值?显然,普通的队列无法每次快速找出队中最大值。一个容易联想到的方法是用优先队列(如c++中的 priority_queue ),但是优先队列每次出队的都是最大值,而这个值(很)可能不是左边界值。

更进一步思考,可以发现其实左右边界也不总是需要考虑的——关键在于,维护“这个窗口的最大值”和“可能在下一个窗口中成为最大值的值”,每次取这些数中的最大值加入结果,并尝试把新的右边界值加入其中,这样需要维护的数就变少了不少。

所以针对这题,我们理想的数据结构是一种特殊的队列:

  • 按序存储(窗口中的)一部分数据,便于快速取出最大值
  • 每次滑动窗口、更新队列时,如果原来的最大值恰是原来的左边界值,需要将其出队

这样的数据结构(特殊的单调队列)没有现成的,需要手动搓出来,之后的代码就简单了:每次尝试出队、入队、取队头(最大值)加入结果即可。

代码

c++

// 用于解题的单调队列,原则:队列中元素单调递减
class MyQueue
{
private:
    deque<int> q; // 双端队列

public:
    void push(int num)
    {
        while (!q.empty() && q.back() < num)
            q.pop_back();
        q.push_back(num);
    }

    void pop() { q.pop_front(); }

    int front() { return q.front(); }
};

// 解题函数
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
    vector<int> res;
    MyQueue q;
    for (int i = 0; i < k; ++i) 
        q.push(nums[i]);
    res.push_back(q.front());
    int left = 1, right = k;
    while (right < nums.size()) {
        if (nums[left - 1] == q.front())
            q.pop();
        q.push(nums[right]);
        res.push_back(q.front());
        left++, right++;
    }
    return res;
}

go

type MyQueue struct {
	q []int
}

func (mq MyQueue) front() int {
	return mq.q[0]
}

func (mq MyQueue) back() int {
	return mq.q[len(mq.q) - 1]
}

func (mq *MyQueue) pop() {
	mq.q = mq.q[:len(mq.q)-1]
}

func (mq *MyQueue) push(num int) {
	for len(mq.q) != 0 && mq.back() < num {
		mq.pop()
	}
	mq.q = append(mq.q, num)
}

func (mq MyQueue) size() int {
	return len(mq.q)
}

func maxSlidingWindow(nums []int, k int) []int {
	res := []int{}
	mq := MyQueue{}
	left, right := 0, k-1
	for right < len(nums) {
		if mq.size() > 0 && nums[left] == mq.front() {
			mq.pop()
		}
		mq.push(nums[right])
		res = append(res, mq.front())
		left++
		right++
	}
	return res
}

该思路更详细的讲解参见 代码随想录——滑动窗口最大值


347. 前K个高频元素

点此跳转题目链接

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶: 你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

题解

将问题拆解,依次解决:

1️⃣ 记录数组中元素出现的频率:容易想到用哈希表实现,key:元素;value:出现次数。

2️⃣ 统计高频词汇:找到哈希表中,value最高的k个key,也就是要对整个哈希表按照value排序。比较方便的方法就是采用优先队列,将所有键值对按照value大小加入其中,队列最前面的k个元素即为所求。

代码(C++)

vector<int> topKFrequent(vector<int> &nums, int k)
{
    vector<int> res;
    auto cmp = [](pair<int, int> a, pair<int, int> b) { return a.second < b.second; };
    priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);
    unordered_map<int, int> freqMap;
    for (int num : nums)
        freqMap[num]++;
    for (auto pair : freqMap)
        pq.push(pair);
    for (int i = 0; i < k; i++) {
        res.push_back(pq.top().first);
        pq.pop();
    }
    return res;
}
;