5-2 栈和队列相关题目


版权声明:以下题目均来自 LeetCode, 仅仅提供跳转到力扣官网的链接,不在本页面出现题目内容,本文章内容禁止商业用途。

目录

小节 位置
5.2.1 栈的常见题目
5.2.2 单调栈
5.2.3 队列的常见题目
5.2.4 优先队列

5.2.1 栈的常见题目:

5.2.1.1 用栈实现队列

LeetCode 232.用栈实现队列 | | 返回目录5.2.1

思路:队列是先入的先出,所以要用一个栈存储数据,另一个栈用来调整顺序。

class MyQueue:

def __init__(self):
self.__s_a = []
self.__s_b = []

def push(self, x: int) -> None:
# 专门用 a 栈来存储数据
self.__s_a.append(x)

def pop(self) -> int:
# 先将a栈的数据全部导入b栈,此时顺序就反过来了
while self.__s_a:
self.__s_b.append(self.__s_a.pop())
# 这样b栈的栈顶元素就是刚刚的队头元素,将其弹出给res
res = self.__s_b.pop()
# 然后再把b栈里的元素恢复到a栈中,保持a栈存储元素,b栈常态为空的状态
while self.__s_b:
self.__s_a.append(self.__s_b.pop())
return res

def peek(self) -> int:
# 与pop的操作几乎一致,只不过不用使用pop弹出
while self.__s_a:
self.__s_b.append(self.__s_a.pop())
res = self.__s_b[-1]
while self.__s_b:
self.__s_a.append(self.__s_b.pop())
return res

def empty(self) -> bool:
return not self.__s_a

5.2.1.2 最小栈

LeetCode 155.最小栈 | | 返回目录5.2.1

思路1:用一个栈维护数据,再用另一个栈维护最小值。数据栈在进行入栈、出栈操作时,也要同时对最小值栈进行操作。

class MinStack:
import math
def __init__(self):
self.__stack_list=[]
self.__min_list=[]

def push(self, val: int) -> None:
self.__stack_list.append(val)
if self.__min_list:
#self.__min_list.append(val)
min_v = min(val, self.__min_list[-1])
self.__min_list.append(min_v)
else:
self.__min_list.append(val)

def pop(self) -> None:
if self.__stack_list:
self.__stack_list.pop()
self.__min_list.pop()
return

def top(self) -> int:
if self.__stack_list:
return self.__stack_list[-1]
else:
return math.nan

def getMin(self) -> int:
if self.__min_list:
return self.__min_list[-1]
else:
return math.nan

思路2:由于思路1中用到了一个辅助栈,空间的消耗会是原始数据的2倍。这里可以考虑一个不用辅助栈的方案.
只维护一个栈,栈里存储的是与 min_value 的差值:delta
min_value 其实承担了两个功能,第一个就是栈中最小值,另一个就是如果delta是小于0的话,那就是栈中对应位置的真实值。
换句话说,那个delta栈,想从它恢复成原始数据栈,要么就是对应的delta值加上min,要么就是直接填入min值。这完全取决于delta的正负,这个正负的信息其实很重要。

class MinStack:
def __init__(self):
# 维护一个delta栈,和一个最小值
self.__delta_stack = []
self.__minv = -1
# 这个最小值的初始值可以不用在意,
# 最小值是在栈不为空的时候才会有意义的

def push(self, val: int) -> None:
if len(self.__delta_stack) == 0:
# 如果栈为空,当前数就是唯一存在的数,自然就是最小值
self.__minv = val
# 它与自己的差值明显就是 0
self.__delta_stack.append(0)

else:
delta = val - self.__minv
if delta < 0:
# 说明新来的val比已有的minv更小,需要更新 minv 为当前val
self.__minv = val
# 如果delta大于0 ,说明原来的minv确实还是更小的,就不用更新
# 然后不管是大于0还是小于0 ,依然要将delta入栈
self.__delta_stack.append(delta)

def pop(self) -> None:
if self.__delta_stack:
delta = self.__delta_stack.pop()
if delta < 0:
# 说明min_v储存的值就是对应的 val
# 这是由我们之前的入栈操作决定的
val = self.__minv
# 并还原之前的 minv
self.__minv = val - delta
else:
val = delta + self.__minv
return val
else:
return None

def top(self) -> int:
if self.__delta_stack:
delta = self.__delta_stack[-1]
return self.__minv if delta < 0 else delta+self.__minv
else:
return None


def getMin(self) -> int:
return self.__minv if self.__delta_stack else None

5.2.1.3 删除字符串中的所有相邻重复项

LeetCode 1047.删除字符串中的所有相邻重复项 | | 返回目录5.2.1

方案:用一个栈来循环装入每个字符,每个字符入栈时,都要与栈顶元素(即前一个字符)对比一下,若不相同才装入;若相同就不装入,同时删除栈顶元素。

class Solution:
def removeDuplicates(self, s: str) -> str:
char_s = []

for ch in s:
if char_s: # 如果栈不为空才进行后续操作
if ch == char_s[-1]:
char_s.pop()
continue
char_s.append(ch)

return ''.join(char_s)

5.2.1.4 有效的括号

LeetCode 20.有效的括号 | | 返回目录5.2.1

方案:和上面的 5.2.1.2 的思想类似,只不过上面是成对的消字符,这里是成对的消除括号,原理是一样的。如果中途就碰见不能消除的情况,就返回False了。最终消除完之后看看是否还剩的有括号

class Solution:
def isValid(self, s: str) -> bool:
bracket_s = []
for ch in s:
if ch == '(' or ch == '[' or ch == '{':
bracket_s.append(ch)

elif ch == ')':
if bracket_s and bracket_s[-1] == '(':
bracket_s.pop()
else:
return False
elif ch == ']':
if bracket_s and bracket_s[-1] == '[':
bracket_s.pop()
else:
return False
elif ch == '}':
if bracket_s and bracket_s[-1] == '{':
bracket_s.pop()
else:
return False
return len(bracket_s) == 0

5.2.1.5 最长有效括号

LeetCode 32.最长有效括号 | |返回目录5.2.1

方案:此题依旧和上面两个题是一样的原理,做消消乐。同样是成对的消括号,只不过这一次要算一下消除的区域的长度。一个很自然的想法就是:需要消掉的部分的最后一个字符的索引index,减去尚未被消除的部分的最后一个字符的索引index,就是此次消除区域的长度gap。最后要得到的是最长的这个gap,只需要每次用一个max函数更新一下就行。

