算法-递归与栈,延时计算

文章目录

  • 递归
  • 栈和递归
  • 双端队列的条件递归

递归

递归是什么,粗略来说,就是当以计算依赖上一步的结果。

只有完成上一步的计算,才能进行当前的计算操作,步步依赖,直到最开始的明确的值。

现在以leecode230来讲述一遍

给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。

说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

很显然,只要经过中序遍历,然后取对应数组的第 k − 1 k - 1 k1个元素即可。

前序遍历: r o o t → l e f t → r i g h t root \rightarrow left \rightarrow right rootleftright

中序遍历: l e f t → r o o t → r i g h t left \rightarrow root \rightarrow right leftrootright

后序遍历: l e f t → r i g h t → r o o t left \rightarrow right \rightarrow root leftrightroot

其中所谓的,对应的其实是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

你可能感兴趣的:(算法)