【小小福讲算法】硅谷工程师十五分钟带你深入理解 Recursion (递归)算法,及其衍生出的算法(分治算法Divide and Conquer, 回溯 Backtracking)
标签
编程 算法 递归 LeetCode 技术 核心 油管链接🔗: https://www.youtube.com/watch?v=AqGagBmFXgw&t=70s
-
Recursion
-
为什么 Function 需要 Call 自己?
-
因为大问题(Big problem)可以分解成小问题(Sub problems)
-
小问题通常很容易解答!
-
e.g. 跑步 10 km 很难,但是跑步 10 km = 跑步 9 km + 先跑 1 km,所以后一步不是个艰难的任务…
-
递归函数的结构模版
-
终止条件(#Base_Case) + 递归关系(#Recursion_Relation)
def foo( arguments ): Base Case Recursion Relation -
Recursion 在计算机中的实现方式
-
任何一个函数调用,计算机会在内存中生成一块区域,用于存放函数的参数,返回地址等,这块区域叫做“栈(Stack)“
-
递归函数也是一种函数调用 ⇒ 因而也会生成一系列的 stack
step-1: 跑步函数(3km) call 跑步函数(2km) call 跑步函数(1km) call 跑步函数(0km) step-2: 跑步函数(0km)back & release 跑步函数(1km)back & release 跑步函数(2km)back & release 跑步函数(3km) -
例题讲解
例题 LeetCode 700. Search in a Binary Search Tree
# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def searchBST(self, root: Optional[TreeNode], val: int) -\> Optional[TreeNode]: # base case if not root: return None if root.val == val: return root # recursion relation if val \> root.val: return self.searchBST(root.right, val) else: return self.searchBST(root.left, val)/** * Definition for a binary tree node. * public class TreeNode { * public var val: Int * public var left: TreeNode? * public var right: TreeNode? * public init() { self.val = 0; self.left = nil; self.right = nil; } * public init(_ val: Int) { self.val = val; self.left = nil; self.right = nil; } * public init(_ val: Int, _ left: TreeNode?, _ right: TreeNode?) { * self.val = val * self.left = left * self.right = right * } * } */ class Solution { func searchBST(_ root: TreeNode?, _ val: Int) -\> TreeNode? { // Base code guard let root = root, root.val != val else { return root } // recursion relation if root.val \> val { return self.searchBST(root.left, val) } else { return self.searchBST(root.right, val) } } } -
递归的三种形式
-
Memorization 缓存
- 将计算结果保存,避免重复计算
-
Divide and conquer 分治
- 将一个大问题分解成小问题,各个击破,然后将小问题的解组合起来
-
Backtracking 回溯
- 不断试错,知错就改
- 类似于暴力搜索,但是比暴力搜索高效(因为知错就改)
-
递归问题的形式(一)
-
如果递归问题会产生重复的子问题,那么可以使用 cache 记住子问题的答案,避免重复计算。
# Initialize cache (which is essentially a dictionary) cache: Dict[arg, value] = dict() # 初始化 cache def foo(arg): # check answer if already exists if arg in cache: return cache[arg] # 直接返回 cache 答案,如果答案存在 # Base # Recursion Relation # Save the answer cache[arg] = result # 将计算的答案写入 cache -
例题 Memorization
heading:: 2 LeetCode 509: Fibonacci-number !#Pasted_image_20231024113314.png
class Solution { var cache = [Int: Int]() func fib(_ n: Int) -\> Int { if let c = cache[n] { return c } var result = 0 if n \< 2 { result = n } else { result = fib(n - 1) + fib(n - 2) } cache[n] = result return result } } -
递归问题的形式(二)Divide and Conquer 分治
heading:: 2 Divide and Conquer 分而治之,几乎等同于标准的 Recursion,唯一的区别是,最后需要将子问题的结果进行合并!
> 1. Divide. Divide the Problem S into a set of subproblems: {S1, S2,…Sn} where n >= 2, i.e. there are usually more than one subproblem > > 2. Conquer. Solve each subproblem recursively > > 3. Combine. Combine the results of each subproblem.
-
例题 Divide and Conquer
heading:: 2 LeetCode 98: Validate Binary Search Tree
class Solution: def isValidBST(self, root: TreeNode) -\> bool: def validate(node, low=-match.inf, high=math.inf): # Empty trees are valid BSTs if not node: return True # The current node's value must be between low and high. if node.val \<= low or node.val \>= high: return False # The left and right subtree must also be valid. return (validate(node.right), node.val, high) and validate(node.left, low, node.val)) return validate(root) -
递归问题的形式(二)Divide and Conquer 的代码模板
heading:: 2
def divide_and_conquer( S ): # (1). Divide the problem into a set of subproblems. [S1, S2, ... Sn] = divide(S) # (2). Solve the subproblem recursively, # obtain the results of subproblems as [R1, R2, ... Rn]. rets = [divide_and_conquer(Si) for Si in [S1, S2, ... Sn]] [R1, R2, ... Rn] = rets # (3). combine the results from the subproblems. # and return the combine result. return combine([R1, R2, ... Rn]) -
递归问题的形式(三)Backtracking 回溯
heading:: 2 回溯的问题的形式:通常要求寻找满足所有xxx条件的结果,并且问题可以使用递归实现。
个人理解:一步一步先前探索,每一步尝试所有的可能,一旦发现当前不是可行解,立即停止(知错就改)
-
例题 LeetCode 22: 找出所有由 n 个左右括号组成的有效的表达式
heading:: 2
class Solution { func generateParenthesis(_ n: Int) -\> [String] { var res: [String] = [] func backtrack(left: Int, right: Int, s: String) { // find solution: // left == right && left == n * 2 if s.count == n * 2 { res.append(s) return } // try and given or back if left \< n { backtrack(left: left + 1, right: right, s: s + "(") } if left \> right { backtrack(left: left, right: right + 1, s: s + ")") } } backtrack(left: 0, right: 0, s: "") return res } } -
Backtracking 问题的模板
heading:: 2
def backtrack(candidate): if find_solution(candidate): ouput(candidate) return # iterate all possible candidates. for next_candidate in list_of_candidates: if is_valid(next_candidate): # try this partial candidate solution place(next_candidate) # given the candidate, explore further. backtrack(next_candidate) # backtrack remove(next_candidate) -
写在最后
heading:: 2
-
递归是一种非常 intuitive 的解决复杂问题的方法
-
递归分两步:Base Case + Recursion Relation
-
递归遇到重复计算的情况 ⇒ 使用 memorization
-
递归遇到子问题结果组合的情况 ⇒ 使用 divide-and- conquer
-
递归要求返回所有满足条件的解答 ⇒ 使用 backtracking