目录
栈与队列
理论基础
232.用栈实现队列
225. 用队列实现栈
20. 有效的括号
1047. 删除字符串中的所有相邻重复项
150. 逆波兰表达式求值
239.滑动窗口最大值
347.前 K 个高频元素
栈与队列
理论基础
队列是先进先出,栈是先进后出。
232.用栈实现队列
使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。
class MyQueue:
def __init__(self):
"""
in主要负责push,out主要负责pop
"""
self.stack_in = []
self.stack_out = []
# python的list像栈,先入后出。需要实现队列,先入先出。
# ✔
def push(self, x: int) -> None:
"""
有新元素进来,就往in里面push
"""
self.stack_in.append(x)
# ✔
def pop(self) -> int:
"""
Removes the element from in front of queue and returns that element.
"""
if self.empty():
return None
# if self.stack_in:
# for i in range(len(self.stack_in)):
# self.stack_out.append(self.stack_in.pop())
# return self.stack_out.pop()
# 以上会出错,例如:
# ["MyQueue","push","push","push","push","pop","push","pop","pop","pop","pop"]
# [[],[1],[2],[3],[4],[],[5],[],[],[],[]]
# 输出
# [null,null,null,null,null,1,null,5,2,3,4]
# 预期结果
# [null,null,null,null,null,1,null,2,3,4,5]
# push[5]时,基于先入先出的原则,[5]应该最后被pop出来。但按照上面的错误流程,在out已经有值的情况下,[5]还是会被从in转移到out然后被先pop出来。
# 正确应该是先把out里的全pop完然后再把[5]从in转移到out然后pop。
if self.stack_out:
return self.stack_out.pop()
else:
for i in range(len(self.stack_in)):
self.stack_out.append(self.stack_in.pop())
return self.stack_out.pop()
# 为什么上面有self.stack_out.pop(),下面有self.pop(),这两个有什么区别?
# 区别在于,self.stack_out.pop()调用的不是我们在这里定义的pop函数。它调用的是stack_out作为list类的库函数,
# 是list类固有的。每个list都有,随便新定义一个list,它也可以用.pop方法,返回的是list最末位的元素。
# 例如 定义list0 = [1,2,3,4],要求return list0.pop(), list0,返回值为[4,[1,2,3]]
# 下面的self.pop()是调用我们在这里定义的pop函数。
def peek(self) -> int:
"""
Get the front element.
"""
ans = self.pop()
self.stack_out.append(ans)
return ans
def empty(self) -> bool:
"""
只要in或者out有元素,说明队列不为空
"""
return not (self.stack_in or self.stack_out)
225. 用队列实现栈
使用队列实现栈的下列操作:
push(x) -- 元素 x 入栈
pop() -- 移除栈顶元素
top() -- 获取栈顶元素
empty() -- 返回栈是否为空
注意:
你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
class MyStack:
def __init__(self):
self.que = deque()
def push(self, x: int) -> None:
self.que.append(x)
def pop(self):
if self.empty():
return None
# for i in range(len(self.que) - 1):
# self.que.append(self.que.pop()) # 错误的。deque()的pop()是先入后出的pop
# # 例如[1,2,3],pop()返回的是3而不是想象中的1.
# # 所以上式直接是,pop3然后push3然后pop2然后push2,最后deque还是没变。。
# # 应该用popleft()实现队列的先入先出,[1,2,3]返回1.
for i in range(len(self.que) - 1):
self.que.append(self.que.popleft())
return self.que.popleft()
# 这里的操作比较单纯暴力的
def top(self) -> int:
if self.empty():
return None
return self.que[-1]
def empty(self) -> bool:
if self.que != deque():
return False
else:
return True
# 也可以用下面这个来判断,更简单直接
# return not self.que
20. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
括号匹配是使用栈解决的经典问题。
先来分析一下 这里有三种不匹配的情况,
- 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
- 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
- 第三种情况,字符串里右方向的括号多余了,所以不匹配。
class Solution:
def isValid(self, s: str) -> bool:
st = []
for i in range(len(s)):
if s[i] == '(':
st.append(')')
elif s[i] == '{':
st.append('}')
elif s[i] == '[':
st.append(']')
elif st == [] or s[i] != st[-1]:
return False
else:
st.pop()
return False if st else True
算法题做题前先想想能不能第一步先剪枝。降低计算量。
修改后在开头增加剪枝的两行,判断字符串长度是否为单数,若为单数则必不可能全成对。
if len(s) % 2 != 0:
return False
可改进:
for i in range(len(s)):换成 for item in s:
elif st == []换成 elif not st
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= S.length <= 20000
S 仅由小写英文字母组成。
本题是匹配相邻元素,最后都是做消除的操作。
本题也是用栈来解决的经典题目。
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
class Solution:
def removeDuplicates(self, s: str) -> str:
res = []
for item in s:
if not res or item != res[-1]:#为空或值不同则增
res.append(item)
else:#值同则消
res.pop()
return ''.join(res)
150. 逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
向零取整(向零截断) 取距离0最近的精确整数值。
示例 1:
输入: ["2", "1", "+", "3", " * "]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入: ["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 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
res = []
for item in tokens:
if item == '+' or item == '-' or item == '*' or item == '/':
nums1 = res.pop()
nums2 = res.pop()
if item == '+':
res.append(nums1 + nums2)
if item == '-':
res.append(nums2 - nums1)
# 注意不是nums1 - nums2 因为pop的顺序
if item == '*':
res.append(nums1 * nums2)
if item == '/':
# res.append(nums2 // nums1)
# 上式不对。例如6/(-132)结果本来应该是0但是这个除法(//)(也称地板除)会得到-1的结果。正确应该是:
res.append(int(nums2 / nums1))
else:
res.append(int(item))
return res.pop()
eval()是python中功能非常强大的一个函数
将字符串当成有效的表达式来求值,并返回计算结果
所谓表达式就是:eval这个函数会把里面的字符串参数的引号去掉,把中间的内容当成Python的代码,eval函数会执行这段代码并且返回执行结果
stack.append( int(eval(f'{second_num} {item} {first_num}')))
239.滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length
这是使用单调队列的经典题目。
难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。
暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。
分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
进2:2
进3:3
进5:5
进1:5 1
进4:5 4
后来的大的吃小的
后来的小的保留下
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移动的时候左侧pop的元素的value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作。此时单调队列继续往后增加值而不弹出值
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,并继续移动滑窗,若push进滑窗的是小值则保留在单调队列里。
# deque 是Python标准库 collections 中的一个类,实现了两端都可以操作的队列,相当于双端队列
# append(item),添加一个数据到队列的尾部。与列表的append()方法功能相似。
# appendleft(item),添加一个数据到队列的头部。与append()的添加方向相反。
# pop(),将队列尾部的数据弹出,并作为返回值。
# popleft(),将队列头部的数据弹出,并作为返回值。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# 视频里讲的是左边pop出,右边push进(但push进的时候涉及右边出和进 视频里用push_back和pop_back),左边大右边小
# 这里尝试右边pop出,左边push进(但涉及appendleft和popleft),左边小右边大
def pop(que, val): #(右出)
# 如果没有这个if条件会怎样。因为但凡是需要pop的好像都是pop最大值
# 回答:必须要有if。否则,比如-1 3 1 -3,滑动到3 1 -3的时候要pop出-1,但此时-1已经被3卷走了,所以滑动窗口里本来也没-1,因此不需要进行pop操作。
# if not que and que[-1] == val: 判断条件不是not que而是que,表示que不是空而是有值的时候
if que and que[-1] == val: # 此时pop的就是当前窗口最大值
que.pop()
return que
def push(que, val): # (左出左进)
while que and que[0] < val:
que.popleft()
que.appendleft(val)
return que
que = deque()
res = []
for i in range(len(nums)):
if i < k:
que = push(que, nums[i])
else:
res.append(que[-1])
# 注意append是用小括号()而不是中括号[]
que = push(que, nums[i])
que = pop(que, nums[i - k])
# 上两行交换会否有影响?不能交换,要先push再pop,否则可能push进的是当前窗口的最大值但由于pop先于push,导致pop的还是上一个窗口的最大值。
res.append(que[-1])
return res
# 下面是卡哥的
# 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 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
你可以按任意顺序返回答案。
这道题目主要涉及到如下三块内容:
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
首先统计元素出现的频率,这一类的问题可以使用map来进行统计。
然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素)
我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
# 以下为自己学完之后写的版本
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
map_ = {}
for item in nums:
map_[item] = map_.get(item, 0) + 1
# map_.get(item, 0)表示若有item则返回item否则返回0
pri_que = []
# for (key, freq) in map_: ✖
for (key, freq) in map_.items():
if len(pri_que) < k:
heapq.heappush(pri_que, (freq, key))
else:
heapq.heappush(pri_que, (freq, key))
heapq.heappop(pri_que)
# 必须先push再pop,否则不对。
return [i[1] for i in pri_que]
# # python内置模块 --- heapq:实现了一个小根堆。
# # 模块内常见的操作函数:
# # 1. heapify(x) : 将列表x转换为堆(小根堆)。
# # 2. heappush(heap,item): 将item压入堆中。(heap使存放堆的数组)
# # 3. heappop(heap): 从堆中弹出最小的项,并将其值返回。
# # heapq堆数据结构最重要的特征是heap[0]永远是最小的元素
# # ————————————————
# # heapq.heapify(list)
# # 将列表转换为最小堆 ,转换完后仍然是列表,所以heapify只是对列表中的元素进行了堆属性的排序
# # import heapq
# # res=[7,1,5,4,2,3]
# # print("列表",res,type(res)) #列表 [7, 1, 5, 4, 2, 3]
# # heapq.heapify(res)
# # print("最小堆",res,type(res)) #最小堆 [1, 2, 3, 4, 7, 5]
# # ————————————————
# # heapq.heappush(heap, item)
# # 将元素item压入heap堆(列表)中
# # import heapq
# # res=[7,1,5,6,2,3]
# # heapq.heapify(res)
# # heapq.heappush(res,8)
# # print("最小堆",res,type(res)) #最小堆 [1, 2, 3, 6, 7, 5, 8]
# # ————————————————
# # heapq.heappop(heap)
# # 删除并返回最小值,因为堆的特征是heap[0]永远是最小的元素,所以一般都是删除第一个元素。故,需要先通过heapify(list) 将list转变为heap。
# # import heapq
# # res=[7,1,5,6,2,3]
# # print("列表",res,type(res)) #列表 [7, 1, 5, 6, 2, 3]
# # heapq.heapify(res)
# # min_d=heapq.heappop(res)
# # print(min_d) #1
# # print("最小堆",res,type(res)) #最小堆 [2, 5, 3, 6, 7]