递归是什么,粗略来说,就是当以计算依赖上一步的结果。
只有完成上一步的计算,才能进行当前的计算操作,步步依赖,直到最开始的明确的值。
现在以leecode230来讲述一遍
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
很显然,只要经过中序遍历,然后取对应数组的第 k − 1 k - 1 k−1个元素即可。
前序遍历: r o o t → l e f t → r i g h t root \rightarrow left \rightarrow right root→left→right
中序遍历: l e f t → r o o t → r i g h t left \rightarrow root \rightarrow right left→root→right
后序遍历: l e f t → r i g h t → r o o t left \rightarrow right \rightarrow root left→right→root
其中所谓的
序
,对应的其实是root
的位置,别记错了哦。
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
container = []
def collect(node: TreeNode):
# 空节点, 不操作
if node is None:
return
# 叶子节点, 直接操作
if (node.left is None) and (node.right is None):
container.append(node.val)
else:
# 先左边
collect(node.left)
# 具体操作
container.append(node.val)
# 后右边
collect(node.right)
collect(root)
return container[k - 1]
不仅展现递归,它还告诉我们一些规律
递归必须有边界,它对应具体的计算或操作,甚至是直接的答案(斐波那契数列)。
每一步的计算总是依赖于下一步,需要制定的是其中的关系。
依赖分为计算依赖和流程依赖,斐波那契属于计算依赖,而这个案例,仅仅是流程依赖。
具体的操作之中并不依赖于之前的计算。
先来复习一下栈的特性:单口出入。
class Stack(object):
def __init__(self):
self.container = []
def empty(self):
return len(self.container) == 0
def push(self, item):
self.container.append(item)
def pop(self):
if self.empty():
return None
return self.container.pop(-1)
def top(self):
return self.container[-1]
leecode20
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
class Solution:
def isValid(self, s: str) -> bool:
mapping = {'}': '{', ']': '[', ')': '('}
stack = Stack()
for item in s:
# 空字符有效
if ' ' == item:
continue
if item in mapping:
# 一开头就错
if stack.empty():
return False
# 如果是右半截,必定有对应弹出
if mapping[item] == stack.top():
stack.pop()
# 无对应,直接返回
else:
return False
else:
stack.push(item)
# 如果有剩余, 左边多
return stack.empty()
本质上,递归
就是用栈
实现的,因为栈只有一个出口,我们每次计算都只能是栈口
的数据。
同时,栈口的数据可以和栈顶的数据进行互动,不停的叠加,也就完成了每一步的逼近。
而那些存在依赖的延时计算,可以先压入栈中,等到轮到它计算的时候,前置的依赖已经准备好了。
只是,关键的是我们能不能对一个问题抽象出递归的思路。
leecode739
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
直接解法,存在重复计算,一个待定的数值,我们为何要重复计算多次呢。
按照这个思路,我们可以进行直接计算,得出如下版本
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
result = [0 for _ in range(len(T))]
unknown = []
for item in enumerate(T):
# 第一次肯定不知道
if len(unknown) == 0:
unknown.append(item)
continue
# 最后一个总是不知道的
last = -1
while -len(unknown) <= last < 0:
someday = unknown[last]
# 判断新的是否大于之前未知的
if item[1] > someday[1]:
result[someday[0]] = item[0] - someday[0]
del unknown[last]
else:
# 后续添加的一定比之前的小
break
# 新加的肯定不知道
unknown.append(item)
return result
该抓住的都抓住了,唯一的关键点就是并没有理解到递归的思想。
之后添加未知的必定是温度小于之前的,也就是说,要想比对后面的,必须比对之前的。
这里采用的-1
,并没有递归的精髓所在,而仅仅是对于unknow
的去除,对数组操作的必要性。
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
result = [0 for _ in range(len(T))]
unknown = Stack()
for item in enumerate(T):
if unknown.empty():
unknown.push(item)
continue
while not unknown.empty():
top = unknown.top()
if item[1] > top[1]:
result[top[0]] = item[0] - top[0]
unknown.pop()
else:
break
unknown.push(item)
对于代码的简洁,你会说是因为使用list
模拟stack
的原因,这只是其中一方面。
使用list
模拟stack
固然会增加操作,但是,上一种方法只是想如何更好的移除那些已知的未知。
就数据结构的选择,一开始选择list
就是错误的,递归当然是用stack
,上面只是误打误撞的相似了。
刨除数据结构的选择,关键在于递归思想
的差异,相同的做法,思想不同,价值不同,因为,思想是可以迁移的。
来看看leecode239
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
暴力做法就不说了,我们来说说双端队列的条件递归。
筛选一个最大值,其实就是这样
def max(*args):
maxValue = args[0]
for value in args[1:]:
if value > maxValue:
maxValue = value
恩,单出口的重复,当然可以使用递归
def max(*args):
stack = Stack()
for value in args:
if stack.empty():
stack.push(value)
continue
if value > stack.top():
stack.pop()
stack.push(value)
return stack.pop()
好像有点蠢,但是本质是相同的,尤其是在非单元素筛选当中,这种做法绝对是更好的。
该题目,重点就在于两点
也就是窗口外,其他场景下,更多的是以时间为窗口。
值大小的判断,就不用赘述了。
其中隐藏的最重要的一点,就是有效最大值的筛选。为了这一点,必须保留选举值。
尤其是,你保留的选举值,必须有效,必定有效。
整道题目,本质就是在进行有效候选值的最大值筛选,重点就是如何避免重复比对。
使用单个的值,肯定无法完成任务,除非递增或者递减。
如果使用list
,我们又不是全部记录,特殊操作显得多余。
思考stack
,我们只是需要压入有效最大值,同时保证移除无效最大值就好了。
不过出口只有一个诶,我们需要漏底。
class Stack(object):
def __init__(self, limit):
self.limit = limit
self.container = []
def max(self):
return self.container[0]
'''
验证,保证有效
'''
def valid(self, index):
if index - self.container[0][0] >= self.limit:
self.container.pop(0)
'''
压入有效最大值,移除有效小值
'''
def push(self, item):
while len(self.container) > 0:
if self.container[-1][1]< item[1]:
self.container.pop(-1)
else:
break
self.container.append(item)
self.valid(item[0])
这里我把漏底的功能让stack
内部维护了,也就是valid
验证数据有效性,它的方向是刚好相反的。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
stack = Stack(k)
result = []
for item in enumerate(nums):
stack.push(item)
if item[0] < k - 1:
continue
result.append(stack.max()[1])
return result
最大值,其实一直都是栈底那个,并且保证实时更新,后续的都是候选的有效最大值。
只有后续有效的最大值大于栈底的最大值,或者栈底最大值过期,否则一直是栈底最大,不必更新。
更重要的是,它在堆栈方向是有序的,我们从来不用考虑筛选的问题。
它是双端队列,但是这种场景下,我更喜欢把它当做可以漏底
のstack
。