算法学习 Day10 栈和队列2

77 阅读6分钟

150. 逆波兰表达式求值

文章讲解

视频讲解

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

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

注意:

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

示例 1:

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

解题思路

后缀表达式,使用栈。遇到数字入栈,遇到操作符出栈两个元素,然后计算结果后入栈。

注意:

  • 1 对于减法和除法,运算顺序别搞反了,栈顶第二个数是被除(减)数
  • 2 还要注意除法取整
  • 时间复杂度:O(n)
  • 空间复杂度:O(n) 最坏情况下栈中可能存储所有操作数。
class Solution:

    def evalRPN(self, tokens: List[str]) -> int:
        stk = []
        for i in range(len(tokens)):
            c = tokens[i]
            if c in "+-*/":
                num1 = stk.pop()
                num2 = stk.pop()
                num3 = eval(f"{num2} {c} {num1}")
                num3 = int(num3)
                stk.append(int(num3))
            else:
                stk.append(int(c))
        return stk.pop()
        

总结

这是一个典型的使用栈来求解逆波兰表达式的问题。关键点在于正确处理运算符的顺序, 以及整数除法的向零截断。

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]

解题思路

解决滑动窗口最大值问题的核心思路是使用单调队列。我们通过动态维护一个长度为 k 的窗口,使得队首元素始终为当前窗口的最大值。

需要注意的是,逐个比对每个窗口的最大值是不可取的,因为之前的最大值可能是窗口的第一个元素,而在下一个窗口中该元素可能已经被淘汰。

  • 时间复杂度:O(n)
  • 空间复杂度:O(k) 最多存k个元素。

class MonotonicQueue:
    ''' 动态维护一个队首最大的单调队列,单调递减 '''
    def __init__(self):
        self.queue = []

    def push(self, elem:int):
        ''' 插入元素,把比他小的元素都弹出 '''
        while self.queue and self.queue[-1] < elem:
            self.queue.pop()
        self.queue.append(elem)

    def pop(self, elem:int):
        ''' 删除值为elem的元素 '''
        if self.queue[0] == elem:
            self.queue.pop(0)

    def max(self):
        ''' 返回最大值 '''
        return self.queue[0]

class Solution:

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        window = MonotonicQueue()
        res = []
        for i in range(len(nums)):
            # 先把前k-1个加入队列
            if i < k -1:
                window.push(nums[i])
            else:
                window.push(nums[i])
                res.append(window.max())
                window.pop(nums[i-k+1])
        return res

总结

利用单调队列的性质,我们可以在 O(n) 的时间复杂度内高效地解决滑动窗口最大值问题。需要注意的是,在每次滑动窗口时,不能仅仅依赖于上一个窗口的最大值进行比较,因为上一个窗口的最大值可能是最早的元素,而该元素可能已经被当前窗口淘汰。因此,我们必须在每次更新窗口时,重新计算当前窗口的最大值,以确保得到正确的结果。

347. 前 K 个高频元素

文章讲解

视频讲解

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

示例 1:

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

示例 2:

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

解题思路1 哈希表+数组

  • 1 频率统计:使用哈希表统计每个元素的出现频率。
  • 2 建立频率数组:根据频率建立一个数组,索引代表频率,值为出现该频率的元素列表。
  • 3 逆序遍历:从高频到低频遍历频率数组,收集前 k 个高频元素。
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        ''' 统计频率然后返回topk频率的元素 '''
        # 1 频率统计
        num2cnt = {}
        for num in nums:
            num2cnt[num] = num2cnt.get(num, 0) + 1

        # 2 建立频率->整数的hash映射,由于频率是有限的[0,size],使用数组做hash
        size = len(nums)
        freq2num = [[] for _ in range(size+1)] # 注意:size+1,因为还有0 由于会出现相同频率,所以需要数组存储
        for num,freq in num2cnt.items():
            freq2num[freq].append(num)

        # 3 逆序遍历k个高频整数
        res = []
        for i in range(len(freq2num)-1,0,-1):
            for val in freq2num[i]:
                res.append(val)
                if len(res) == k:
                    return res
        return None

解题思路2 哈希表 + 最小堆(优先队列)

  • 1 频率统计:同样使用哈希表统计每个元素的出现频率。
  • 2 使用最小堆:维护一个大小为 k 的最小堆,存储频率最高的 k 个元素。
  • 3 提取结果:从堆中提取出元素,得到前 k 个高频元素。

时间复杂度:O(N log k)

  • 遍历数组 nums: O(N)
  • 建立小顶堆: O(N log k):
  • 取出 k 个元素: O(k)

将 N 个元素插入到大小为 k 的小顶堆中,每次插入一个元素,需要进行 log k 次比较和交换操作,才能维护堆的性质。 总的时间复杂度就是 N 次插入操作,每次需要 log k 的时间,因此是 O(N log k)。

空间复杂度:O(N),用于存储频率和元素的映射。

import heapq

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        ''' 统计频率然后返回topk频率的元素 '''
        # 1 频率统计
        val2freq = {}
        for num in nums:
            val2freq[num] = val2freq.get(num, 0) + 1

        # 2 建立优先队列(最小堆),存储topk大的频率->数值的映射
        pq = []
        for val, freq in val2freq.items():
            heapq.heappush(pq, [freq, val]) 
            # 弹出队首最小的元素,维护队列中有k个频率最大的元素
            if len(pq) > k:
                heapq.heappop(pq)

        # 3 保存topk的元素
        res = []
        while pq:
            res.append(heapq.heappop(pq)[1])

        return res[::-1]

总结

  • 哈希表 + 数组 方法在频率较高的情况下效率较高,适合处理频率分布较为均匀的情况。
  • 哈希表 + 最小堆 方法在处理大数据量时更为灵活,尤其是当 k 相对较小的情况下,能够有效减少内存使用。(对频率分布不均匀的情况可能更有效,因为我们只需要关注前 k 个高频元素,而不需要处理所有元素的频率。)