算法-遞歸與棧,延時計算

遞歸

遞歸是什麼,粗略來說,就是當以計算依賴上一步的結果。

只有完成上一步的計算,才能進行當前的計算操作,步步依賴,直到最開始的明確的值。

現在以leecode230來講述一遍

給定一個二叉搜索樹,編寫一個函數 kthSmallest 來查找其中第 k 個最小的元素。

說明:
你可以假設 k 總是有效的,1 ≤ k ≤ 二叉搜索樹元素個數。

很顯然,只要經過中序遍歷,然後取對應數組的第k1k - 1個元素即可。

前序遍歷: rootleftrightroot \rightarrow left \rightarrow right

中序遍歷:leftrootrightleft \rightarrow root \rightarrow right

後序遍歷: leftrightrootleft \rightarrow right \rightarrow 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章