'''code version.1'''
class Solution:
def longestValidParentheses(self, s: str) -> int:
# 我们将(符号,索引) 的结构进行入栈,这样就既能判断符号,又可以知道对应的索引了
# 这里为何要将 stack 初始化为 (')', -1)?
# 因为如果字符串是 "()"开头,这块区域消掉之后,前面并没有未消除区域,我们不太好追溯左边界的索引值,所以要人为设置一个左边界
# 所以我们假设是由')'开头,且位于-1位置,这样不影响原数据。
# 而且这个开头一定不会被消掉,相当于规定了一个边界值。所以我们的栈一定不会为空,简化了条件
stack, gap = [[')', -1]], 0
for i in range(len(s)):
ch = s[i]
if ch == '(':
# 如果为左括号,直接入栈符号和其索引
stack.append([ch, i])
else: # 如果来的是右括号
if stack[-1][0] =='(':
# 当前来的是右括号,且栈顶元素(上一个符号)是左括号
# 说明可以被消掉,则先消除前面的符号
stack.pop()
# 然后用当前索引 i, 减去尚未消掉部分的最后一个字符的索引,得到gap
gap = max(gap, i-stack[-1][1])
else:
# 如果为右括号,且无法消掉,直接修改其索引,相当于更新边界
stack[-1][1] = i
# stack.append([ch, i])
return gap
''' code version.2
上面的code中,栈每次入栈的时候,同时存下了(符号,索引), 两个数据;
这样的好处是便于理解,从直观上看和我们上面的题目的code能统一;
但是辅助栈消耗的空间就较大了,这里可以在原有的基础上进一步update
'''
### update 进行如下修改
class Solution:
def longestValidParentheses(self, s: str) -> int:
stack, gap = [-1], 0
for i in range(len(s)):
ch = s[i]
if ch == '(':
# 如果为左括号,直接入栈 索引
stack.append(i)
else:
# 直接弹出栈顶元素
stack.pop()
# 下面这段实现的功能是:
# 如果栈不为空,那就可以直接计算gap值。
# 如果 弹出栈顶元素之后,栈为空了,那就将现在的右括号的索引入栈存下
if stack:
gap = max(gap, i-stack[-1])
else:
stack.append(i)
# 和上面的code发现判断条件从 是左括号还是右括号的,变成了判断栈是否为空
# ① 假设弹出的是左括号,那么必然不为空,
# 因为栈中剩下的要么是左括号,要么是右括号(因为我们一开始先假设了一个右括号放在-1位置作为边界值)
# ② 假设弹出的是右括号,那么栈必然空了,因为在上面的code,我们始终只维护了一个作为边界的右括号;即如果栈不为空,那么有且只有一个右括号存在,且位于最左侧
# 既然我们不需要再判断出栈的元素是左括号还是右括号,只需要判断栈是否为空
# 那么意味着我们不需要将符号入栈了!!!只需要入栈索引即可,所以code就改为了这个版本
return gap

5.2.1.6 基本计算器

LeetCode 224.基本计算器 | | 返回目录5.2.1

方案:
这个题只有加法和减法两种运算符出现,而这两种运算符根据交换律和结合律,是可以从左到右依次计算就行。
但是此题还涉及到括号’(‘和’)’,那么这个括号会造成什么影响呢?从直观上来看,括号内的区域要先计算,所以一种思路是先判断括号,然后先计算括号内区域的值,最后退化成只有普通加减法的情况,当涉及到括号的多层嵌套的时候,可能会有点复杂;

另一种思路,那就是消掉括号,把式子改写为没有括号的情况,而且由于只涉及到加、减运算,消掉括号后依然能保持从左往右的基本计算顺序,只要保证消括号时数字前面的运算符能够完成转换即可。

这里采用第二种思路,消括号的思路。 如果把一对括号( )包含的区域视为一个整体的话,这个整体都会受到括号外的正/负号的影响。
比如括号外是’+’,那么括号内的每一个运算符都要在原来的基础上多一个’+’,对于加减运算而言,相当于原来的符号不变;
如果括号外是’-‘,那么括号内的每一个运算符都要取一次反,即括号内’+’变’-‘,括号内’-‘变’+’。我们在代码中用+1和-1来描述这两种状态。

此外该题目还隐藏了一个陷阱: 题目举的例子中都是个位数。如果遇到十位、百位不为空的情况,是需要注意,这个数字也会按照字符被分割开。比如12会被分割成1和2的字符依次输入,我们需要将其还原为原始的12!

class Solution:
def calculate(self, s: str) -> int:
# 把一对括号()包含的区域视为一个整体的话,这个整体都会受到括号外的正/负号的影响
# 所以对括号内数组的正负号再乘以括号外这个整体的正负号,才是实际的正负号
# 这里用 +1 表示正,-1表示负,初始状态明显为正
num, preSign = 0, 1
# 用一个栈来存储每一个括号区域整体的正负号
sign_list = [1]

res = 0
# 因为题目中说,字符仅仅包含 数字、'+'、'-'、'('、')'、和 ' '
# 所以我们人为设置一个右边界 + ---> s + '+'
# 这是为了在遇到以数字结尾的情况时,仍能进入运算阶段
# 就好比在使用计算器时,如果不使用'='号键,按'+'键也能得出之前的加减结果
for ch in s + '+':
if ch == ' ':
continue
elif ch.isdigit():
# 如果是几个数字字符连续出现,这样就能获得整个数字的实际值,比如"12"
num = num*10 + ord(ch) - ord('0')
else: # ch == '(' or ')' or '+' or '-'
# 进入到非数字环节,就要结算前面的数字部分
res += preSign * num
# 需要将数字部分重新置零
num = 0
if ch in '-+':
# 后面区域的真实的符号,是要和最近一层的括号区域的符号乘以一次进行修正
preSign = sign_list[-1]*(-1 if ch == '-' else 1)
elif ch == '(':
# 遇到左括号, 就需要对前面的符号进行暂存
sign_list.append(preSign)
elif ch == ')':
# 当一个括号结束时,就要消掉一个括号区域的符号
sign_list.pop()
return res

5.2.1.7 基本计算器Ⅱ

LeetCode 227.基本计算器II | | 返回目录5.2.1

