Leetcode 20有效的括号,150逆波兰表达式,1047删除字符串中邻居元素的重复项 239滑动窗口的最大值
Leetcode 20
题目:
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
提示: - 1 <= s.length <= 1 0 4 10^4 104
- s 仅由括号 ‘()[]{}’ 组成
算法思想:
本题要考虑到三种情况:
- ( [ { } ] ( ) —— 找不到对应右括号
- [ { ( } } ] —— 左右括号对应不上
- { [ ( ) ] } ) ) ) ) —— 有括号多了
主要利用栈这种数据结构,当遇到左括号的时候,入栈一个对应的右括号;当遇到右括号的时候,出栈一个右括号与当前的右括号比较。
第1种情况,当遍历完整个字符串的时候,栈中仍然保存着“(”的对应结果,但此时已经扫描到字符串结尾。
第2种情况,当扫描到“}”的时候,此时栈顶元素为“)”,即“(”的对应结果。不匹配。
第3种情况,当栈中有括号都对应结束之后,字符串仍然没有扫描到结尾。
考虑到主要思想后,算法思想如下:
遍历字符串,遇到“( 或 { 或 [”,入栈对应的“) 或 } 或 ]”,每遇到一次右括号,便出栈一个元素,看是否匹配。
代码实现:
class Solution:
def isValid(self, s: str) -> bool:
'''
本题共计三种情况:
1.([{}]() # 左边括号多了
2.[{(}}] # 左右括号数量相等,但是左右括号不对齐
3.{[()]})))) # 前边左右括号对齐 ,但是右括号多了
'''
# 首先来个剪枝的条件
if len(s) % 2 != 0:
return False
stack = [] # 使用栈,省空间
for item in s:
if item == '(':
stack.append(")")
elif item == '[':
stack.append(']')
elif item == "{":
stack.append("}")
elif not stack or stack[-1] != item: # 对应第三种情况和第二种情况
return False
else:
stack.pop()
return True if not stack else False # 对应第一种情况
或 使用字典,代码如下:
class Solution:
def isValid(self, s: str) -> bool:
# 使用字典的方式解决该 问题
stack = []
if len(s) % 2 != 0:
return False
mapping = {
'(': ")",
'[': "]",
'{': "}"
}
for item in s:
if item in mapping.keys():
stack.append(mapping[item])
elif not stack or stack[-1] != item: # 处理后两种情况
return False
else: # 相等就出栈
stack.pop()
return True if not stack else False
Leetcode 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 <= 1 0 4 10^4 104
- tokens[i] 是一个算符(“+”、“-”、“*” 或 “/”),或是在范围 [-200, 200] 内的一个整数
算法思想:
正常我们习惯于
3
+
4
∗
2
+
3
3+4*2+3
3+4∗2+3这种形式的计算方式,但是这可能带来不正确的计算结果。想到得到正确的计算结果,必须加上括号
(
3
+
4
)
∗
(
2
+
3
)
(3+4)*(2+3)
(3+4)∗(2+3)。
什么是逆波兰表达式?
是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
- 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
我们将数字入栈,每遇到一个操作符,出栈两个元素,在将计算结果入栈,直到将最终的计算结果存到栈中。
代码实现:
from operator import add, sub, mul
class Solution:
def evalRPN(self, tokens) -> int:
op_map = {
"+": add,
"-": sub,
"*": mul,
"/": lambda x, y: int(x / y)
}
stack = []
for token in tokens:
if token not in {'+', "-", "*", "/"}:
stack.append(token)
else:
op2 = stack.pop()
op1 = stack.pop()
op2 = int(op2)
op1 = int(op1)
stack.append(op_map[token](op1, op2))
return int(stack.pop())
Leetcode 1047
题目:
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:“abbaca”
输出:“ca”
解释:
例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。
提示:
- 1 <= S.length <= 20000
- S 仅由小写英文字母组成。
算法思想:
利用栈这种数据结构,栈的作用是:保存遍历过且当前能保留下来的元素。
没扫描一个元素,与当前栈顶的元素比较,如果相同则出栈,否则入栈该元素。直到遍历结束,栈中的元素即位不重复元素。
代码实现:
class Solution:
def removeDuplicates(self, s: str) -> str:
# 使用栈模拟删除行为
# 栈的作用是:保存遍历过且当前能保留下来的元素
res = list()
for item in s:
if res and res[-1] == item:
res.pop()
else:
res.append(item)
return "".join(res)
或使用双指针,模拟栈的行为,代码如下:
# 方法二,使用双指针模拟栈,如果不让用栈可以作为备选方法。
class Solution:
def removeDuplicates(self, s: str) -> str:
res = list(s)
# 快指针遍历当前所有元素,慢指针记录当前需要保留的元素
slow = fast = 0
length = len(res)
while fast < length:
res[slow] = res[fast]
if slow > 0 and res[slow] == res[fast]:
slow = slow - 1
else:
slow = slow + 1
fast = fast + 1
return ''.join(res[0: slow])
Leetcode 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 <= 1 0 5 10^5 105
- -104 <= nums[i] <= 104 104 104
- 1 <= k <= nums.length
算法思想:
一种简单直观的想法是:每次入队一个元素,入队后取当前队列的最大值,直到遍历所有元素为止。但是取最大值的操作时间复杂度很高。那有没有一种简单的方法?
怎么获取最大值?那么队列中的元素一定是有序的,而且最大值一定放到出口,否则怎么获取最大值呢。问题有来了,队列中的元素都是有序元素了,该如何知道要删除的是哪个元素呢?
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。这个维护元素单调递减的队列就叫做单调队列。
对于push操作,每次出队比当前元素要小的所有元素,直到遇到大于等于自己的元素停止。对于pop操作,每次只出栈当前窗口中的最大值元素,也就是第一个元素,为什么要这样做?
如果窗口大小是3,当前队列中元素是3,2,1,如果第四个元素是0,该入队了,但是当前的窗口位3,该怎么办?因此这种操作的目的是:为了维护当前窗口的公平,否则局部最大值会占据着窗口。
代码实现:
'''
暴力解法:时间复杂度O(n*k)
那有没有更简单的方法实现?
'''
from collections import deque
class MyQueue: # 自定义单调队列
def __init__(self) -> None:
self.queue = deque()
def pop(self, value):
'''
这里每次弹出的元素是当前窗口中最大值
'''
if self.queue and value == self.queue[0]:
self.queue.popleft()
def push(self, value):
'''
如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止
'''
while self.queue and value > self.queue[-1]:
self.queue.pop()
self.queue.append(value)
def front(self):
'''
查询当前队列里的最大值,直接返回队列的最前端元素即可
'''
return self.queue[0]
class Solution:
def maxSlidingWindow(self, nums, k: int):
que = MyQueue()
result = []
for i in range(k):
que.push(nums[i])
result.append(que.front())
for i in range(k, len(nums)):
# 每加入一个新元素,都要先尝试pop一次, 例如 3,2, 1, 最后加入的数字是0
que.pop(nums[i-k])
que.push(nums[i])
result.append(que.front())
return result