遞歸
遞歸是什麼,粗略來說,就是當以計算依賴上一步的結果。
只有完成上一步的計算,才能進行當前的計算操作,步步依賴,直到最開始的明確的值。
現在以leecode230來講述一遍
給定一個二叉搜索樹,編寫一個函數 kthSmallest 來查找其中第 k 個最小的元素。
說明:
你可以假設 k 總是有效的,1 ≤ k ≤ 二叉搜索樹元素個數。
很顯然,只要經過中序遍歷,然後取對應數組的第個元素即可。
前序遍歷:
中序遍歷:
後序遍歷:
其中所謂的
序
,對應的其實是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]
給定一個只包括 '(',')','{','}','[',']' 的字符串,判斷字符串是否有效。
有效字符串需滿足:
左括號必須用相同類型的右括號閉合。
左括號必須以正確的順序閉合。
注意空字符串可被認爲是有效字符串。
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()
棧和遞歸
本質上,遞歸
就是用棧
實現的,因爲棧只有一個出口,我們每次計算都只能是棧口
的數據。
同時,棧口的數據可以和棧頂的數據進行互動,不停的疊加,也就完成了每一步的逼近。
而那些存在依賴的延時計算,可以先壓入棧中,等到輪到它計算的時候,前置的依賴已經準備好了。
只是,關鍵的是我們能不能對一個問題抽象出遞歸的思路。
請根據每日 氣溫 列表,重新生成一個列表。對應位置的輸出爲:要想觀測到更高的氣溫,至少需要等待的天數。如果氣溫在這之後都不會升高,請在該位置用 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
。