方案:
这里相对于上题来说,没有括号符,但是又多了乘法和除法运算符,而这两个运算符的优先级比加、减运算要高。所以我们可以考虑先对高优先级的部分进行运算,其余部分不动。就能退化为简单的没有括号的数组相加形式。
具体实现方式为:
如果遇到’+’号,就将数字直接存入栈中;
如果遇到’-‘号,就将数字取反后存入栈中;
如果遇到’*、/‘号,就将数字与栈顶元素运算之后,替换原有栈顶元素存入栈中;

class Solution:
def calculate(self, s: str) -> int:
num_stack = []
preSign = '+'
num = 0
for ch in s+'+':
if ch == ' ':
continue
elif ch.isdigit():
num = num*10 + ord(ch) - ord('0')
else:
if preSign == '+':
num_stack.append(num)
elif preSign == '-':
num_stack.append(-num)
elif preSign == '*':
num_stack.append(num_stack.pop() * num)
# 也可以写成 num_stack[-1] = num_stack[-1] * num
else:
num_stack.append(int(num_stack.pop() / num))
# 也可以写成 num_stack[-1] = int(num_stack[-1] / num)
num = 0
preSign = ch
return sum(num_stack)

5.2.1.8 字符串解码

LeetCode 394.字符串解码 | | 返回目录5.2.1

这个题的思想和 5.2.1.6 很相似,也是有括号,只不过这里是中括号[ ];
同样也会有括号外的因素影响括号内的值,5.2.1.6是括号外的符号影响括号内的符号,此题是括号的数字影响括号内的字符重复次数;所以我们也需要用辅助栈来存储括号外的值:
建立辅助栈 num_list 和 str_list 来分别存储括号外的数字和字符串。
对于数字和字符本身的处理,也借鉴5.2.1.6中的处理方式。

class Solution:
def decodeString(self, s: str) -> str:
num, tmp_str = 0, ''
num_list, str_list = [], []

for ch in s:
if ch.isdigit():
# 如果出现连续的数字字符,通过此方法来还原其真实数值
num = num*10 + ord(ch) - ord('0')
elif ch.isalpha():
# 如果出现连续的单字符,也通过此方法来还原大的字符串
tmp_str += ch
elif ch == '[':
# 出现左括号,意味着要对括号外的元素进行存储了
num_list.append(num)
num = 0
str_list.append(tmp_str)
tmp_str = ''
else:
# 出现右括号,意味着要对最近的一对括号进行清算了
out_num = num_list.pop()
out_str = str_list.pop()
# 一定要注意,括号外的顺序是 out_str + out_num
# out_num要乘以的是当前括号内的tmp_str
tmp_str = out_str + out_num * tmp_str

return tmp_str

5.2.1.9 逆波兰表达式求值

LeetCode 150.逆波兰表达式求值 | | 返回目录5.2.1

方案:该题的思路其实在例3中已经体现出来了,就是观察规律而已。
规律就是每遇到一个运算符号,就要用这个符号与运算左侧最近的两个数字,得到一个数字后,继续向右遍历。所以又需要用辅助栈来存储数字。

class Solution:
def evalRPN(self, tokens: List[str]) -> int:
s = []
for ch in tokens:
if ch not in "+-*/":
# 如果遇到了数字,直接用栈进行存储
s.append(int(ch))
else:
# 如果遇到的是运算符,就先把左侧两个数出栈
# 然后就要用运算符去结合这两个数,得到的结果,存入栈中
a2 = s.pop()
a1 = s.pop()
if ch == '+':
s.append(a1+a2)
elif ch == '-':
s.append(a1-a2)
elif ch == '*':
s.append(a1*a2)
else:
s.append(int(a1/a2))
return s[0]

5.2.1.10 验证栈序列

LeetCode 946. 验证栈序列 | | 返回目录5.2.1

方案:此题主要是模拟栈的压入压出操作。
对于pushed的数字,先假设都能压入,那么每来一个数字,都压入辅助栈;
每次压入之后,都要看看栈顶元素是否是poped中的对应位置的数字,是的话,那就又把该数字pop出去

class Solution:
def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool:
s, i = [], 0
for num in pushed:
# 每遍历一个pushed中的数字,就将其压入辅助栈
s.append(num)
while (s and s[-1] == popped[i]):
s.pop()
i+=1
return len(s) == 0

5.2.2 单调栈

所谓单调栈,就是从栈顶到栈底的方向上是单调递增(减),所以其实从栈底到栈顶的方向也是单调的。(因为python中一般用 list 来当栈用,所以从序号0开始看的话,我们一般都是看栈底到栈顶的方向。)

单调栈如何用操作呢?假设讨论单调递增的情况,如果一个元素比栈顶的元素大,我们才入栈;否则就先pop掉栈顶元素,然后继续比;直到满足等待入栈的元素比栈顶元素大了,或者栈已经为空了,才入栈。这样栈中的元素就是能保持单调递增的关系

5.2.2.0 解法总结

单调栈通常被用来解决:求上一个更大(小)值 和 下一个更大(小)值的问题,如果遇到这类题型,其核心code的写法几乎都是差不多的,在这里先总结一下。

''' 假设 数据列表是 data_lt, 单调栈是 help_s ;
在下面讨论的情况中,单调栈存储的都是索引值index'''

'''(1) 求下一个更大值 '''
# 此时构造单调栈的目标是【单调递减】(可以相等)
# 理由如下:
# 如果元素一直递减(或相等),意味着【下一个更大数】一直未出现;
# 那么栈就可以一直默认压入元素
# 直到遇到一个【更大数】,栈才会进行pop出栈操作

# 核心code
while help_s and data_lt[help_s[-1]] < data_lt[i]:
# 当栈不为空,且遇当前数 > 栈顶元素,说明遇到了【下一个更大数】
# 就需要出栈,直到当前数 <= 栈顶元素,栈又能保持单调递减性,既可以跳出循环
# old_i 代表栈顶元素的索引,很明显,当前i位置的数,就是old_i位置数的【下一个更大数】
old_i = help_s.pop()
# gap存储的是old_i位置的数,距离它的【下一个更大数】i位置的 间距
gap[old_i] = i - old_i
# next_big_id,存储的是old_i位置数,的【下一个更大数】的索引
next_big_id[old_i] = i
# 如果i位置的数能能保持栈的单调递减,就压入i位置的索引
help_s.append(i)

'''(4) 求下一个更小值'''
# 此时构造单调栈的目标是【单调递增】(可以相等)
# 理由如下:
# 如果元素一直递增(或相等),意味着【下一个更小数】一直未出现;
# 那么栈就可以一直默认压入元素
# 直到遇到一个【更小数】,栈才会进行pop出栈操作

