递归与分治

大问题化小,小到可直接求解。 递归是函数调用自身,把「当前问题」归结为「规模更小、结构相同」的子问题,直到达到基线条件(最小情况)直接返回。分治(divide and conquer)则先分解(把问题拆成若干子问题)、再解决子问题(通常递归)、最后合并子结果。归并排序、快速排序都是分治;快速幂、汉诺塔、最大子数组和等也是经典例子。本章讲递归思维、分治框架、归并排序回顾、快速幂与汉诺塔,以及至少三道例题的 Python 实现(类型标注与注释)。

一、递归思维

写递归时抓住两点:基线条件(何时不再递归,直接给出结果)和递归关系(当前结果如何由更小规模的结果得到)。例如阶乘:fact(0)=1;fact(n)=n·fact(n-1)。递归调用会形成调用栈,层数过深可能栈溢出;可改为迭代或尾递归优化(Python 未做尾递归优化)。

递归调用形成树状结构,先递到底再归回来
def fact(n: int) -> int:
    """阶乘:基线 n<=0 返回 1;递归 fact(n)=n*fact(n-1)。"""
    if n <= 0:
        return 1
    return n * fact(n - 1)


def fib_naive(n: int) -> int:
    """斐波那契(仅演示递归):F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)。存在大量重复计算,应改用记忆化或递推。"""
    if n <= 0:
        return 0
    if n == 1:
        return 1
    return fib_naive(n - 1) + fib_naive(n - 2)

二、分治策略

分治三步骤:分解(divide):将问题拆成若干规模更小的子问题;解决(conquer):递归求解子问题(若子问题足够小则直接解);合并(combine):将子问题的解合并为原问题的解。分治通常用递归实现,子问题之间相互独立(无重叠)时效率高;若子问题有重叠,则适合用动态规划(下一章)避免重复计算。

分治:原问题拆成子问题,分别求解后合并

三、归并排序回顾(分治典范)

归并排序(第 12 章):将数组成两半,两半的排序(递归),并两个有序段。递推式 T(n)=2T(n/2)+O(n),由主定理得 O(n log n)。子问题互不重叠,是典型分治。

四、快速幂(分治)

计算 xn(n 为非负整数):若 n=0 则结果为 1;若 n 为偶则 xn = (xn/2)2;若 n 为奇则 xn = x·(x(n-1)/2)2。每次 n 减半,O(log n) 次乘法。若 n 为负数,可转为 (1/x)-n(x≠0)。

