1. 用栈实现队列
题目:
思路:
python中使用列表来实现栈的操作
list.append(x) # 代表进栈
list.pop() # 代表出栈
用栈实现队列,需要采用俩个栈,一个输入栈一个输出栈
实现过程:
- 在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
- 最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
代码:
class MyQueue:
def __init__(self):
"""
in主要负责push,out主要负责pop
初始化俩个输入输出栈
"""
self.stack_in = []
self.stack_out = []
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_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()
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)
2. 有效的括号
题目:
思路:
括号匹配是使用栈解决的经典问题。
入栈技巧:在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了
经过分析,括号匹配问题,一共有三种不匹配的情况:
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
-
第三种情况,字符串里右方向的括号多余了,所以不匹配。
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
最后,字符串遍历完后,并且栈为空,就说明全部匹配完成了。
代码:
# 方法一,仅使用栈,更省空间
class Solution:
def isValid(self, s: str) -> bool:
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
3. 删除字符串中的所有相邻重复项
题目:
思路:
使用栈解决该问题。匹配问题都是栈的强项。
代码:
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
for i in s:
if not stack or i != stack[-1]:
stack.append(i)
else:
stack.pop()
return ''.join(stack)
4. 逆波兰表达式求值
题目:
思路:
用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
代码:
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for i in tokens:
if i != '+' and i != '-' and i != '*' and i != '/':
stack.append(int(i))
else:
num1 = stack.pop()
num2 = stack.pop()
if i == '+':
stack.append(num2 + num1)
elif i == '-':
stack.append(num2 - num1)
elif i == '*':
stack.append(num2 * num1)
elif i == '/':
stack.append(int(num2 / num1))
return stack.pop()
【注意】
# 注意俩者的区别
num1 = -6
num2 = 132
print(int(num1 / num2)) ---> 0
print(num1 // num2) ---> -1
5. 滑动窗口最大值
题目:
思路:
单调队列的经典题目。
单调队列
维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。Python中没有直接支持单调队列,需要我们自己来实现一个单调队列。
如何实现单调队列呢?
pop 和 push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
定义单调队列
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]
【注】
- 开头导入了Python标准库中的 deque 类,该类实现了双端队列数据结构,可以高效地从队列的两端执行添加和删除操作。
- 首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。
整体代码实现
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
时间复杂度: O(n)
空间复杂度: O(k)
6. 前K个高频元素
题目:
思路:
这道题目主要涉及到如下三块内容:
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
对频率进行排序,我们可以使用一种 容器适配器就是优先级队列。
什么是优先级队列?
优先级队列就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
如何排序?
可以使用堆对元素进行排序
什么是堆?
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
该使用大顶堆还是小顶堆呢?
我们要用小顶堆,因为要统计最大前k个元素,将小顶堆里保持k个最大元素,然后每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
代码实现
#时间复杂度:O(nlogk)
#空间复杂度:O(n)
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
#要统计元素出现频率
map_ = {} #nums[i]:对应出现的次数
for i in range(len(nums)):
map_[nums[i]] = map_.get(nums[i], 0) + 1
#对频率排序
#定义一个小顶堆,大小为k
pri_que = [] #小顶堆
#用固定大小为k的小顶堆,扫描所有频率的数值
for key, freq in map_.items():
heapq.heappush(pri_que, (freq, key))
if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
heapq.heappop(pri_que)
#找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
result = [0] * k
for i in range(k-1, -1, -1):
result[i] = heapq.heappop(pri_que)[1]
return result
【注】
-
heapq 是 Python 标准库中的一个模块,提供了堆队列算法的实现。堆(heap)是一种特殊的数据结构,它是一个数组,可以看作一个近似完全二叉树。在堆中,父节点的值总是小于或等于任何一个子节点的值。这种性质决定了堆的根节点是最小元素。heapq中默认是小顶堆。
-
heapq 模块提供了一些用于操作堆的函数,主要是将一个列表(或其他可迭代对象)转换为堆、进行堆的插入、删除等操作。这个模块的目标是提供高效的堆队列操作,通常用于优先级队列等场景。
-
heapq 模块中一些常用的函数:
heapq.heapify(iterable)
: 将可迭代对象转换为堆数据结构。heapq.heappush(heap, ele)
: 将元素推入堆中。heapq.heappop(heap)
: 弹出并返回堆中的最小元素。heapq.heapreplace(heap, ele)
: 弹出并返回堆中的最小元素,然后推入一个新元素。heapq.nlargest(n, iterable, key=None)
: 返回可迭代对象中最大的 n 个元素。heapq.nsmallest(n, iterable, key=None)
: 返回可迭代对象中最小的 n 个元素。