# 核心code
while help_s and data_lt[help_s[-1]] > data_lt[i]:
# 当栈不为空,且遇当前数 < 栈顶元素,说明遇到了【下一个更小数】
# 就需要出栈,直到当前数 >= 栈顶元素,栈又能保持单调递减性,既可以跳出循环
# old_i 代表栈顶元素的索引,很明显,当前i位置的数,就是old_i位置数的【下一个更小数】
old_i = help_s.pop()
gap[old_i] = i - old_i
next_small_id[old_i] = i

help_s.append(i)

'''(3) 求上一个更大值 '''
# 此时构造单调栈的目标是【单调递减】(不可以相等)
# 理由如下:
# 如果元素一直递减(不可以相等),意味着前面的数一直比后面的数大,故后面的数的【上一个更大数】,就是紧邻它的上一个数;
# 那么栈就可以一直默认压入元素
# 直到遇到一个较大的数,此时栈顶元素小,为了寻找前面是否有当前数的【上一个更大数】,就需要出栈操作

# 这里要注意,相较于求【下一个更大数】,此时的单调栈中是不允许出现连续相等值的
# 因为如果出现相等值,比如连续出现两个6,那么第二个6的前一个数。并不是它的【上一个更大数】
# 为了去寻找上一个更大数,只能往更前方去寻找,所以还是要执行出栈操作

# 核心code
while help_s and data_lt[help_s[-1]] <= data_lt[i]:
# 当栈不为空,且遇当前数 >= 栈顶元素,说明当前数需要去更前方寻找【上一个更大数】
# 就需要出栈,直到当前数 < 栈顶元素,栈又能保持单调性,说明找到了当前数的【上一个更大数】
help_s.pop()
# 跳出while循环后,栈顶的数是满足<上一个更大数>的条件的
last_big_id[i] = help_s[-1] if help_s else -1
gap[i] = i - help_s[-1] if help_s else i # or -1

help_s.append(i)

'''(4) 上一个更小值 '''
# 此时构造单调栈的目标是【单调递增】(不可以相等)
# 理由如下:
# 如果元素一直递增(不可以相等),意味着前面的数一直比后面的数小,故后面的数的【上一个更小数】,就是紧邻它的上一个数;
# 直到遇到一个较小的数,此时栈顶元素较大,为了寻找前面是否有当前数的【上一个更小数】,就需要出栈操作

# 这里要注意,相较于求【下一个更小数】,此时的单调栈中是不允许出现连续相等值的
# 因为如果出现相等值,比如连续出现两个6,那么第二个6的前一个数。并不是它的【上一个更小数】
# 为了去寻找上一个更小数,只能往更前方去寻找,所以还是要执行出栈操作

# 核心code
while help_s and data_lt[help_s[-1]] >= data_lt[i]:
# 当栈不为空,且遇当前数 <= 栈顶元素,说明当前数需要去更前方寻找【上一个更小数】
# 就需要出栈,直到当前数 > 栈顶元素,栈又能保持单调性,说明找到了当前数的【上一个更小数】
help_s.pop()
# 跳出while循环后,栈顶的数是满足<上一个更小数>的条件的
last_small_id = help_s[-1] if help_s else -1
gap[i] = i - help_s[-1] if help_s else i # or -1

help_s.append(i)

'''
找更大 、 更小时,栈里的数据
'''
# 找【更大】数,不管是上一个还是下一个,单调栈中 数据的趋势是递减的;
# 找【更小】数,不管是上一个还是下一个,单调栈中 数据的趋势是递增的。

5.2.2.1 每日温度

LeetCode 739.每日温度 | | 返回目录5.2.2

方案:
此题的核心思想就是:【求下一个更大数】
用的是单调栈,且保持单调递减的关系。
就是如果后面的温度总比前一天小(或相等),我们就可以不管它,相当于直接入单调栈;
如果后面的温度比前面的大,我们才来计算相差的gap天数。具体看操作看code:

class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
s = [] # s是单调栈
N = len(temperatures)
# gap 是用来保存相差天数的数组,默认为0
gap = [0]*N
# 这里我们的单调栈存储的是天数(index),但是用的是温度值进行比较大小
# 因为只要能获得index,就能从温度数组中定位到具体的温度
# 对于该方法,我们维护的单调栈是递减的(或相等)
# 即每遍历到一个元素,栈顶的元素应当 不小于 当前元素
# 否则,当前元素就是【更大数】,就需要出栈计算差值
for i in range(N):
# 单调栈不为空,且当前元素 比 栈顶元素大的情况,我们就需要操作
while s and temperatures[i] > temperatures[s[-1]]:
# 将单调栈的栈顶元素出栈
# 对于该题,lower_i 是之前较低温度的索引
lower_i = s.pop()
# 同时顺便计算一波差值delta
# 即被出栈的那一天,与当前日期的差值,(因为当前天就是一个高温值)
gap[lower_i] = i - lower_i
# 直到满足了单调递减的关系,(或者栈为空),我们才入栈
s.append(i)
return gap

5.2.2.2 下一个更大元素I

LeetCode 496.下一个更大元素 I | | 返回目录5.2.2

方案:问题分为两个子问题:
①依据nums1中的元素定位到nums2中的索引位置;
②求该位置下一个更大的元素;
对于问题①,虽然可以用list.index(obj)来返回,但是该操作每次执行的时间复杂度是O(N);
所以干脆先遍历一道nums2,将元素和索引存入哈希表中,后续再查的时候,查询的时间复杂度就是O(1)。
对于问题②,其实就是 【求下一更大数】 这个问题,直接套用单调栈核心code。

'''方案1. 暴力求解'''
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash_l = {}
N2 = len(nums2)
# 先将nums2的元素与序号对应关系存入哈希表
for i, num in enumerate(nums2):
hash_l[num] = i

# 每次遍历nums1获得的num,都去num2中其位置右侧进行遍历,试图找到最近的最大值
# 时间复杂度 O(N1*N2)
res = []
for num in nums1:
index = hash_l[num]
flag = False
while index < N2 and not flag:
if nums2[index] > num:
res.append(nums2[index])
flag = True
index += 1
if not flag: res.append(-1)
return res

