分治算法完全指南

前置知识

建议先掌握递归算法,分治是递归的重要应用。预计学习时间 1-2 周。

一、什么是分治算法? 🎯

分治算法(Divide and Conquer)是一种将复杂问题分解为更小的相同子问题,分别解决后再合并结果的算法设计范式。

核心思想

分治 vs 其他策略

策略 子问题关系 典型算法 适用场景
分治 独立 归并排序、快速排序 子问题独立
动态规划 重叠 背包问题、最短路径 子问题重叠
贪心 逐步选择 Dijkstra、霍夫曼编码 满足贪心性质
回溯 搜索空间 N皇后、数独 组合优化

关键区别

二、分治算法模板 📐

通用框架

分治算法模板
def divide_and_conquer(problem):
    """
    分治算法通用模板
    :param problem: 待解决的问题
    :return: 问题的解
    """
    # 1. 基本情况:问题足够小,直接求解
    if is_base_case(problem):
        return solve_base_case(problem)
    
    # 2. 分解:将问题分解为子问题
    sub_problems = divide(problem)
    
    # 3. 解决:递归求解子问题
    sub_results = [divide_and_conquer(p) for p in sub_problems]
    
    # 4. 合并:合并子问题的解
    result = combine(sub_results)
    
    return result

模板解析

四个关键步骤
1. 判断基本情况:问题是否小到可以直接解决 2. 分解问题:如何拆分成更小的子问题 3. 递归求解:对每个子问题调用自身 4. 合并结果:如何将子问题的解组合起来 这四步构成了分治算法的完整流程。

三、经典分治算法 🏆

1. 归并排序

归并排序是分治算法的经典代表,完美体现了"分-治-合"的思想。

归并排序实现
def merge_sort(arr):
    """
    归并排序 - 分治算法
    :param arr: 待排序数组
    :return: 排序后的数组
    """
    # 基本情况:数组长度为 0 或 1
    if len(arr) <= 1:
        return arr
    
    # 分解:找到中点,分成两半
    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]
    
    # 解决:递归排序两半
    left_sorted = merge_sort(left)
    right_sorted = merge_sort(right)
    
    # 合并:合并两个有序数组
    return merge(left_sorted, right_sorted)

def merge(left, right):
    """合并两个有序数组"""
    result = []
    i = j = 0
    
    # 比较两个数组的元素,按顺序放入结果
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    # 处理剩余元素
    result.extend(left[i:])
    result.extend(right[j:])
    
    return result

# 测试
arr = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(arr))  # [3, 9, 10, 27, 38, 43, 82]

性能分析

2. 快速排序

快速排序也是分治算法,但它的"合并"步骤非常简单(不需要额外操作)。

快速排序实现
def quick_sort(arr, low=0, high=None):
    """
    快速排序 - 分治算法
    :param arr: 待排序数组
    :param low: 起始索引
    :param high: 结束索引
    :return: 排序后的数组
    """
    if high is None:
        high = len(arr) - 1
    
    if low < high:
        # 分解:分区操作
        pivot_index = partition(arr, low, high)
        
        # 解决:递归排序左右两部分
        quick_sort(arr, low, pivot_index - 1)
        quick_sort(arr, pivot_index + 1, high)
    
    return arr

def partition(arr, low, high):
    """分区操作:将小于基准的放左边,大于基准的放右边"""
    pivot = arr[high]  # 选择最后一个元素作为基准
    i = low - 1
    
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    
    # 将基准放到正确位置
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

# 测试
arr = [10, 7, 8, 9, 1, 5]
print(quick_sort(arr))  # [1, 5, 7, 8, 9, 10]

性能对比

O(n log n)
平均时间
O(n²)
最坏时间
O(log n)
空间复杂度
快速排序的最坏情况

当数组已经有序且每次都选最后一个元素作为基准时,快速排序退化为 O(n²)。

优化方法

3. 二分搜索

二分搜索是分治思想的简化版,每次将搜索范围减半。

