一、150.逆波兰表达式求值
本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题题目链接/文章讲解/视频讲解:代码随想录
1. 看完代码随想录的想法
(1)首先需要充分理解什么是逆波兰表达式,相当于树中的后缀表达式,与平时使用的中序表达式并不相同。
定义一个初始化的空栈,然后去遍历输入的逆波兰表达式,遇到数字就向栈中添加数字元素,遇到运算符就取出栈顶的两个数字进行运算,再存放进栈中。直到最后全部遍历完成,输出最后结果。
(2)首先,从operator引入add, sub, mul运算;然后由于题目中要求两个整数之间的除法总是向零截断,不包含除0运算,所以额外去定义一个除法函数,防止除数为0,处理异常值。
(3)其次,定义一个空栈stack,然后去遍历输入的字符串数组,如果字符不是加减乘除中任意一个,那么说明是数字,则用int强制将char转化为整数,然后添加进栈中;如果遍历到的字符是运算符,则依次取出两个,然后将运算后的数添加进栈中;
(4)全部遍历完成后返回栈顶元素。
from operator import mul, add, sub
def div(x, y):
if x * y > 0:
return int(x/y)
else:
return -(abs(x) // abs(y))
class Solution(object):
op_map = {'+': add, '-': sub, '*': mul, '/': div}
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for i in tokens:
if i not in {'+', '-', '*', '/'}:
stack.append(int(i))
else:
op2 = stack.pop()
op1 = stack.pop()
stack.append(self.op_map[i](op1,op2))
return stack.pop()
2. 实现过程中遇到的困难及解决
(1)如何判断循环到的元素是数字:如果不在运算符字典中,那么就是数字;或者使用isdigit()函数进行判断。
(2)如何弹出栈顶两个元素进行运算:直接pop()一次弹出,并且将运算后的数再写入栈中,便于下次运算;要注意运算符是调用字典,用key取value运算函数。
(3)如何进行异常处理:对分母为0以及负数进行特别处理,通过取整、取绝对值的方式。
(4)实现过程中debug:1)空格没对齐报错;2)op1、op2的顺序需要特别注意,op1是第二个取出的,op2是栈顶元素,并且要按照先后放入栈的顺序进行运算。
二、239.滑动窗口最大值
之前讲的都是栈的应用,这次该是队列的应用了。本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。
题目链接/文章讲解/视频讲解:代码随想录
1. 看完代码随想录的想法
(1)整体思路:按照给定k的大小去圈定滑动窗口,每次返回滑动窗口中的最大值,并将其添加进入结果集中;所以自定义一个队列,维护队列出口元素为滑动窗口最大值,只要新添加进来的元素比队列出口的元素大,那么把队列出口的元素全部弹出,然后将新元素添加进入队列入口,并且返回队列出口元素,将其放进结果集中,表示这是这一个滑动窗口的最大值。
(2)首先,自定义一个能完成上述操作的新类,然后最后在给定的主类中调用自定义类方法。
(3)自定义新类:首先引入单调队列,定义一个初始化队列方法;然后定义pop()函数,弹出之前必须判断队列是否为空,不为空才可以弹出,同时需要判断当前要弹出的数值是否是队列出口元素,必须在队列出口才可以弹出元素。
接下来定义push()函数,同样判断队列是否为空,并且如果需要添加的元素大于入口处的元素,那么就将比需添加元素的数值小的元素全部弹出,直到需要添加的数值小于等于队列入口元素,这样才能保持队列的元素单调从大到小。
最后定义获取滑动窗口最大值的函数front(),直接返回队列前端即可,因为该队列是由大到小排序的。
(4)主函数:首先定义一个新队列,调用自定义类方法,然后定义一个空数组,用于存放滑动窗口的最大元素。接下来,将给定的前k个元素调用push函数放进队列,最后调用front函数,将这组滑动窗口的最大值存放进入数组。然后继续向后遍历给定数组,从第k个一直遍历到数组末尾,然后调用pop函数移除最前面元素,在队列末尾添加元素,同时记录滑动窗口最大值。最后返回result。
from collections import deque
class MyQueue: #单调队列(从大到小
def __init__(self):
self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
#每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
#同时pop之前判断队列当前是否为空。
def pop(self, value):
if self.queue and value == self.queue[0]:
self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()
#如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
#这样就保持了队列里的数值是单调从大到小的了。
def push(self, value):
while self.queue and value > self.queue[-1]:
self.queue.pop()
self.queue.append(value)
#查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
def front(self):
return self.queue[0]
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
que = MyQueue()
result = []
for i in range(k): #先将前k的元素放进队列
que.push(nums[i])
result.append(que.front()) #result 记录前k的元素的最大值
for i in range(k, len(nums)):
que.pop(nums[i - k]) #滑动窗口移除最前面元素
que.push(nums[i]) #滑动窗口前加入最后面的元素
result.append(que.front()) #记录对应的最大值
return result
三、347.前 K 个高频元素
大/小顶堆的应用, 在C++中就是优先级队列本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。
题目链接/文章讲解/视频讲解:代码随想录
1. 看完代码随想录的想法
(1)整体思路:首先构建map,用于统计数组中每个元素出现的频率,key-value形式;因为需要统计的是高频元素,所以每次弹出的元素不可以是最大的,因此使用小顶堆。
(2)定义一个空字典defaultdict(int),用于统计输入的数组中每个元素及其出现的频率;
然后定义一个新字典,用于将上述字典的key和value交换,然后将这个字典的出现频率转换为数组形式,并对其进行排序,也就是将频率由小到大进行排序;
接下来定义一个result空数组用于存放结果集,以及一个计数器cnt,初始化为0;然后对输入的数组进行遍历,通过key[-1]找到字典中频率最高的元素,将其存入结果集,同时将计数器+1,同时删除较小元素,一直到满足前k个高频元素全部存入结果集。
最后返回结果集即可。
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
value_dict = defaultdict(int)
for i in nums:
value_dict[i] += 1
index_dict = defaultdict(list)
for key in value_dict:
index_dict[value_dict[key]].append(key)
key = list(index_dict.keys())
key.sort()
result = []
cnt = 0
while key and cnt != k:
result += index_dict[key[-1]]
cnt += len(index_dict[key[-1]])
key.pop()
return result
四、总结
在栈与队列系列中,强调栈与队列的基础,也是很多同学容易忽视的点。
使用抽象程度越高的语言,越容易忽视其底层实现,而C++相对来说是比较接近底层的语言。
用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。
接着,通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。
通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。