'''方案2. 利用单调栈,借鉴【5.2.8 每日温度】的解法'''
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
h={}
N = len(nums2)
s, next_big_val = [], [-1]*N
# 注意,此题需要求的是【下一个更大数】的数值,所以 next_big_val直接存入数值
# 且题目中规定了,不存在的话值为-1

for i in range(N):
h[nums2[i]]=i
while s and nums2[s[-1]] < nums2[i]:
lower_i = s.pop()
next_big_val[lower_i] = nums2[i] # 这里直接存储nums2的元素
s.append(i)

return [next_big_val[h[num]] for num in nums1]

5.2.2.3 下一个更大元素 II

LeetCode 503.下一个更大元素 II | | 返回目录5.2.2

方案:问题本质依旧是【求下一个更大数】;核心点在于数组可以循环。
也就是一个数能够往右看的范围,到数组尾部后,又可以从头可以回到它自己身上。
那么说明如果看完一圈还没有找到更大的话,那再多看几圈也不会有更大的。所以数组最多遍历2次就够了。

'''1.最简单的思路,就是把原来数组扩充一倍,
只不过返回结果的时候,只返回前N个就可以了'''
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
N = len(nums)
s, res = [], [-1]*2*N
nums2 = nums + nums
for i in range(len(nums2)):
while s and nums2[i] > nums2[s[-1]]:
tmp_i = s.pop()
res[tmp_i] = nums2[i] # 直接存储数字值
s.append(i)
return res[0:N]
'''方案1稍微有点耗内存,可以考虑不用扩充数组的方式来循环,即求模运算。
但求模可能又会多花点时间'''
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
s = []
N = len(nums)
next_big_val = [-1]*N
for i in range(2*N):
# nums取值将index限定在[0,N-1]
while s and nums[s[-1]] < nums[i%N]:
lower_i = s.pop()
next_big_val[lower_i] = nums[i%N]
# s存入index时,也要求模运算,将index限定在[0,N-1]
s.append(i%N)
return next_big_val

5.2.2.4 股票价格跨度

LeetCode 901.股票价格跨度 | | 返回目录5.2.2

方案:问题本质:【求上一个更大数】。gap是当前 i 位置与上一个更大元素的位置的gap。那么在这个gap区间,所有的数都是小于当前值的,这就是该题要求的跨度。
所以我们需要维护一个 单调递减栈。

class StockSpanner:

def __init__(self):
self.__s = []
self.__data = []
self.__len = 0

def next(self, price: int) -> int:
# 需要先存入新来的数据
self.__len +=1
self.__data.append(price)
i = self.__len - 1
while self.__s and self.__data[self.__s[-1]] <= price:
self.__s.pop()
gap = i- self.__s[-1] if self.__s else self.__len
self.__s.append(i)

return gap

5.2.2.5 柱状图中最大的矩形

LeetCode 84.柱状图中最大的矩形 | | 返回目录5.2.2

方案:
首先,对于每一个柱状体,都至少有一个面积,就是它自身。
其次,如果一个柱状体的左右两侧都有高于或等于它的柱状体,那么它就能以它自身的高度进行横向扩展。
所以,对于每一个柱状体能够构成的矩形,其高度是固定的,就是该柱状体的高度,我们只需要求能够扩展的宽度,就能求出以每一个柱状体为base,能够构成的最大矩形的面。
所以这个题目,就转化成了一个求每个柱状体能够扩展的最大的宽度的这么一个问题。
也就是说,对于每一个柱状体,需要找到左侧第一个比它小的柱状体,和右侧第一个比它小的柱状体。这二者之间间隔的宽度,就是能够扩展的最大宽度。
问题就转化成了:【上一个更小数】,和【下一个更小数】

class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
N = len(heights)
left_small_list, right_small_list = [-1]*N, [N]*N
L_s, R_S = [], []
res = []

for i in range(N):
# 求【上一个更小数】的索引
while L_s and heights[i] <= heights[L_s[-1]]:
L_s.pop()
left_small_list[i] = L_s[-1] if L_s else -1 # L_S 为空的话,最左侧边界为-1
L_s.append(i)

# 求【下一个更小数】的索引
while R_S and heights[i] < heights[R_S[-1]]:
larger_i = R_S.pop()
right_small_list[larger_i] = i
R_S.append(i)
# print(left_small_list, right_small_list)
# 区间宽度就是:下一个最小的索引 - 上一个最小的索引 - 1
return max([heights[i]*(right_small_list[i] - left_small_list[i]-1) for i in range(N)])

5.2.2.7 接雨水

LeetCode 42.接雨水 | | 返回目录5.2.2

该题在处已经讲过,但是同样可以用单调栈来做。
考虑何时当前柱子能够接到水?必然是其左右侧都有高于它的边界才行。
如果对于遍历的数,如果求【上一个更大数】,那么这上一个更大数(可相等),和当前数之间的区域,就都能装水;
因为这区间的中的数,都小于当前数,同样更小于找到的【上一个更大数(相等数)】;
由于维护的单调栈是单调递减的,所以在寻找【上一个更大数】的过程中,每从栈中pop处一个数,
该数的左侧数一定是比它高的(因为递减关系),那么被pop出来的数 的左侧的数,就能作为被pop出的数的左边界。
此时,就能计算从pop出的位置开始,向右延伸到当前位置为右边的,一条横的水柱的体积。

''' 单调栈解法'''
class Solution:
def trap(self, height: List[int]) -> int:
N = len(height)
# 最左和最右的柱子只能当边界,无法装水
# 柱子一定要至少有3个才能装水
water = 0
if N <3:
return water

s = []
for i in range(N):
while s and height[i] > height[s[-1]]:
# 过程中每pop出一个索引,就要计算pop出的这个位置,能装多少水
tmp = s.pop()
if s:
# 当栈中还有元素可以作为左边界时,才进行下面的运算
# 刚刚被pop出的tmp的左侧数,也就是当前栈顶元素,作为tmp的左边界
L = s[-1]
# 以当前位置i作为tmp的右边界,计算区间宽度
W = i - L -1
# 计算这个横向水柱的高度
H = min(height[L], height[i]) - height[tmp]
water += W * H
s.append(i)
return water

5.2.3 队列的常见题目:

5.2.3.1 用队列实现栈

LeetCode 225.用队列实现栈 | | 返回目录5.2.3

