栈与队列
栈与队列是非常重要的基础数据结构,本文汇总了《代码随想录》和《Leetcode101》中关于栈与队列的练习题及其题解,旨在帮助读者更深入地理解相关概念和解题思路。如有疏漏或错误,恳请批评指正。
文章目录
-
- 栈与队列
-
- [1. 栈](#1. 栈)
-
- [[232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks)](#232. 用栈实现队列)
- [[225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues)](#225. 用队列实现栈)
- [[155. 最小栈](https://leetcode.cn/problems/min-stack/)](#155. 最小栈)
- [[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses)](#20. 有效的括号)
- [[1047. 删除字符串中的所有相邻重复项](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string)](#1047. 删除字符串中的所有相邻重复项)
- [[150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation)](#150. 逆波兰表达式求值)
- [[735. 小行星碰撞](https://leetcode.cn/problems/asteroid-collision/)](#735. 小行星碰撞)
- [[394. 字符串解码](https://leetcode.cn/problems/decode-string/)](#394. 字符串解码)
- [[224. 基本计算器](https://leetcode.cn/problems/basic-calculator/)](#224. 基本计算器)
- [[71. 简化路径](https://leetcode.cn/problems/simplify-path/)](#71. 简化路径)
- [2. 队列](#2. 队列)
-
- [[933. 最近的请求次数](https://leetcode.cn/problems/number-of-recent-calls/)](#933. 最近的请求次数)
- [[649. Dota2 参议院](https://leetcode.cn/problems/dota2-senate/)](#649. Dota2 参议院)
- [[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum)](#239. 滑动窗口最大值)
- [3. 优先队列](#3. 优先队列)
-
- [[347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements)](#347. 前 K 个高频元素)
- [[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/)](#23. 合并 K 个升序链表)
- [[218. 天际线问题](https://leetcode.cn/problems/the-skyline-problem)](#218. 天际线问题)
- [[313. 超级丑数](https://leetcode.cn/problems/super-ugly-number)](#313. 超级丑数)
- [[870. 优势洗牌](https://leetcode.cn/problems/advantage-shuffle)](#870. 优势洗牌)
- [4. 单调栈](#4. 单调栈)
-
- [[739. 每日温度](https://leetcode.cn/problems/daily-temperatures)](#739. 每日温度)
- [[496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i)](#496. 下一个更大元素 I)
- [[503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii)](#503. 下一个更大元素 II)
- [[901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/)](#901. 股票价格跨度)
- [[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water)](#42. 接雨水)
- [[84. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram)](#84. 柱状图中最大的矩形)
1. 栈
232. 用栈实现队列
问题描述:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你 只能 使用标准的栈操作 ------ 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
Example:
输入: ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 1, 1, false] 解释: MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false
思路:使用两个栈来模拟队列,如果是push操作,就将元素添加到第一个栈s1
,如果是pop或者top操作,如果第二个栈s2
不为空,则弹出/返回栈顶元素,否则将s1
中的元素依次移动到s2
中然后再弹出栈顶元素
python
class MyQueue:
def __init__(self):
self.s1 = deque()
self.s2 = deque()
def push(self, x: int) -> None:
self.s1.append(x)
def pop(self) -> int:
if self.s2:
return self.s2.pop()
while self.s1:
self.s2.append(self.s1.pop())
return self.s2.pop()
def peek(self) -> int:
top = self.pop()
self.s2.append(top)
return top
def empty(self) -> bool:
return len(self.s1) == 0 and len(self.s2) == 0
225. 用队列实现栈
问题描述:
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的标准操作 ------ 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
Example:
输入: ["MyStack", "push", "push", "top", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 2, 2, false] 解释: MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // 返回 2 myStack.pop(); // 返回 2 myStack.empty(); // 返回 False
思路:用两个队列模拟栈,如果是push操作,就将元素添加到第一个队列q1
,如果是pop或者top操作,就将第一个队列中的队尾元素前的所有元素push到第二个队列q2
中,然后弹出/返回q1
的队首元素,完成操作后,交换q1
和q2
的内容
python
class MyStack:
def __init__(self):
self.q1 = deque()
self.q2 = deque()
def push(self, x: int) -> None:
self.q1.append(x)
def pop(self) -> int:
while len(self.q1) > 1:
self.q2.append(self.q1.popleft())
top = self.q1.popleft()
self.q1, self.q2 = self.q2, self.q1
return top
def top(self) -> int:
while len(self.q1) > 1:
self.q2.append(self.q1.popleft())
top = self.q1.popleft()
self.q2.append(top)
self.q1, self.q2 = self.q2, self.q1
return top
def empty(self) -> bool:
return len(self.q1) == 0
优化,只使用一个队列
python
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x: int) -> None:
self.q.append(x)
def pop(self) -> int:
n = len(self.q)
for i in range(n - 1):
self.q.append(self.q.popleft())
top = self.q.popleft()
return top
def top(self) -> int:
n = len(self.q)
for i in range(n - 1):
self.q.append(self.q.popleft())
top = self.q.popleft()
self.q.append(top)
return top
def empty(self) -> bool:
return len(self.q) == 0
155. 最小栈
问题描述:
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
Example:
输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] 输出: [null,null,null,null,-3,null,0,-2] 解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.
思路一: 额外建立一个新栈,栈顶表示原栈里所有值的最小值。每当在原栈里插入一个数字时,若该数字小于等于新栈栈顶,则表示这个数字在原栈里是最小值,我们将其同时插入新栈内。每当从原栈里取出一个数字时,若该数字等于新栈栈顶,则表示这个数是原栈里的最小值之一,我们同时取出新栈栈顶的值
python
class MinStack:
def __init__(self):
self.s1 = deque()
self.s2 = deque()
def push(self, val: int) -> None:
self.s1.append(val)
if len(self.s2) == 0 or val < self.s2[-1]:
self.s2.append(val)
def pop(self) -> None:
top = self.s1.pop()
if top == self.s2[-1]:
self.s2.pop()
def top(self) -> int:
return self.s1[-1]
def getMin(self) -> int:
return self.s2[-1]
思路二:如果要求不能用额外的栈呢?我们可以用一个全局变量min保存当前栈中的最小值,并且此时栈中不再存放元素的值,而是存放当前元素与栈中最小值的差
- 如果遇到push操作,计算元素 v a l val val与最小值的差值 d i f f diff diff,如果 d i f f < 0 diff < 0 diff<0,说明该元素比栈中最小值更小,此时需要更新栈中的最小值 m i n = m i n − d i f f min = min -diff min=min−diff,并将差值压入栈中,如果 d i f f ≥ 0 diff \ge 0 diff≥0,说明该元素不是最小值,直接将差值压入栈中
- 如果遇到pop操作,首先判断栈顶元素 d i f f diff diff的符号,如果 d i f f < 0 diff < 0 diff<0,说明弹出的栈顶元素是当前的最小值,因此需要更新栈中的最小值 m i n = m i n − d i f f min = min - diff min=min−diff,如果 d i f f ≥ 0 diff \ge 0 diff≥0,则pop操作不会影响栈中的最小值
- 如果遇到top操作,判断栈顶元素 d i f f diff diff的符号,如果 d i f f < 0 diff < 0 diff<0,说明栈顶元素是当前的最小值,直接返回 m i n min min的值即可,如果 d i f f ≥ 0 diff \ge 0 diff≥0,我们可以通过该差值回复原始的元素值 m i n + d i f f min + diff min+diff
- 对于getMin操作,直接返回 m i n min min即可
python
class MinStack:
def __init__(self):
self.s = deque()
self.min = 0
def push(self, val: int) -> None:
if not self.s:
self.s.append(0)
self.min = val
else:
diff = val - self.min
self.s.append(diff)
if diff < 0:
self.min = val
def pop(self) -> None:
if self.s:
diff = self.s.pop()
if diff < 0:
self.min -= diff
def top(self) -> int:
if self.s[-1] > 0:
return self.min + self.s[-1]
else:
return self.min
def getMin(self) -> int:
return self.min
20. 有效的括号
问题描述:
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
Example:
**输入:**s = "()[]{}"
**输出:**true
思路:用一个栈来存放等待匹配的左括号,如果遍历到左括号,压入栈中;如果遇到右括号,判断是否能与栈顶元素匹配,如果可以则将栈顶元素弹出,否则返回False,当遍历完字符串后,如果栈为空则说明所有的左括号都能被匹配,返回True,否则返回False
python
class Solution:
def isValid(self, s: str) -> bool:
stack = deque()
for ss in s:
if ss in ['(', '[', '{']:
stack.append(ss)
else:
if len(stack) == 0:
return False
top = stack[-1]
if ss == ')' and top == '(':
stack.pop()
elif ss == ']' and top == '[':
stack.pop()
elif ss == '}' and top == '{':
stack.pop()
else:
return False
return len(stack) == 0
1047. 删除字符串中的所有相邻重复项
问题描述:
给出由小写字母组成的字符串 s
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 s
上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
Example:
输入:"abbaca" 输出:"ca" 解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
思路:用一个栈存放结果,如果当前字符和栈顶元素相同,则将栈顶元素弹出
python
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = deque()
for ss in s:
if len(stack) == 0:
stack.append(ss)
else:
top = stack[-1]
if top == ss:
stack.pop()
else:
stack.append(ss)
return ''.join(list(stack))
150. 逆波兰表达式求值
问题描述:
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
Example:
输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
思路:用一个栈来存放数值,当遇到数字时,压入栈中,当遇到+
、-
、*
、/
等符号时从数值栈中取出栈顶的两个元素,计算出结果后再压入栈中
python
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
num_stack = deque()
i = 0
for token in tokens:
if token not in ['+', '-', '*', '/']:
num_stack.append(int(token))
else:
b = num_stack.pop()
a = num_stack.pop()
if token == '+':
res = a + b
elif token == '-':
res = a - b
elif token == '*':
res = a * b
else:
res = int(a / b)
num_stack.append(res)
return num_stack[-1]
735. 小行星碰撞
问题描述:
给定一个整数数组 asteroids
,表示在同一行的小行星。
对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。
找出碰撞后剩下的所有小行星。碰撞规则:两个小行星相互碰撞,较小的小行星会爆炸。如果两颗小行星大小相同,则两颗小行星都会爆炸。两颗移动方向相同的小行星,永远不会发生碰撞。
Example:
输入:asteroids = [5,10,-5] 输出:[5,10] 解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
思路:如果栈为空,直接压入栈,否则判断栈顶元素 s t a c k [ − 1 ] stack[-1] stack[−1]和当前元素 c u r cur cur的正负号,如果 s t a c k [ − 1 ] > 0 stack[-1] > 0 stack[−1]>0并且 c u r < 0 cur < 0 cur<0,说明会发生碰撞,此时需要判断双方的大小关系
- a b s ( s t a c k [ − 1 ] ) > a b s ( c u r ) abs(stack[-1]) > abs(cur) abs(stack[−1])>abs(cur),当前的行星会爆炸
- a b s ( s t a c k [ − 1 ] ) = a b s ( c u r ) abs(stack[-1]) = abs(cur) abs(stack[−1])=abs(cur),两个行星都会爆炸
- a b s ( s t a c k [ − 1 ] ) < a b s ( c u r ) abs(stack[-1]) < abs(cur) abs(stack[−1])<abs(cur),前一颗行星会爆炸
这里需要用一个while循环来持续监测当前行星是否会和前一颗行星爆炸,如果前一颗行星爆炸,则当前行星继续和更前方的行星进行碰撞,直到不会发生碰撞为止
python
class Solution:
def asteroidCollision(self, asteroids: List[int]) -> List[int]:
n = len(asteroids)
stack = deque()
for i in range(n):
if not stack:
stack.append(asteroids[i])
continue
cur = asteroids[i]
while stack and stack[-1] > 0 and cur < 0:
if abs(stack[-1]) == abs(cur):
cur = 0
elif abs(stack[-1]) > abs(cur):
cur = stack[-1]
else:
cur = cur
stack.pop()
if cur != 0:
stack.append(cur)
return list(stack)
394. 字符串解码
问题描述:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
Example:
输入:s = "3[a]2[bc]" 输出:"aaabcbc" 输入:s = "3[a2[c]]" 输出:"accaccacc"
思路一:栈中存放正在解码的子字符串,并用一个flag变量记录k[]
的嵌套层数,如果遇到[
,直接入栈并将flag加一;如果遇到数字,则解析数字并将其入栈;如果遇到]
,则逐个弹出栈中的元素直到遇到前一个[
,将这些元素拼接起来构成子串,然后再从栈中取出重复的次数 k k k并将子串重复 k k k次,如果此时栈为空,则将重复后的子串拼接到结果中,否则将其入栈,然后将flag减一;如果遇到字母,当flag为0时直接拼接到结果中,否则将其入栈
python
class Solution:
def decodeString(self, s: str) -> str:
n = len(s)
stack = deque()
ans = ""
flag = 0
i = 0
while i < n:
if s[i] == ']':
sub_s = ''
while stack and stack[-1] != '[':
sub_s = stack.pop() + sub_s
stack.pop()
num = stack.pop()
sub_s = sub_s * num
if stack:
stack.append(sub_s)
else:
ans += sub_s
flag -=1
elif s[i] == '[':
stack.append(s[i])
elif s[i].isdigit():
num = 0
while s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
stack.append(num)
flag += 1
continue
else:
if flag == 0:
ans += s[i]
else:
stack.append(s[i])
i += 1
return ans
思路二:栈中同时存放解码后和正在解码的子串,无需flag变脸来记录嵌套层数,遇到 [ [ [或者字母时,直接入栈,遇到数字时,解析数字后再入栈,遇到 ] ] ]时,则逐个弹出栈中的元素直到遇到前一个[
,将这些元素拼接起来构成子串,然后再从栈中取出重复的次数 k k k,并将子串重复 k k k次后再加入栈中,当遍历完字符串后,将战中的子串拼接起来即可
python
class Solution:
def decodeString(self, s: str) -> str:
n = len(s)
stack = deque()
i = 0
while i < n:
if s[i].isdigit():
num = 0
while i < n and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
stack.append(num)
continue
elif ord('a') <= ord(s[i]) <= ord('z'):
stack.append(s[i])
elif s[i] == '[':
stack.append(s[i])
elif s[i] == ']':
ss = ""
while stack and stack[-1] != '[':
ss = stack.pop() + ss
stack.pop()
ss = ss * stack.pop()
stack.append(ss)
i += 1
return ''.join(stack)
224. 基本计算器
问题描述:
给你一个字符串表达式 s
,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval()
。
Example:
输入:s = "(1+(4+5+2)-3)+(6+8)" 输出:23
思路一:括号展开
python
class Solution:
def calculate(self, s: str) -> int:
n = len(s)
sign = 1
stack = deque([1])
res = 0
i = 0
while i < n:
if s[i] == ' ':
i += 1
elif s[i] == '+':
sign = stack[-1]
i += 1
elif s[i] == '-':
sign = -stack[-1]
i += 1
elif s[i] == '(':
stack.append(sign)
i += 1
elif s[i] == ')':
stack.pop()
i += 1
else:
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
res = res + sign * num
return res
思路二:中缀表达式转后缀表达式,需要对前导符号进行额外的处理
python
class Solution:
def calculate(self, s: str) -> int:
s = s.replace(' ', '')
n = len(s)
rpn = []
stack = deque()
priority = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
'@': 3
}
i = 0
while i < n:
if s[i].isdigit():
num = 0
while i < n and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
rpn.append(str(num))
continue
elif s[i] == '(':
stack.append(s[i])
elif s[i] == ')':
while stack and stack[-1] != '(':
rpn.append(stack.pop())
stack.pop()
else:
if s[i] == '-':
if i == 0 or s[i - 1] == '(':
stack.append('@')
i += 1
continue
while stack and stack[-1] != '(' and priority[stack[-1]] >= priority[s[i]]:
rpn.append(stack.pop())
stack.append(s[i])
i += 1
while stack:
rpn.append(stack.pop())
return self.calc(rpn)
def calc(self, tokens):
n = len(tokens)
ans = 0
stack = deque()
i = 0
while i < n:
if tokens[i] not in ['+', '-', '*', '/', '@']:
stack.append(int(tokens[i]))
elif tokens[i] == '@':
stack.append(-stack.pop())
else:
b = stack.pop()
a = stack.pop()
if tokens[i] == '+':
res = a + b
elif tokens[i] == '-':
res = a - b
elif tokens[i] == '*':
res = a * b
else:
res = int(a / b)
stack.append(res)
i += 1
return stack[-1]
简洁版
python
class Solution:
def calculate(self, s: str) -> int:
s = s.replace(' ', '').replace('(+', '(0+').replace('(-', '(0-')
n = len(s)
priority = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
'^': 3
}
num_stack = deque([0])
op_stack = deque()
i = 0
while i < n:
if s[i].isdigit():
num = 0
while i < n and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
num_stack.append(num)
continue
elif s[i] == '(':
op_stack.append(s[i])
elif s[i] == ')':
while op_stack and op_stack[-1] != '(':
self.calc(num_stack, op_stack)
op_stack.pop()
else:
while op_stack and op_stack[-1] != '(' and priority[op_stack[-1]] >= priority[s[i]]:
self.calc(num_stack, op_stack)
op_stack.append(s[i])
i += 1
while op_stack:
self.calc(num_stack, op_stack)
return num_stack[-1]
def calc(self, num_stack, op_stack):
b = num_stack.pop()
a = num_stack.pop()
op = op_stack.pop()
if op == '+':
res = a + b
elif op == '-':
res = a - b
elif op == '*':
res = a * b
elif op == '/':
res = a / b
elif op == '^':
res = a ** b
num_stack.append(int(res))
return num_stack[-1]
71. 简化路径
问题描述:
给你一个字符串 path
,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/'
开头),请你将其转化为 更加简洁的规范路径。
在 Unix 风格的文件系统中规则如下:
- 一个点
'.'
表示当前目录本身。 - 此外,两个点
'..'
表示将目录切换到上一级(指向父目录)。 - 任意多个连续的斜杠(即,
'//'
或'///'
)都被视为单个斜杠'/'
。 - 任何其他格式的点(例如,
'...'
或'....'
)均被视为有效的文件/目录名称。
返回的 简化路径 必须遵循下述格式:
- 始终以斜杠
'/'
开头。 - 两个目录名之间必须只有一个斜杠
'/'
。 - 最后一个目录名(如果存在)不能 以
'/'
结尾。 - 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含
'.'
或'..'
)。
返回简化后得到的 规范路径 。
Example:
**输入:**path = "/.../a/.../b/c/.../d/./"
输出:"/.../b/d"
思路:
python
class Solution:
def simplifyPath(self, path: str) -> str:
n = len(path)
stack = deque()
names = path.split("/")
for name in names:
if name == '' or name == '.':
continue
elif name == '..':
if stack:
stack.pop()
else:
stack.append(name)
return '/' + '/'.join(stack)
2. 队列
933. 最近的请求次数
问题描述:
写一个 RecentCounter
类来计算特定时间范围内最近的请求。
请你实现 RecentCounter
类:
RecentCounter()
初始化计数器,请求数为 0 。int ping(int t)
在时间t
添加一个新请求,其中t
表示以毫秒为单位的某个时间,并返回过去3000
毫秒内发生的所有请求数(包括新请求)。确切地说,返回在[t-3000, t]
内发生的请求数。
保证 每次对 ping
的调用都使用比之前更大的 t
值。
Example:
输入: ["RecentCounter", "ping", "ping", "ping", "ping"] [[], [1], [100], [3001], [3002]] 输出: [null, 1, 2, 3, 3] 解释: RecentCounter recentCounter = new RecentCounter(); recentCounter.ping(1); // requests = [1],范围是 [-2999,1],返回 1 recentCounter.ping(100); // requests = [1, 100],范围是 [-2900,100],返回 2 recentCounter.ping(3001); // requests = [1, 100, 3001],范围是 [1,3001],返回 3 recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
思路:队列的特点是先进先出,所以当一个新的请求到来时,最先过期的是队首元素,所以我们只需要从队首元素开始遍历,只要请求时间小于 t − 3000 t-3000 t−3000,我们就将其从队列中删除,这样队列中剩下的元素个数就是 [ t − 3000 , t ] [t-3000, t] [t−3000,t]内发生的请求数
python
class RecentCounter:
def __init__(self):
self.q = deque()
def ping(self, t: int) -> int:
self.q.append(t)
while self.q and self.q[0] < t - 3000:
self.q.popleft()
return len(self.q)
649. Dota2 参议院
问题描述:
Dota2 的世界里有两个阵营:Radiant
(天辉)和 Dire
(夜魇)
Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的 一 项:
- 禁止一名参议员的权利 :参议员可以让另一位参议员在这一轮和随后的几轮中丧失 所有的权利 。
- 宣布胜利 :如果参议员发现有权利投票的参议员都是 同一个阵营的 ,他可以宣布胜利并决定在游戏中的有关变化。
给你一个字符串 senate
代表每个参议员的阵营。字母 'R'
和 'D'
分别代表了 Radiant
(天辉)和 Dire
(夜魇)。然后,如果有 n
个参议员,给定字符串的大小将是 n
。
以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。所有失去权利的参议员将在过程中被跳过。
假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。输出应该是 "Radiant"
或 "Dire"
。
Example:
输入:senate = "RDD" 输出:"Dire" 解释: 第 1 轮时,第一个来自 Radiant 阵营的参议员可以使用第一项权利禁止第二个参议员的权利。 这一轮中,第二个来自 Dire 阵营的参议员会将被跳过,因为他的权利被禁止了。 这一轮中,第三个来自 Dire 阵营的参议员可以使用他的第一项权利禁止第一个参议员的权利。 因此在第二轮只剩下第三个参议员拥有投票的权利,于是他可以宣布胜利
思路:用两个队列q1
和q2
分别存放Radiant阵营和Dire阵营的参议员的位置(即先后顺序),同时遍历两个队列,如果 q 1 [ 0 ] < q 2 [ 0 ] q1[0]<q2[0] q1[0]<q2[0],说明Radiant阵营的参议员在Dire阵营的参议员的前面,此时最好的策略是禁止Dire参议员的权利,也就是直接从队列中移除,而Radiant阵营的参议员本轮已经没有权利需要进入下一轮投票,所以我们将其添加到队列尾,同时将其位置加 n n n( n n n是两个队列的长度和),这样就可以保证本轮中位置靠后的Dire议员可以禁止本轮已经行使过权利的Radiant议员; q 1 [ 0 ] > q 2 [ 0 ] q1[0]>q2[0] q1[0]>q2[0]的情况同理,如此循环下去直到某一个队列为空,这样另一个非空队列所代表的阵营就可以宣布胜利
python
class Solution:
def predictPartyVictory(self, senate: str) -> str:
q1, q2 = deque(), deque()
for i in range(len(senate)):
if senate[i] == 'R':
q1.append(i)
else:
q2.append(i)
n = len(q1) + len(q2)
while q1 and q2:
top1 = q1.popleft()
top2 = q2.popleft()
if top1 < top2:
q1.append(top1 + n)
else:
q2.append(top2 + n)
if q1:
return 'Radiant'
if q2:
return 'Dire'
239. 滑动窗口最大值
问题描述:
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
Example:
输入: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
思路:滑动窗口本质上可以看作是一个队列,元素是先进先出的顺序,因此滑动窗口的最大值就是求当前队列的最大值,看到这个问题很容易联想到155. 最小栈,我们是否也可以额外维护一个队列来存放最大值呢?实际上不太好实现,因为栈的插入和删除都是对同一端进行操作,当插入元素时,我们可以对比栈中的最小值和插入元素来获取插入后的最小值,当删除元素时,直接从栈中弹出就可以了,因为元素 s t a c k [ i ] stack[i] stack[i]的删除不会对最小栈 [ 0 , i − 1 ] [0,i-1] [0,i−1]范围中的最小值产生影响。但是队列就不可以了,由于队列的插入和删除都是分别对右端和左端进行操作,假如用一个最小队列来存放原始队列 [ 0 , i ] [0,i] [0,i]范围内的最小值,那么删除元素时可能会对最小队列 [ 1 , i ] [1,i] [1,i]范围中的最小值产生影响,所以我们需要换一种思路
既然队列的特点是先入先出,那么当窗口向右移动时,我们将窗口左端的值从队列左端删除,同时从队尾开始将队列中所有小于窗口右端的元素弹出,这样队列的最左端就永远是当前窗口内的最大值 ,这道题也是单调栈的一种延申:该双端队列利用从左到右递减来维持大小关系
python
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
q = deque()
ans = []
for i in range(n):
if i < k:
while q and nums[i] > q[-1]:
q.pop()
q.append(nums[i])
else:
ans.append(q[0])
# 弹出逻辑
if nums[i - k] == q[0]:
q.popleft()
# 放入逻辑
while q and nums[i] > q[-1]:
q.pop()
q.append(nums[i])
ans.append(q[0])
return ans
3. 优先队列
优先队列的实现
python
class MinHeap:
def __init__(self):
self.heap = []
def swim(self, pos):
parent = (pos - 1) >> 1
while parent >= 0 and self.heap[pos] < self.heap[parent]:
self.heap[parent], self.heap[pos] = self.heap[pos], self.heap[parent]
pos = (pos - 1) >> 1
parent = (parent - 1) >> 1
def sink(self, pos):
n = len(self.heap)
while 2 * pos + 1 < n:
child = 2 * pos + 1
if child + 1 < n and self.heap[child] > self.heap[child + 1]:
child += 1
if self.heap[pos] <= self.heap[child]:
break
self.heap[pos], self.heap[child] = self.heap[child], self.heap[pos]
pos = child
def push(self, val):
n = len(self.heap)
self.heap.append(val)
self.swim(n)
def pop(self):
n = len(self.heap)
top = self.heap[0]
self.heap[0] = self.heap[n - 1]
self.heap.pop()
self.sink(0)
return top
def top(self):
return self.heap[0]
def __len__(self):
return len(self.heap)
def heap_sort(nums):
heap = MinHeap()
for num in nums:
heap.push(num)
ans = []
while len(heap) > 0:
ans.append(heap.pop())
return ans
347. 前 K 个高频元素
问题描述:
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
Example:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
思路:首先遍历整个数组,并使用哈希表记录每个数字出现的次数,并形成一个「出现次数数组」。找出原数组的前 k k k个高频元素,就相当于找出「出现次数数组」的前 k k k大的值
最简单的做法是给「出现次数数组」排序。但由于可能有 O ( N ) O(N) O(N)个不同的出现次数(其中 N N N为原数组长度),故总的算法复杂度会达到 O ( N l o g N ) O(NlogN) O(NlogN),不满足题目的要求
在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:
- 如果堆的元素个数小于 k k k,就可以直接插入堆中
- 如果堆的元素个数等于 k k k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k k k个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中
遍历完成后,堆中的元素就代表了「出现次数数组」中前 k k k大的值
python
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
heap = []
freq = Counter(nums)
for key, value in freq.items():
if len(heap) == k:
if value > heap[0][0]:
heapq.heapreplace(heap, [value, key])
else:
heapq.heappush(heap, [value, key])
ans = []
for i in range(k):
ans.append(heapq.heappop(heap)[1])
return ans
23. 合并 K 个升序链表
问题描述:
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
Example:
输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
思路一:从数组中取出两个链表,然后将其合并为一个新的升序链表然后放入数组中,重复此过程直到数组中只剩下一个链表,该链表就是所求的合并链表
python
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
if len(lists) == 1:
return lists[0]
r = None
while len(lists) > 1:
p = lists.pop()
q = lists.pop()
r = self.merge(p, q)
lists.append(r)
return r
def merge(self, p, q):
dummy = ListNode()
r = dummy
while p and q:
if p.val < q.val:
r.next = ListNode(p.val)
p = p.next
else:
r.next = ListNode(q.val)
q = q.next
r = r.next
if p:
r.next = p
if q:
r.next = q
return dummy.next
思路二:用小根堆存放数组中所有链表的头节点,每次从小根堆中取出值最小的头节点,加入合并链表中,如果头节点的下一个节点不为空,就然后将下一个节点重新放入小根堆中,重复此过程直到堆为空
python
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
dummy = ListNode()
p = dummy
heap = []
for h in lists:
if h:
heapq.heappush(heap, [h.val, id(h), h])
while heap:
v, _, h = heapq.heappop(heap)
p.next = h
p = p.next
if h.next:
heapq.heappush(heap, [h.next.val, id(h.next),h.next])
return dummy.next
218. 天际线问题
问题描述:
城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings
表示,其中三元组 buildings[i] = [lefti, righti, heighti]
表示:
lefti
是第i
座建筑物左边缘的x
坐标。righti
是第i
座建筑物右边缘的x
坐标。heighti
是第i
座建筑物的高度。
你可以假设所有的建筑都是完美的长方形,在高度为 0
的绝对平坦的表面上。
天际线 应该表示为由 "关键点" 组成的列表,格式 [[x1,y1],[x2,y2],...]
,并按 x 坐标 进行 排序 。关键点是水平线段的左端点 。列表中最后一个点是最右侧建筑物的终点,y
坐标始终为 0
,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
**注意:**输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...]
是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]
Example:
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]] 输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]] 解释: 图 A 显示输入的所有建筑物的位置和高度, 图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
思路:翻译一下题目,假设你处于一个虚拟世界,buildings
中包含了每个种族的[出现时间, 灭绝时间, 强度]
,在同一时间中只有强度最高的种族才能够统治世界,我们要做的就是当某个种族出现或者灭亡时,判断当前统治世界的种族,如果统治世界的种族发生了变化,我们就将新的种族放入到结果集中,这里我们默认一直存在一个强度为0的种族,当其他种族都灭绝时,由它来进行统治世界(对应于两个相邻建筑物之间的地面)
由于统治世界的种族发生变化的时机出现在某个种族的出现时间或灭绝时间,因此我们用一个数组来存放每个种族的出现/灭亡时间和对应的强度,为了方便排序,对于出现时间,令强度为负;对于灭绝时间,令强度为正,然后我们对数组按升序排序,这样我们就可以按照时间顺序遍历所有种族的出现和灭绝时间
如果我们遇到某个种族的出现时间,我们将其强度加入大根堆,否则我们需要将当前种族的强度从大根堆中删除。然后我们获取大根堆中的堆顶元素,该元素代表了当前统治世界的种族强度,如果结果集为空或者该强度和结果集最后一个种族的强度不同,我们就将[当前时间, 强度]
加入结果集中,表示新的种族开始统治世界,当遍历完数组后,结果集中存放的就是所求的答案
这里有个问题是如果我们需要将当前种族的强度从大根堆中删除,需要先查找然后再删除,时间复杂度较高,我们可以用一个哈希表记录强度所对应的删除次数,如果某个种族灭绝了,我们就将该种族对应的强度的删除次数加一,这样再取堆顶元素之前,我们可以判断当前堆顶元素的删除次数是否大于等于1,如果是,我们就要将堆顶元素弹出,然后将哈希表中对应的删除次数减一,重复这个过程,直到堆顶元素的删除次数为0
python
class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
n = len(buildings)
ans = []
points = []
for building in buildings:
points.append([building[0], -building[2]])
points.append([building[1], building[2]])
points = sorted(points, key=lambda x: (x[0], x[1]))
_hash = {}
max_heap = [0]
for i in range(len(points)):
point, height = points[i]
# 如果是左端点 将新的种族加入堆
if height < 0:
heapq.heappush(max_heap, height)
# 如果是右端点 删除堆中结束统治的种族
else:
if height not in _hash:
_hash[height] = 1
else:
_hash[height] += 1
while max_heap:
top = -max_heap[0]
if top in _hash:
if _hash[top] == 1:
del _hash[top]
else:
_hash[top] -= 1
heapq.heappop(max_heap)
else:
break
# 获取堆顶元素 即当前处于统治的种族
h = -max_heap[0]
if not ans or ans[-1][1] != h:
ans.append([points[i][0], h])
return ans
313. 超级丑数
问题描述:
超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes
中。
给你一个整数 n
和一个整数数组 primes
,返回第 n
个 超级丑数 。
题目数据保证第 n
个 超级丑数 在 32-bit 带符号整数范围内。
Example:
输入:n = 12, primes = [2,7,13,19] 输出:32 解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。
思路:我们可以用一个优先队列存放当前已经求出的超级丑数(初始化为[1]),然后每轮循环从队头取出一个元素 h e a p [ 0 ] heap[0] heap[0],用质数数组中的元素 p r i m e s [ i ] primes[i] primes[i]与其组合成 p r i m e s [ i ] ∗ h e a p [ 0 ] primes[i]*heap[0] primes[i]∗heap[0]并加入队列,直到队头元素 h e a p [ 0 ] heap[0] heap[0]可以被当前的质数 p r i m e s [ i ] primes[i] primes[i]整除,这样循环 n − 1 n-1 n−1次后拿到的的队头元素就是第 n n n个超级丑数
为什么 h e a p [ 0 ] % p r i m e s [ i ] = = 0 heap[0]\ \% \ primes[i] == 0 heap[0] % primes[i]==0的时候停止当前轮的循环呢?因为继续循环下去可能导致同一丑数多次进队!
假设 h e a p [ 0 ] = p r i m e s [ i ] ∗ a heap[0] = primes[i] * a heap[0]=primes[i]∗a,由于超级丑数 h e a p [ 0 ] heap[0] heap[0]的质因数都出现在 p r i m e s primes primes中,所以 a ∈ p r i m e s a \in primes a∈primes,而下一个质数 p r i m e s [ i + 1 ] primes[i + 1] primes[i+1]和 h e a p [ 0 ] heap[0] heap[0]进行组合的数就可以展开为$primes[i + 1] * primes[i] * a $ ,这说明 p r i m e s [ i + 1 ] primes[i + 1] primes[i+1]和 h e a p [ 0 ] heap[0] heap[0]进行组合的数也可以用 p r i m e s [ i ] primes[i] primes[i]和 p r i m e s [ i + 1 ] ∗ a primes[i+1] * a primes[i+1]∗a进行组合得到,又因为 a a a在 p r i m e s primes primes中,所以 a a a必定是超级丑数,所以 p r i m e s [ i + 1 ] primes[i+1] primes[i+1]是有可能和 a a a组合构成 p r i m e s [ i + 1 ] ∗ a primes[i+1] * a primes[i+1]∗a的,因此可能会出现重复的超级丑数
python
class Solution:
def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int:
heap = [1]
for i in range(n - 1):
top = heap[0]
heapq.heappop(heap)
for prime in primes:
heapq.heappush(heap, prime * top)
if top % prime == 0:
break
return heap[0]
870. 优势洗牌
问题描述:
给定两个长度相等的数组 nums1
和 nums2
,nums1
相对于 nums2
的优势 可以用满足 nums1[i] > nums2[i]
的索引 i
的数目来描述。
返回 nums1
的任意 排列,使其相对于 nums2
的优势最大化。
Example:
输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11] 输出:[2,11,7,15]
思路一:
python
class Solution:
def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
n = len(nums1)
lmt = 1e9
heap1, heap2 = [], []
for i in range(n):
heapq.heappush(heap1, nums1[i])
heapq.heappush(heap2, [nums2[i], i])
ans = [0] * n
while heap1 and heap2:
top1 = heap1[0]
top2 = heap2[0]
if top1 > top2[0] or top1 >= lmt:
if top1 >= lmt:
top1 -= lmt
ans[top2[1]] = top1
heapq.heappop(heap1)
heapq.heappop(heap2)
else:
heapq.heappop(heap1)
heapq.heappush(heap1, top1 + lmt)
return ans
思路二:贪心
python
class Solution:
def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
n = len(nums1)
p = deque(sorted(nums1, reverse=True))
q = sorted(range(n), key=lambda x: nums2[x], reverse=True)
ans = [0] * n
for i in range(n):
if p[0] > nums2[q[i]]:
ans[q[i]] = p.popleft()
else:
ans[q[i]] = p.pop()
return ans
4. 单调栈
单调栈通过维持栈内值的单调递增(递减)性,在整体 O ( n ) O(n) O(n) 的时间内处理需要大小比较的问题
遇到求下一个最小/最大元素的问题时要第一时间想到单调栈!
739. 每日温度
问题描述:
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
Example:
输入: temperatures = [73,74,75,71,69,72,76,73] 输出: [1,1,4,2,1,1,0,0]
思路:单调栈的经典题目,维持栈内的单调递减性质,如果当前元素小于栈顶元素,则不改变栈内的单调递减性,直接将当前元素加入栈,如果当前元素大于栈顶元素,说明出现了更高的温度,将栈顶元素弹出后继续用新的栈顶元素与当前元素比较,直到栈为空或者当前元素不大于栈顶元素,此时再将当前元素加入栈
python
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
ans = [0] * n
stack = deque()
stack.append(0)
for i in range(1, n):
while stack and temperatures[stack[-1]] < temperatures[i]:
ans[stack[-1]] = i - stack[-1]
stack.pop()
stack.append(i)
return ans
496. 下一个更大元素 I
问题描述:
nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
大的元素。
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。
对于每个 0 <= i < nums1.length
,找出满足 nums1[i] == nums2[j]
的下标 j
,并且在 nums2
确定 nums2[j]
的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1
。
返回一个长度为 nums1.length
的数组 ans
作为答案,满足 ans[i]
是如上所述的 下一个更大元素 。
Example:
输入:nums1 = [4,1,2], nums2 = [1,3,4,2]. 输出:[-1,3,-1] 解释:nums1 中每个值的下一个更大元素如下所述: - 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。 - 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。 - 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
思路:单调栈+哈希表,首先遍历 n u m s 2 nums2 nums2,用单调栈求出每一个元素的下一个更大元素,将结果存放在哈希表中,然后再遍历 n u m s 1 nums1 nums1,根据元素的值直接从哈希表中取出在 n u m s 2 nums2 nums2中对应位置右侧下一个更大的元素
总的时间复杂度为 O ( m + n ) O(m+n) O(m+n),其中 m m m和 n n n为数组 n u m s 1 nums1 nums1和 n u m s 2 nums2 nums2的长度
python
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
m, n = len(nums1), len(nums2)
stack = deque()
next_greater = dict.fromkeys(nums2, -1)
for i in range(n):
while stack and nums2[stack[-1]] < nums2[i]:
next_greater[nums2[stack[-1]]] = nums2[i]
stack.pop()
stack.append(i)
ans = []
for i in range(m):
ans.append(next_greater[nums1[i]])
return ans
503. 下一个更大元素 II
问题描述:
给定一个循环数组 nums
( nums[nums.length - 1]
的下一个元素是 nums[0]
),返回 nums
中每个元素的 下一个更大元素 。
数字 x
的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1
。
Example:
输入: nums = [1,2,1] 输出: [2,-1,2] 解释: 第一个 1 的下一个更大的数是 2; 数字 2 找不到下一个更大的数; 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
思路一:将两个 n u m s nums nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后只需要返回结果集中第一个 n u m s nums nums数组的部分
python
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
n = len(nums)
nums = nums[:] + nums[:-1]
stack = deque()
next_greater = [-1] * n
for i in range(len(nums)):
while stack and nums[stack[-1]] < nums[i]:
if stack[-1] < n:
next_greater[stack[-1]] = nums[i]
stack.pop()
stack.append(i)
return next_greater
思路二:不拼接数组,手动模拟循环的过程,相当于遍历两遍原数组,用取模操作来获取数组索引
python
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
n = len(nums)
stack = deque()
next_greater = [-1] * n
for i in range(2 * len(nums) - 1):
while stack and nums[stack[-1]] < nums[i % n]:
next_greater[stack[-1]] = nums[i % n]
stack.pop()
stack.append(i % n)
return next_greater
901. 股票价格跨度
问题描述:
设计一个算法收集某些股票的每日报价,并返回该股票当日价格的 跨度 。
当日股票价格的 跨度 被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
- 例如,如果未来 7 天股票的价格是
[100,80,60,70,60,75,85]
,那么股票跨度将是[1,1,1,2,1,4,6]
。
实现 StockSpanner
类:
StockSpanner()
初始化类对象。int next(int price)
给出今天的股价price
,返回该股票当日价格的 跨度 。
Example:
输入: ["StockSpanner", "next", "next", "next", "next", "next", "next", "next"] [[], [100], [80], [60], [70], [60], [75], [85]] 输出: [null, 1, 1, 1, 2, 1, 4, 6] 解释: StockSpanner stockSpanner = new StockSpanner(); stockSpanner.next(100); // 返回 1 stockSpanner.next(80); // 返回 1 stockSpanner.next(60); // 返回 1 stockSpanner.next(70); // 返回 2 stockSpanner.next(60); // 返回 1 stockSpanner.next(75); // 返回 4 ,因为截至今天的最后 4 个股价 (包括今天的股价 75) 都小于或等于今天的股价。 stockSpanner.next(85); // 返回 6
思路:
python
class StockSpanner:
def __init__(self):
self.prices = []
self.stack = deque()
def next(self, price: int) -> int:
n = len(self.prices)
while self.stack and self.prices[self.stack[-1]] <= price:
self.stack.pop()
if self.stack:
ans = n - self.stack[-1]
else:
ans = n + 1
self.stack.append(n)
self.prices.append(price)
return ans
42. 接雨水
问题描述:
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
Example:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
思路一:暴力法,按照列遍历,然后寻找当前位置 i i i两侧最大的列 l l l和 r r r,当前列可以接的雨水体积就是 m i n ( h e i g h t s [ l ] , h e i g h t s [ r ] ) − h e i g h t s [ i ] min(heights[l],heights[r]) - heights[i] min(heights[l],heights[r])−heights[i],对所有列的雨水(除了第一列和最后一列)求和就是最终的结果
python
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
ans = 0
for i in range(n):
if i == 0 or i == n - 1:
continue
h = self.exapnd(height, i)
if h > height[i]:
ans += h - height[i]
return ans
def exapnd(self, height, pos):
left_max = right_max = height[pos]
for i in range(pos - 1, -1, -1):
if height[i] > left_max:
left_max = height[i]
for i in range(pos + 1, len(height)):
if height[i] > right_max:
right_max = height[i]
return min(left_max, right_max)
优化,如果再求每一列的雨水时都遍历搜索两侧最高的列,那么总的时间复杂度是 O ( n 2 ) O(n^2) O(n2),但实际上我们可以预先求出每个位置两侧最高的列,这样求每一列的雨水时就无需再遍历搜索,总的时间复杂度是 O ( n ) O(n) O(n)
python
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
left_max = [0] * n
right_max = [0] * n
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i - 1], height[i])
right_max[n - 1] = height[-1]
for i in range(n - 2, -1, -1):
right_max[i] = max(right_max[i + 1], height[i])
ans = 0
for i in range(n):
if i == 0 or i == n - 1:
continue
h = min(left_max[i], right_max[i])
if h > height[i]:
ans += h - height[i]
return ans
思路二:单调栈,除了按照列遍历,我们还可以按照行计算,如下图所示,按照这种思路,我们本质上是在寻找凹形结构,也就是寻找当前位置左侧和右侧第一个高于当前位置的列,对于这类问题很容易联想到单调栈
如果栈为空或者栈顶元素的高度大于当前位置的高度,直接入栈,否则弹出栈顶元素 t o p top top,如果此时栈不为空,说明形成了一个凹槽,可以接雨水,接下来就是计算凹槽中矩形的面积。矩形的长度为当前位置和此时栈顶元素所处位置中间区域的长度,即 i − s t a c k [ − 1 ] − 1 i - stack[-1] - 1 i−stack[−1]−1,高度为 m i n ( h e i g h t [ i ] , h e i g h t [ s t a c k [ − 1 ] ] ) − h e i g h t [ t o p ] min(height[i], height[stack[-1]])-height[top] min(height[i],height[stack[−1]])−height[top] ,在得到了长度和高度后就可以计算矩形面积并累加到结果中
python
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
stack = deque()
ans = 0
for i in range(n):
while stack and height[stack[-1]] < height[i]:
top = stack.pop()
if stack:
w = i - stack[-1] - 1
h = min(height[i], height[stack[-1]]) - height[top]
area = w * h
ans += area
stack.append(i)
return ans
84. 柱状图中最大的矩形
问题描述:
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
Example:
输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10
思路一:暴力法,这道题初看很复杂,但实际上和接雨水类似,我们可以按照列来遍历,以当前列的柱子高度为矩形高度向两侧扩展,如果两侧的柱子更高,那么扩展不会受影响,但如果遇到更低的柱子,就无法继续扩展下去,此时形成的矩形就是以当前列的柱子高度为矩形高度所能达到的最大矩形,如果对每一列都进行扩展,那么我们就可以求出在该柱状图中,能够勾勒出来的矩形的最大面积
因此,该问题就转换成了寻找当前位置 i i i左侧和右侧第一个低于当前位置的列 l l l和 r r r ,从而以当前列的柱子高度为矩形高度扩展形成的最大矩形面积就可以表示为 ( r − l − 1 ) × h e i g h t s [ i ] (r - l - 1) \times heights[i] (r−l−1)×heights[i]
python
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
min_left = [0] * n
min_right = [0] * n
min_left[0] = -1
for i in range(1, n):
t = i - 1
while t >= 0 and heights[t] >= heights[i]:
t = min_left[t]
min_left[i] = t
min_right[n - 1] = n
for i in range(n - 2, -1, -1):
t = i + 1
while t < n and heights[t] >= heights[i]:
t = min_right[t]
min_right[i] = t
ans = 0
for i in range(n):
left = min_left[i]
right = min_right[i]
area = (right - left - 1) * heights[i]
ans = max(ans, area)
return ans
优化,和接雨水同理,我们可以预先求出每个位置两侧第一个低于当前位置的列,这里我们用 l e f t [ i ] left[i] left[i]和 r i g h t [ i ] right[i] right[i]来分别表示 i i i左侧第一个小于 h e i g h t s [ i ] heights[i] heights[i]的柱子下标和 i i i右侧第一个小于 h e i g h t s [ i ] heights[i] heights[i]的柱子下标,对于位置 i i i左侧第一个小于当前位置的下标求解,我们可以从 k = i − 1 k=i-1 k=i−1的位置开始往前搜索,如果 k ≥ 0 k \ge0 k≥0且 h e i g h t s [ k ] ≥ h e i g h t s [ i ] heights[k] \ge heights[i] heights[k]≥heights[i],说明位置 k k k的高度仍然不小于当前高度,此时我们从左侧第一个小于 h e i g h t s [ k ] heights[k] heights[k]的位置继续寻找,即 k = l e f t [ k ] k=left[k] k=left[k],如此循环直到 h e i g h t s [ k ] < h e i g h t s [ i ] heights[k] < heights[i] heights[k]<heights[i]或者 k < 0 k < 0 k<0,对于第一种情况,说明我们找到了第一个小于当前位置的柱子,而第二种情况说明不存在更小的柱子,这里我们将 l e f t left left数组全部初始化为-1,这样 l e f t [ i ] = − 1 left[i]=-1 left[i]=−1就代表左侧没有更小的柱子, r i g h t right right数组的求解也是同理
python
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
left = [-1] * n
right = [n] * n
for i in range(1, n):
k = i - 1
while k >= 0 and heights[k] >= heights[i]:
k = left[k]
left[i] = k
for i in range(n - 2, -1, -1):
k = i + 1
while k <= n - 1 and heights[k] >= heights[i]:
k = right[k]
right[i] = k
ans = 0
for i in range(n):
ans = max(ans, (right[i] - left[i] - 1) * heights[i])
return ans
思路二:单调栈,这里和接雨水是一样的思路,只是维护栈内的单调递减顺序变成了维护栈内的单调递增顺序,但有一点不同的是,我们预先在 h e i g h t s heights heights的两侧各插入了一个0,这是为什么呢?
在接雨水中,第一个和最后一个位置是接不到雨水的,所以不需要对左右边界做额外的处理,但是在本题中所有列都需要求其所能扩展成的最大矩形面积,而单调栈中只有栈内有元素且违反了递增顺序时才会进行处理,这就导致代码可能无法考虑左右边界的情况,插入0就可以保证左右边界的柱子不为0时一定会进入while循环中,从而保证不遗漏边界的情况
python
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
heights.append(0)
heights.insert(0, 0)
n = len(heights)
stack = deque()
ans = 0
for i in range(n):
while stack and heights[stack[-1]] > heights[i]:
top = stack.pop()
if stack:
w = i - stack[-1] - 1
h = heights[top]
area = w * h
ans = max(ans, area)
stack.append(i)
return ans