python
def binary_search(arr, target, low=0, high=None):
    """
    二分搜索 - 分治算法
    :param arr: 有序数组
    :param target: 目标值
    :param low: 左边界
    :param high: 右边界
    :return: 目标值的索引,未找到返回 -1
    """
    if high is None:
        high = len(arr) - 1
    
    # 基本情况:搜索范围为空
    if low > high:
        return -1
    
    # 分解:计算中间位置
    mid = low + (high - low) // 2
    
    # 解决:根据比较结果决定搜索哪一半
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        # 搜索右半部分
        return binary_search(arr, target, mid + 1, high)
    else:
        # 搜索左半部分
        return binary_search(arr, target, low, mid - 1)

# 测试
arr = [1, 3, 5, 7, 9, 11, 13]
print(binary_search(arr, 7))   # 3
print(binary_search(arr, 6))   # -1

时间复杂度:O(log n),每次减半

4. 大整数乘法(Karatsuba 算法)

传统乘法需要 O(n²) 时间,Karatsuba 算法通过分治将复杂度降到 O(n^1.585)。

Karatsuba 大整数乘法
def karatsuba(x, y):
    """
    Karatsuba 大整数乘法
    :param x, y: 两个大整数
    :return: 乘积
    """
    # 基本情况:数字足够小,直接相乘
    if x < 10 or y < 10:
        return x * y
    
    # 计算数字位数
    n = max(len(str(x)), len(str(y)))
    m = n // 2
    
    # 分解:将数字分成高位和低位
    high_x, low_x = divmod(x, 10**m)
    high_y, low_y = divmod(y, 10**m)
    
    # 解决:递归计算三个乘积
    z0 = karatsuba(low_x, low_y)
    z1 = karatsuba(high_x + low_x, high_y + low_y)
    z2 = karatsuba(high_x, high_y)
    
    # 合并:组合结果
    return z2 * 10**(2*m) + (z1 - z2 - z0) * 10**m + z0

# 测试
print(karatsuba(1234, 5678))  # 7006652
print(1234 * 5678)             # 7006652(验证)

优势:对于非常大的整数,比传统乘法快得多

四、分治算法的性能分析 📊

主定理(Master Theorem)

主定理用于分析分治算法的时间复杂度。

对于递归式:T(n) = aT(n/b) + f(n)

其中:

主定理三种情况
情况 1:f(n) = O(n^(log_b(a) - ε)) → T(n) = Θ(n^log_b(a)) 情况 2:f(n) = Θ(n^log_b(a)) → T(n) = Θ(n^log_b(a) log n) 情况 3:f(n) = Ω(n^(log_b(a) + ε)) → T(n) = Θ(f(n))

实例分析

算法 递归式 a b f(n) 时间复杂度
归并排序 T(n) = 2T(n/2) + O(n) 2 2 O(n) O(n log n)
快速排序 T(n) = 2T(n/2) + O(n) 2 2 O(n) O(n log n) 平均
二分搜索 T(n) = T(n/2) + O(1) 1 2 O(1) O(log n)
Karatsuba T(n) = 3T(n/2) + O(n) 3 2 O(n) O(n^1.585)

五、分治的应用场景 💼

场景一:大规模数据处理

MapReduce 框架本质上就是分治思想:

场景二:计算机图形学

场景三:数值计算

FFT 简化概念
def fft_basic(concept_only):
    """
    快速傅里叶变换(概念示例)
    将 DFT 分解为偶数和奇数部分
    
    X[k] = E[k] + W_N^k * O[k]
    X[k+N/2] = E[k] - W_N^k * O[k]
    
    其中:
    - E[k] 是偶数部分的 DFT
    - O[k] 是奇数部分的 DFT
    - W_N^k 是旋转因子
    """
    pass  # 实际实现较复杂,此处仅展示思路

场景四:最近点对问题

问题:在平面上的 n 个点中,找出距离最近的两个点。

暴力解法:O(n²),检查所有点对

分治解法:O(n log n)

最近点对问题(简化版)
import math