方案:核心点在于队列只能从队首元素出队,也就是说,先进的先出;而栈是后进的先出。
那么如果要出栈,只需要把队尾的那个元素,挪到队首即可,怎么挪呢?
可以把前面的所有元素先存到另一个队列里去,这样队列元素的顺序依然能够保持,剩下一个队尾元素,进行出队列操作即可。

class MyStack:
def __init__(self):
# 虽然也能用list来充当队列,但是这里可以用更为标准的队列类:deque来作为队列使用
# 注意,deque其实是个双端队列,这里我们使用左侧为出队,右侧入队
from collections import deque
self.__q_a = deque([])
self.__q_b = deque([])

def push(self, x: int) -> None:
if not self.__q_a and not self.__q_b:
# 如果两个队列初始都为空,就先默认用队列a来填充元素
self.__q_a.append(x)

# 否则的话就使用二者中 不为空 的那个队列来填充
# (我们在后续的操作中,会保证常规状态下至少有一个队列是空的,另一个队列用来装元素)
# (所以不会出现两个队列都有元素的状态)
elif not self.__q_a:
self.__q_b.append(x)
else:
self.__q_a.append(x)

def pop(self) -> int:
# 如果当前是队列a有元素
if self.__q_a:
# 那就将队列a中除了队尾元素之外的所有元素装入队列b
while len(self.__q_a)>1:
self.__q_b.append(self.__q_a.popleft())
# 然后使用出队列操作弹出a队列中仅剩的这一个队尾元素(被当做栈顶元素)
return self.__q_a.popleft()
# 结束后,会发现队列a为空,元素全在队列b中了

# 如果当前队列b有元素,操作同上
elif self.__q_b:
while len(self.__q_b)> 1:
self.__q_a.append(self.__q_b.popleft())
return self.__q_b.popleft()

# 两个队列都为空
else:
return None

def top(self) -> int:
# 栈的top()操作本来就有队列中对应的 rear()操作,即取末尾元素
if self.__q_a:
return self.__q_a[-1]
else:
return self.__q_b[-1]

def empty(self) -> bool:
if not self.__q_a and not self.__q_b:
return True
else:
return False

5.2.3.2 设计循环队列

LeetCode 622.设计循环队列 | | 返回目录5.2.3

思路:
①首先,要设置 head 和 tail 两个游标,分别来控制队首和队尾;
∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷
②在初始化的时候,heal 和 tail 两个游标默认值通常是0的;
那么也就推出,在后续判断队列是否为空的时候,判断条件就是 head == tail;
∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷
③对于普通队列,如果tail超过了数组的长度length,那就说明队列满了;
但是由于此题是要构造循环队列,所以单纯用数组的长度来限制,是不够的;
将数组收尾相连成环的话,会发现,如果 tail的下一个位置是head,说明确实无空间再存储新元素了;
所以满队列的条件应当是 (tail +1) % length。
∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷
④关于tail游标指向的位置,是否应该指向最后一个元素?
在初始化时,head 和 tail 都是0,且 判断空队列的条件是,head == tail;
所以在队列为空的时候,tail指向的位置是head,但其实head处是没有元素的;
如果新增一个元素,head位置有元素了,队列不为空,那么tail就不应该再指向head;
而是应该指向head 的下一位;
由此可以知道,tail其实并不指向最后一个元素的位置,而是应该指向最后一个元素的下一个位置;
成为类似于“右边界”的存在。
所以,如果要让队列能存储的数据有 K 个,那么实际上需要有 K+1 长度的数组;
这样,当队列在不dequeue操作,第一次装满时:head=0,指向第一个元素;最后一个元素的位置是K;tail指向的是K+1

class MyCircularQueue:

def __init__(self, k: int):
self.__cycle_q = [-999] *(k+1)
self.__head = 0
self.__tail = 0
self.__len = k+1

def enQueue(self, value: int) -> bool:
if self.isFull():
return False
else:
self.__cycle_q[self.__tail] = value
self.__tail = (self.__tail+1) % self.__len
return True

def deQueue(self) -> bool:
if self.isEmpty():
return False
else:
# 并不需要真的删除原来的head指向的元素
# 只需要将它排除出有效区间即可
self.__head = (self.__head+1) % self.__len
return True

def Front(self) -> int:
if self.isEmpty():
return -1
else:
return self.__cycle_q[self.__head]

def Rear(self) -> int:
if self.isEmpty():
return -1
else:
# 这里要留心,tail指向的是最后元素的下一位
# 所以最后的元素是 tail-1的位置!
return self.__cycle_q[self.__tail-1]

def isEmpty(self) -> bool:
return self.__head == self.__tail


def isFull(self) -> bool:
return (self.__tail+1) % self.__len == self.__head

5.2.4 优先队列:

优先队列,是一种特殊的队列。它的增加和删除元素的方式和普通队列一样,入队从队尾增加元素,出队删除队头元素。区别点在于,普通队列的内部元素顺序就是按照元素入队列的顺序排布的,而优先队列自己会将内部元素按照一定的顺序进行排布。

也就是说,在新元素添加到队尾之后,队列会自己将这个新元素放到合适的位置,使得满足其顺序定义。而在出队列操作,弹出队首元素后,队列也会对剩下的元素重新梳理一遍顺序。而这个顺序,其实就是之前在排序章节,堆排序部分,讲过的堆结构的顺序。

所以优先队列也被称为堆队列(Heap queue),可以理解为大(小)根堆。python中有自带的优先队列的库:heapq
官方文档点击此处官方源码点击此处
下面讲解几个常用的操作:

import heapq
list_test = [10,7,8,9,5,6,4,2,1,0]

'''
1.heapify
将list转换为一个 小根堆,最小的元素会被放到队首(根节点)
'''
heapq.heapify(list_test)
print(list_test)
# [0, 1, 4, 2, 5, 6, 8, 10, 9, 7]
# (结果顺序并不一定和这个相同,只要0在根结点,满足小根堆,都是对的)

'''
2.heappush
将新元素插入末尾,并按照小根堆的顺序排序
'''
heapq.heappush(list_test, -1)
heapq.heappush(list_test, 20)
print(list_test)
# [-1, 0, 4, 2, 1, 6, 8, 10, 9, 7, 5, 20]
# (结果顺序并不一定和这个相同,只要-1在根结点,20在某个叶子结点,满足小根堆,都是对的)

''' 3.heappop
将队首元素(根节点)弹出,并将剩下的元素恢复成小根堆
'''
top = heapq.heappop(list_test)
print(list_test)
# [0, 1, 4, 2, 5, 6, 8, 10, 9, 7, 20]
print(top)
# -1

