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个高频元素,而不需要处理所有元素的频率。)