分治法
算法定义
分治法(Divide and Conquer Paradigm)是一种将复杂问题分解为规模较小的相似子问题,递归解决子问题,再将子问题的解合并以得到原问题解的算法设计策略。其核心思想是 "分而治之",通过将大问题分解为更容易处理的小问题来降低求解难度。
算法思路
- 分解(Divide):将原问题分解为若干个规模较小、结构与原问题相似的子问题
- 解决(Conquer):若子问题规模较小且易于解决,则直接解决;否则递归解决各子问题
- 合并(Combine):将各子问题的解合并为原问题的解
分治法的递归过程通常可以用数学递归式来表示,如 T (n) = aT (n/b) + f (n),其中 a 是子问题数量,n/b 是子问题规模,f (n) 是合并步骤的时间复杂度。
算法复杂度
- 时间复杂度:
- 由递归关系式决定,常见的有 O (n log n)(如归并排序、快速排序平均情况)
- 最坏情况可能达到 O (n²)(如快速排序最坏情况)
- 部分问题可达到 O (n)(如线性时间选择算法)
- 空间复杂度:
- 通常为 O (log n)(递归栈空间)或 O (n)(如归并排序需要额外存储空间)
- 取决于合并步骤是否需要额外空间
优点和缺点
- 优点:
- 提高算法效率,将高复杂度问题转化为低复杂度子问题
- 适合并行计算,子问题可独立求解
- 代码结构清晰,易于理解和实现
- 可解决多种复杂问题
- 缺点:
- 可能需要额外的存储空间(如归并排序)
- 递归调用有额外的时间开销
- 对于某些问题,合并步骤可能很复杂
- 并非所有问题都适合分治策略
应用场景
- 排序算法:归并排序、快速排序
- 查找算法:二分查找
- 矩阵运算:Strassen 矩阵乘法
- 字符串处理:大整数乘法、最长公共子序列
- 图论问题:最近点对问题、凸包问题
- 计算几何:平面划分问题
Java 代码实现
下面以经典的 "归并排序"、"快速排序" 和 "二分查找" 为例,展示分治法的几种实现方式:
import java.util.Arrays;
import java.util.Random;
public class DivideAndConquerExamples {
// 1. 归并排序:经典分治法实现
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int[] temp = new int[arr.length];
mergeSortRecursive(arr, 0, arr.length - 1, temp);
}
private static void mergeSortRecursive(int[] arr, int left, int right, int[] temp) {
// 基本情况:子数组长度为1时直接返回
if (left >= right) {
return;
}
// 分解:将数组分为两半
int mid = left + (right - left) / 2;
// 递归解决左右子数组
mergeSortRecursive(arr, left, mid, temp);
mergeSortRecursive(arr, mid + 1, right, temp);
// 合并:将两个有序子数组合并
merge(arr, left, mid, right, temp);
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左子数组起始索引
int j = mid + 1; // 右子数组起始索引
int k = left; // 临时数组起始索引
// 合并两个子数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 复制剩余元素
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组复制回原数组
System.arraycopy(temp, left, arr, left, right - left + 1);
}
// 2. 快速排序:优化实现(三数取中法选择基准)
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSortRecursive(arr, 0, arr.length - 1);
}
private static void quickSortRecursive(int[] arr, int left, int right) {
// 基本情况:子数组长度小于等于1时直接返回
if (left >= right) {
return;
}
// 优化:对小规模子数组使用插入排序
if (right - left + 1 <= 10) {
insertionSort(arr, left, right);
return;
}
// 分解:选择基准并分区
int pivotIndex = partition(arr, left, right);
// 递归解决左右子数组
quickSortRecursive(arr, left, pivotIndex - 1);
quickSortRecursive(arr, pivotIndex + 1, right);
}
private static int partition(int[] arr, int left, int right) {
// 优化:三数取中法选择基准
int mid = left + (right - left) / 2;
selectPivot(arr, left, mid, right);
// 将基准元素移到数组末尾
swap(arr, mid, right - 1);
int pivot = arr[right - 1];
int pivotIndex = right - 1;
// 分区操作
int i = left;
int j = right - 2;
while (true) {
// 从左向右找到大于基准的元素
while (arr[i] < pivot) {
i++;
}
// 从右向左找到小于基准的元素
while (j > left && arr[j] > pivot) {
j--;
}
if (i < j) {
swap(arr, i, j);
} else {
break;
}
}
// 将基准元素放到正确位置
swap(arr, i, pivotIndex);
return i;
}
// 三数取中法选择基准
private static void selectPivot(int[] arr, int left, int mid, int right) {
if (arr[left] > arr[mid]) {
swap(arr, left, mid);
}
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
}
// 插入排序:用于小规模数组
private static void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 3. 二分查找:迭代实现(避免递归开销)
public static int binarySearchIterative(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length - 1;
while (left <= right) {
// 优化:防止溢出,等价于(left + right) / 2
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到目标元素
}
// 4. 最大子数组问题:Kadane算法的分治实现
public static int maxSubArraySum(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return maxSubArrayRecursive(arr, 0, arr.length - 1);
}
private static int maxSubArrayRecursive(int[] arr, int left, int right) {
// 基本情况:子数组只有一个元素
if (left == right) {
return arr[left];
}
int mid = left + (right - left) / 2;
// 递归计算左右子数组的最大子数组和
int leftMax = maxSubArrayRecursive(arr, left, mid);
int rightMax = maxSubArrayRecursive(arr, mid + 1, right);
// 计算跨越中点的最大子数组和
int crossMax = maxCrossingSubArray(arr, left, mid, right);
// 返回三者中的最大值
return Math.max(Math.max(leftMax, rightMax), crossMax);
}
private static int maxCrossingSubArray(int[] arr, int left, int mid, int right) {
// 计算左半部分的最大和
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = mid; i >= left; i--) {
sum += arr[i];
if (sum > leftSum) {
leftSum = sum;
}
}
// 计算右半部分的最大和
int rightSum = Integer.MIN_VALUE;
sum = 0;
for (int i = mid + 1; i <= right; i++) {
sum += arr[i];
if (sum > rightSum) {
rightSum = sum;
}
}
// 返回跨越中点的最大和
return leftSum + rightSum;
}
// 测试
public static void main(String[] args) {
// 测试归并排序
int[] arr1 = {38, 27, 43, 3, 9, 82, 10};
mergeSort(arr1);
System.out.println("归并排序结果: " + Arrays.toString(arr1));
// 测试快速排序
int[] arr2 = {38, 27, 43, 3, 9, 82, 10};
quickSort(arr2);
System.out.println("快速排序结果: " + Arrays.toString(arr2));
// 测试二分查找
int[] sortedArr = {3, 9, 10, 27, 38, 43, 82};
int target = 27;
int index = binarySearchIterative(sortedArr, target);
System.out.println("二分查找 " + target + " 的索引: " + index);
// 测试最大子数组问题
int[] arr3 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
System.out.println("最大子数组和: " + maxSubArraySum(arr3));
}
}
代码解析
上面的代码实现了分治法的几种典型写法,并展示了不同的优化思路:
- 归并排序:
- 经典的分治法实现,严格遵循 "分解 - 解决 - 合并" 三步
- 时间复杂度稳定在 O (n log n),但需要 O (n) 的额外空间
- 合并步骤是关键,需要将两个有序子数组合并为一个有序数组
- 快速排序(优化实现):
- 采用三数取中法选择基准元素,避免最坏情况
- 对小规模子数组使用插入排序,减少递归开销
- 平均时间复杂度为 O (n log n),空间复杂度为 O (log n)
- 分区操作是核心,通过交换元素将数组分为两部分
- 二分查找(迭代实现):
- 用迭代代替递归,减少函数调用开销
- 计算中间索引时使用
left + (right - left) / 2避免溢出 - 时间复杂度为 O (log n),空间复杂度为 O (1)
- 最大子数组问题:
- 将问题分解为三种情况:最大子数组在左半部分、右半部分或跨越中点
- 重点在于计算跨越中点的最大子数组和
- 时间复杂度为 O (n log n)
分治法的优化通常集中在以下几个方面:
- 选择合适的分解策略,使子问题规模均衡
- 对小规模问题采用更高效的算法(如快速排序对小规模数组使用插入排序)
- 减少递归开销,必要时用迭代代替递归
- 优化合并步骤,减少额外空间使用
分治法的关键在于找到合适的分解方式和高效的合并策略,对于许多问题而言,分治法能够提供比朴素算法更优的时间复杂度。