''' 4.heapreplace
同时执行heappop和heappush,但注意,操作顺序是:
先pop队首元素,然后push新元素
'''
top2 = heapq.heapreplace(list_test, 11)
print(list_test)
# [1, 2, 4, 9, 5, 6, 8, 10, 11, 7, 20]
print(top2)
# 0

''' 5.heappushpop
同时执行heappush和heappop,
这里先push新元素,然后再pop队首元素
'''
top3 = heapq.heappushpop(list_test, -10)
print(list_test)
# [1, 2, 4, 9, 5, 6, 8, 10, 11, 7, 20]
print(top3)
# -10

''' 6.merge
# 将多个堆合并,合并后的结果也是小根堆
'''
a = [10, 7, 6]
b = [1, 4, 5]
c = heapq.merge(a,b)
print(c, list(c))

''' 7.nlargest
查询堆中的最大n个元素, 并返回
'''
nlarge = heapq.nlargest(3, list_test)
print(nlarge)
# [20, 11, 10]

''' 8.nsmallest
# 查询堆中的最小n个元素
'''
nsmall = heapq.nsmallest(3, list_test)
print(nsmall)
# [1, 2, 4]
print("*"*10)

''' 9._heapify_max
# 构造大根堆
# 注意, 对于数值型数据,一般都不会用到建立大根堆的操作
# 因为将数值取反,建立小根堆,就能完成大根堆的目的
# 举例: list_new = list(map(lambda x : -x, list_test))
'''
heapq._heapify_max(list_test)
print(list_test)
# 同时,如果是用这种方式构造的大根堆,其pop是:
# heapq._heappop_max
# 更为具体的一些写法参考源码: https://github.com/python/cpython/blob/main/Lib/heapq.py
# 大根堆对应的函数并不多,所以在日常中一般自己将数值取反,然后使用小根堆

5.2.4.1 滑动窗口最大值

LeetCode 239.滑动窗口最大值 | | 返回目录5.2.4

思路:滑动窗口是典型的用优先队列的题目。如果直接使用暴力方法,一般都会超时。
这里由于要求窗口内最大的元素,所以要建立大根堆。但是python自带的heapq默认建立的是小根堆,我们只需要把元素取反即可适应它的默认结构,获取结果时记得取反取回来就好

class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
N = len(nums)
if N <= k:
return [max(nums)]

# 暴力求解会超时
# res = []
# for i in range(0,N-k+1):
# res.append(max(nums[i:i+k]))
# return res

import heapq
# 先将第一个宽度为k的窗口内的元素初始化为优先队列
# 记得要将元素值取反去建立小根堆
window = [(-nums[i], i) for i in range(k)]
heapq.heapify(window)
# 小根堆最小的元素,取反之后的原数值就是最大的
res = [-window[0][0]]
# print(window)

# 窗口每挪动一次,就用heappush添加一个数,维护成小根堆
# 需要注意的是,如果堆顶元素,不在当前滑动窗口范围内,要及时pop出去
# i 指向作为滑动窗口的最后一个元素
for i in range(k, N):
# 当前堆顶元素,不在当前窗口 i-k+1 ~ i 内
while window and window[0][1] < i-k+1:
heapq.heappop(window)
# 将 i 位置的数push入
heapq.heappush(window, (-nums[i], i))
res.append(-window[0][0])
'''值得注意的是,堆的规模其实并不一定会维持在k的大小;
因为pop操作只是在堆顶元素不在窗口内时执行;
有一些元素如果已经不在窗口内,但是其并不在堆顶的话,并不影响我们取值;
所以可以留在堆里不用管;
核心是,要保证每一次的堆顶元素,要在考察的窗口内!'''
# print(window)

return res

5.2.4.2 数据流中的第 K 大元素

LeetCode 703.数据流中的第 K 大元素 | | 返回目录5.2.4

思路:因为要返回第K大的元素,所以每新到一个数,我们就需要将其与原来的前k大的元素相比较,然后判断这个新的数是否能够被划入前 k 大的元素中,并可以丢弃那些已经不在前k大范围内的数。
这是一个动态的过程,相当于维护了一个保留前k大数的数组。
python的默认优先队列是小根堆,意思是根节点的值一定比左右子树的值小。
如果设置优先队列的长度为 k , 那么根节点下面就有 k-1 个比它大树,那么根节点恰好就是就是第 k 大 的数。

import heapq
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.__nums = nums
# 先将初始化列表构造为一个小根堆
heapq.heapify(self.__nums)
self.__k = k
def add(self, val: int) -> int:
# 先将新来的元素入队列,形成一个新的小根堆
heapq.heappush(self.__nums, val)
while len(self.__nums) > self.__k:
# 如果队列的数目超过了限定的k
# 那就把根节点的元素弹出,也就是这些数当中最小的那个数弹出
# 剩下的数自然就是较大的k个数
heapq.heappop(self.__nums)
# 根节点正好是第 k 大 的数
return self.__nums[0]

5.2.4.3 数组中的第K个最大元素

LeetCode 215.数组中的第K个最大元素 | | 返回目录5.2.4

思路:该题之前在3-3 排序相关题目3.3.10 已经做过,当时是采用的排序算法,堆方案中也是手写的函数;这里尝试直接使用python自带的优先队列来写code。

写法一:对数组整体应用优先队列,然后再执行k-1次pop操作

import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
'''写法1:使用默认的小根堆函数,所以需要先手动将数组取反'''
# 时间上更快, 空间消耗的反而比自己写heapify操作多一点
nums_new = list(map(lambda x:-x,nums))
heapq.heapify(nums_new)
# 因为是找第 k 个最大数, 所以pop掉k-1个即可
for _ in range(k-1):
heapq.heappop(nums_new)
return -nums_new[0] # 这里要注意之前是将数组元素取反了的,现在要取回来


'''2.如果对函数熟悉的话,直接使用大根堆的函数'''
# 时间和空间的消耗都是最优的
# heapq._heapify_max(nums)
# for _ in range(k-1):
# heapq._heappop_max(nums)
# return nums[0]

写法二:像5.2.2.2那样,维护一个k大小的小根堆

import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# 先初始化一个规模为k的优先队列
tmp = nums[:k].copy()
heapq.heapify(tmp)

for i in range(k,len(nums)):
# 每一次都先push入当前i位置i位置的数字
# 然后对队列执行pop操作,这样使得队列的规模始终是 k
heapq.heappushpop(tmp, (nums[i]))