def pow_recursive(x: float, n: int) -> float:
    """
    快速幂:x^n,n 非负。n=0 返回 1;n 偶则 (x^(n//2))^2,奇则 x * (x^(n//2))^2。
    """
    if n == 0:
        return 1.0
    half = pow_recursive(x, n // 2)
    if n % 2 == 0:
        return half * half
    return x * half * half


def pow_iterative(x: float, n: int) -> float:
    """快速幂迭代:将 n 视为二进制,若第 k 位为 1 则乘上 x^(2^k)。"""
    if n < 0:
        x, n = 1.0 / x, -n
    res = 1.0
    base = x
    while n:
        if n & 1:
            res *= base
        base *= base
        n //= 2
    return res

五、汉诺塔

汉诺塔:三根柱子 A、B、C,A 上有 n 个从大到小叠放的圆盘。要求每次移一个盘、且大盘不能压小盘,把 A 上的盘全部移到 C。递归思路:要把 n 个盘从 A 移到 C,可先把上面 n-1 个从 A 经 C 移到 B,再把最大的从 A 移到 C,最后把 n-1 个从 B 经 A 移到 C。移动次数为 2n-1。

三柱 n 盘:递归移动 n-1 盘到辅助,移最大盘,再移 n-1 盘到目标
from typing import List


def hanoi(n: int, from_rod: str, to_rod: str, aux_rod: str, moves: List[str]) -> None:
    """
    汉诺塔:将 n 个盘从 from_rod 移到 to_rod,辅助柱为 aux_rod。
    步骤记录到 moves:每步为 "A->C" 这样的字符串。
    """
    if n == 0:
        return
    hanoi(n - 1, from_rod, aux_rod, to_rod, moves)
    moves.append(f"{from_rod}->{to_rod}")
    hanoi(n - 1, aux_rod, to_rod, from_rod, moves)

六、最大子数组和(分治)

求连续子数组的最大和。分治:取中点 mid,最大和要么在左半、要么在右半、要么跨越 mid。跨越 mid 时从中点向左、向右各扩展取最大前缀和与最大后缀和,相加即跨中点的最大和。三者取 max。T(n)=2T(n/2)+O(n),O(n log n)。(也可用 DP 或贪心 O(n),见后续章节。)

from typing import List


def max_subarray_sum_divide(arr: List[int], left: int, right: int) -> int:
    """
    分治求 [left, right) 内最大子数组和。若区间为空或单元素直接返回。
    否则 mid=(left+right)//2,取 左半最大、右半最大、跨越 mid 的最大 三者之 max。
    """
    if right - left <= 0:
        return 0
    if right - left == 1:
        return max(0, arr[left])
    mid = (left + right) // 2
    left_max = max_subarray_sum_divide(arr, left, mid)
    right_max = max_subarray_sum_divide(arr, mid, right)
    # 跨越 mid:从 mid 向左的最大后缀和 + 从 mid 向右的最大前缀和
    suffix = 0
    cur = 0
    for i in range(mid - 1, left - 1, -1):
        cur += arr[i]
        suffix = max(suffix, cur)
    prefix = 0
    cur = 0
    for i in range(mid, right):
        cur += arr[i]
        prefix = max(prefix, cur)
    cross = suffix + prefix
    return max(left_max, right_max, cross)

七、小结

递归要明确基线条件与递归关系,注意栈深度。分治分解→解决→合并,子问题独立时高效;归并、快排、快速幂、汉诺塔、最大子数组和都是典型。下一章讲动态规划,处理重叠子问题。

八、例题

以下例题使用 Python 3 类型标注与注释。

例题 1:Pow(x, n)

题目描述:实现 pow(x, n),即 x 的 n 次方。n 可为负数;-231 ≤ n ≤ 231-1。

def my_pow(x: float, n: int) -> float:
    """快速幂:n 为负时用 1/x 和 -n;迭代写法避免递归栈。"""
    if n < 0:
        x, n = 1.0 / x, -n
    res = 1.0
    base = x
    while n:
        if n & 1:
            res *= base
        base *= base
        n >>= 1
    return res

讲解:n 的二进制每一位对应 base 的幂,从低到高若该位为 1 则乘入 res;每次 base 自乘得到 x^(2^k)。时间 O(log n),空间 O(1)。

例题 2:多数元素(分治)

题目描述:数组 nums 中存在出现次数超过 ⌊n/2⌋ 的元素(多数元),找出它。假设总存在。

思路:分治:若左右两半的多数元相同则其为全局多数元;若不同则分别统计两半的多数元在全局中的出现次数,取较多者。也可用 Boyer-Moore 投票 O(n) 空间 O(1)。

from typing import List


def majority_element(nums: List[int]) -> int:
    """分治:区间 [l, r) 的多数元,若左右多数元相同则返回;否则比较两者在区间内的出现次数。"""

    def count_in_range(val: int, l: int, r: int) -> int:
        c = 0
        for i in range(l, r):
            if nums[i] == val:
                c += 1
        return c

    def majority(l: int, r: int) -> int:
        if r - l == 1:
            return nums[l]
        mid = (l + r) // 2
        left_maj = majority(l, mid)
        right_maj = majority(mid, r)
        if left_maj == right_maj:
            return left_maj
        return left_maj if count_in_range(left_maj, l, r) > count_in_range(right_maj, l, r) else right_maj

    return majority(0, len(nums))

讲解:子区间多数元若相同必为全局多数元;若不同则两候选在区间内各数一遍,多的为多数元。T(n)=2T(n/2)+O(n),时间 O(n log n)。

例题 3:合并 K 个升序链表(分治合并)

题目描述:k 个升序链表,合并成一个升序链表。分治:两两合并,再对合并结果继续两两合并,直到剩一个。

from typing import List, Optional


class ListNode:
    def __init__(self, val: int = 0, next: Optional["ListNode"] = None) -> None:
        self.val = val
        self.next = next


def merge_two(l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
    """合并两个有序链表。"""
    dummy = ListNode(0)
    p = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            p.next = l1
            l1 = l1.next
        else:
            p.next = l2
            l2 = l2.next
        p = p.next
    p.next = l1 or l2
    return dummy.next


def merge_k_lists(lists: List[Optional[ListNode]]) -> Optional[ListNode]:
    """分治:合并 lists[l..r-1]。若 l==r 返回 None,l+1==r 返回 lists[l];否则 mid 分两半递归合并再 merge_two。"""
    def merge_range(l: int, r: int) -> Optional[ListNode]:
        if l >= r:
            return None
        if l + 1 == r:
            return lists[l]
        mid = (l + r) // 2
        return merge_two(merge_range(l, mid), merge_range(mid, r))
    return merge_range(0, len(lists))

讲解:每轮将前 half 与后 half 两两合并,结果写回前 half,轮数 O(log k),每轮总合并代价 O(N)(N 为总节点数),总时间 O(N log k)。

例题 4:不同的二叉搜索树 II(递归构造)

题目描述:给定整数 n,生成所有由 1..n 为节点组成的、结构不同的二叉搜索树。

思路:枚举根 k(1..n),左子树由 [1,k-1] 生成的所有 BST,右子树由 [k+1,n] 生成的所有 BST,组合成以 k 为根的所有 BST。

from typing import List, Optional


class TreeNode:
    def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None) -> None:
        self.val = val
        self.left = left
        self.right = right


def generate_trees(n: int) -> List[Optional[TreeNode]]:
    """返回 [1..n] 构成的所有不同 BST。若 start>end 返回 [None]。"""

    def build(start: int, end: int) -> List[Optional[TreeNode]]:
        if start > end:
            return [None]
        res: List[Optional[TreeNode]] = []
        for k in range(start, end + 1):
            lefts = build(start, k - 1)
            rights = build(k + 1, end)
            for L in lefts:
                for R in rights:
                    root = TreeNode(k)
                    root.left = L
                    root.right = R
                    res.append(root)
        return res

    return build(1, n) if n else []

讲解:递归构造 [start, end] 的所有 BST:枚举根 k,左子树列表与右子树列表笛卡尔积得到以 k 为根的所有树。时间与生成的树数量相关,空间 O(递归深度)。