def closest_pair(points):
    """
    最近点对问题 - 分治算法
    :param points: 点列表 [(x, y), ...]
    :return: 最近距离
    """
    # 按 x 坐标排序
    points.sort()
    
    def divide_and_conquer(pts):
        n = len(pts)
        
        # 基本情况:点数少,暴力计算
        if n <= 3:
            min_dist = float('inf')
            for i in range(n):
                for j in range(i+1, n):
                    dist = math.sqrt((pts[i][0]-pts[j][0])**2 + 
                                   (pts[i][1]-pts[j][1])**2)
                    min_dist = min(min_dist, dist)
            return min_dist
        
        # 分解:按 x 坐标分成两半
        mid = n // 2
        mid_point = pts[mid]
        
        # 解决:递归求解左右两部分
        left_min = divide_and_conquer(pts[:mid])
        right_min = divide_and_conquer(pts[mid:])
        
        # 合并:检查跨越中线的点对
        min_dist = min(left_min, right_min)
        
        # 只考虑中线附近 strip 区域内的点
        strip = [p for p in pts if abs(p[0] - mid_point[0]) < min_dist]
        strip.sort(key=lambda p: p[1])  # 按 y 排序
        
        # 检查 strip 中的点对(最多检查 7 个后续点)
        for i in range(len(strip)):
            for j in range(i+1, min(i+8, len(strip))):
                if strip[j][1] - strip[i][1] >= min_dist:
                    break
                dist = math.sqrt((strip[i][0]-strip[j][0])**2 + 
                               (strip[i][1]-strip[j][1])**2)
                min_dist = min(min_dist, dist)
        
        return min_dist
    
    return divide_and_conquer(points)

# 测试
points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)]
print(f"最近距离: {closest_pair(points):.2f}")

六、分治的优缺点 ⚖️

优点:并行性好

分治算法的子问题相互独立,天然适合并行计算。

应用场景

  • 多核 CPU 并行处理
  • GPU 大规模并行
  • 分布式计算集群
优点:效率高

对于许多问题,分治能显著降低时间复杂度。

例子

  • 排序:从 O(n²) 降到 O(n log n)
  • 乘法:从 O(n²) 降到 O(n^1.585)
  • 搜索:从 O(n) 降到 O(log n)
缺点:递归开销

递归调用有额外的函数调用开销和栈空间消耗。

优化方法

  • 小问题使用迭代
  • 尾递归优化(某些语言支持)
  • 手动管理栈
缺点:实现复杂

相比简单算法,分治的实现通常更复杂。

建议

  • 先写伪代码理清思路
  • 从小规模开始测试
  • 添加详细的注释

七、分治 vs 动态规划 🔍

虽然都使用递归,但两者有本质区别:

特性 分治 动态规划
子问题关系 独立 重叠
是否需要记忆化
典型问题 归并排序、快速排序 背包问题、最短路径
时间复杂度 通常较低 可能较高(但避免重复)
空间复杂度 O(log n) 栈空间 O(n) 表格空间
判断标准

问自己:子问题是否会被重复计算?

八、练习与挑战 🏆

推荐练习题

LeetCode 53. 最大子数组和

给定一个整数数组,找到一个具有最大和的连续子数组。 难度:⭐ 中等 可使用分治或 DP

LeetCode 23. 合并K个升序链表

合并 k 个升序链表为一个升序链表。 难度:⭐⭐ 困难 分治思想:两两合并

LeetCode 932. 漂亮数组

对于某些固定的 N,如果数组 A 是整数 1, 2, ..., N 的一个排列,且满足特定条件,则称其为漂亮数组。 难度:⭐⭐ 中等 经典分治问题

LeetCode 315. 计算右侧小于当前元素的个数

给定一个整数数组,返回一个新数组,其中每个元素表示原数组对应位置右侧小于该元素的个数。 难度:⭐⭐ 困难 可使用归并排序思想

九、学习路径建议 🗺️

第1-3天
基础理解
掌握分治的基本思想和模板,实现归并排序。
第4-7天
经典算法
深入学习快速排序、二分搜索等经典分治算法。
第8-10天
高级应用
学习 Karatsuba、FFT、最近点对等高级应用。
第11-14天
实战练习
完成 LeetCode 分治专题,至少 10 道题目。

十、总结 🎯

分治算法是解决许多高效算法的基础,掌握它对提升算法能力至关重要。

掌握分治思维

分治不仅是一种算法技巧,更是一种解决问题的思维方式。学会将大问题分解为小问题!

查看分治专题练习 下载完整代码

© 2026 AI未来实验室 · 用 AI 探索无限可能
站点地图 | Robots.txt | 关于