# 循环结束后, 得到的就是整个数组的前K个最大数
# 堆顶元素就是 第 k 大的数
return tmp[0]

5.2.4.4 前 K 个高频元素

LeetCode 347.前 K 个高频元素 | | 返回目录5.2.4

class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
import heapq
h = {}
# 遍历原始list,时间复杂度 O(N)
for num in nums:
if num not in h:
h[num] = 1
else:
h[num] += 1

N = len(h)
# 频率放在数字前面,这样就能按照频率的大小 构造小根堆
new_nums = [(freq, num) for num, freq in h.items() ]

# 初始化一个 k 规模的小根堆
q = new_nums[:k]
heapq.heapify(q)
# 遍历字典,时间复杂度不超过 O(N),每一次都要恢复成小根堆,时间O(logk)
for i in range(k,N):
# 每一次都先push入当前i位置 的元素
# 然后对队列执行pop操作,这样使得队列的规模始终是 k
heapq.heappushpop(q, new_nums[i])
return [data[1] for data in q]

# 最终时间复杂度应该是 O(N*logk)

5.2.4.5 根据字符出现频率排序

LeetCode 451.根据字符出现频率排序 | | 返回目录5.2.4

思路:这个题和上题很相似,也是先统计频率,然后可以依据频率排序,也可以用优先队列做,两种写法都给出在下面。

class Solution:
def frequencySort(self, s: str) -> str:
h = {}
# 先用哈希表统计字符的频率
for ch in s:
if ch not in h:
h[ch] = 1
else:
h[ch] += 1
'''1.排序后再处理'''
# 生成 (字符,频率)的列表,并依据频率排序 key = lambda x:x[1]
# 这里为简便,直接使用了自带的sort方法;这里如果面试需要的话,可以换成自己现场写的排序code
# tmp = [(k,v) for k,v in h.items()]
# tmp.sort(key = lambda x:x[1], reverse=True)
# res = [ k*v for k,v in tmp]
# return ''.join(res)

'''2.使用优先队列'''
s2 = ''
import heapq
# 依然是根据哈希表,生成(频率,字符)的列表
# 这里将频率放在前面是为了优先队列依据频率数值构造小根堆
tmp = [(v,k) for k,v in h.items()]
heapq.heapify(tmp)
# print(tmp)
while len(tmp)> 0:
# 不断的弹出频率最低的字符和其频率
freq, ch = heapq.heappop(tmp)
# 因为题目要求频率从高到低,后被pop出来的字符,频率是比先被pop出来的更高的
# 即 s2应该放在右侧
s2 = ch*freq + s2
return s2

5.2.4.6 数据流的中位数

LeetCode 295.数据流的中位数 | | 返回目录5.2.4

思路:题目首先给了中位数的定义,是要在有序数组中进行查找。但是并没有保证输入的数据,一定是有序的。
如果按照排序再查找的话,那么每来一个数,就要排序一次,这样时间复杂度会很高。所以不如考虑每来一个数,给它找一个合适的位置放置,即用堆的想法。

这里就可以分为两个堆,一个大根堆,存储小于等于中位数的那一半数字,这样它的堆顶(根节点)就是这一半数字中最接近中位数的那个;一个小根堆,存储大于等于中位数的那一半数字,这样它的堆顶(根节点),也是最接近中位数的那个数。
∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷
只要能维护这两个堆,我们查找中位数就很方便了,只要保持两个堆都各存储一半的数字(偶数个时,规模相等;奇数个时规模相差一),中位数要么就是两个根节点其中之一,要么就是二者的均值。
∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷∷
我们在实际操作中,为了方便起见,始终保持前半部分的数目 大于等于后半部分,
即如果是偶数个,就两部分相等;如果是奇数个,就让前半部分多一个。

import heapq
class MedianFinder:
def __init__(self):
self._left_heap = []
self._right_heap = []
self.__length = 0

def addNum(self, num: int) -> None:
self.__length += 1

# left_heap用来装小于中位数的那部分,且长度为 N//2 或 N//2+1
# right_heap用来装大于中位数的那一部分, 且长度为 N//2

# 如果left部分为空,或者新来的数的数值,小于等于目前的中位数
if len(self._left_heap) == 0 or num <= -self._left_heap[0]:
# 这里对 num取反,是因为该操作默认是维护小根堆,但我们需要该部分数字的关系是大根堆
heapq.heappush(self._left_heap, -num)
# 然后需要判断两个部分的数的规模是否相差在1之内
if len(self._left_heap) > len(self._right_heap) + 1:
# 因为上面是在往left添加元素,所以只可能会出现left比right多的情况
# 此时就把left多出来的pop出来,然后push进right
heapq.heappush(self._right_heap, -heapq.heappop(self._left_heap))

# 如果left不为空,且新来的数大于目前的中位数,就往right添加
else:
heapq.heappush(self._right_heap, num)
# 如果right部分的长度超过了left,就需要pop一个出来,push入left
if len(self._right_heap) > len(self._left_heap):
heapq.heappush(self._left_heap, -heapq.heappop(self._right_heap))


def findMedian(self) -> float:
if len(self._left_heap) > len(self._right_heap):
# 说明此时left部分是 N//2+1个数, right部分是 N//2个数
return -self._left_heap[0]
else:
# 说明left和right部分各有 N//2个数
return (-self._left_heap[0] + self._right_heap[0]) / 2

def print(self):
# 这是自己额外写的一个测试函数,可以查看两个堆里的数的情况
print("self._left_heap: ", self._left_heap)
print("self._right_heap: ", self._right_heap)

5.2.4.7 最接近原点的 K 个点

LeetCode 973.最接近原点的 K 个点 | | 返回目录5.2.4

思路:这个题最在排序部分也总结过,当时用的是排序的方法。
这里也可以使用优先队列来解决。维护一个规模为 k 的堆

import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:


q = [(-x ** 2 - y ** 2, i) for i, (x, y) in enumerate(points[:k])]
heapq.heapify(q)

# 默认的是小根堆,所以要对元素值取反
# 且元素是 d距离放在索引i前面, 这样会根据 d 的数值来进行堆的构造
N = len(points)
for i in range(k, N):
x, y = points[i]
d = -(x ** 2 + y ** 2)
heapq.heappushpop(q, (d, i))

res = [points[idx] for (_, idx) in q]

return res