算法数据结构:分治算法

992 阅读2分钟

1、什么是分治算法

分治算法(divide and conquer)的核心思想其实就是四个字,分而治之,也就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后合并其结果,就得到原问题的解。

实际上,分治算法一般都比较适合用递归来实现,分治算法是一种处理问题的思想,递归是一种编程技巧。分治算法递归实现中,每一层递归都会涉及这样三个操作:

  • 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
  • 解决这些子问题,递归地求解各个子问题,若子问题足够小,则直接求解
  • 合并这些子问题的解,成原问题的解

2、模板

2.1、Java 模板

private static int divide_conquer(Problem problem, ) {
  
  if (problem == NULL) {
    int res = process_last_result();
    return res;     
  }
  
  subProblems = split_problem(problem)
  
  res0 = divide_conquer(subProblems[0])
  res1 = divide_conquer(subProblems[1])
  
  result = process_result(res0, res1);
  
  return result;
}

2.2、Python 模板

def divide_conquer(problem, param1, param2, ...): 
  # recursion terminator 
  if problem is None: 
	print_result 
	return 

  # prepare data 
  data = prepare_data(problem) 
  subproblems = split_problem(problem, data) 

  # conquer subproblems 
  subresult1 = self.divide_conquer(subproblems[0], p1, ...) 
  subresult2 = self.divide_conquer(subproblems[1], p1, ...) 
  subresult3 = self.divide_conquer(subproblems[2], p1, ...) 
  …

  # process and generate the final result 
  result = process_result(subresult1, subresult2, subresult3, …)
	
  # revert the current level states

3、实战

3.1、括号生成

class Solution {
    List<String> res =  new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        if (n < 1) {
            return res;
        }
        generate(0, 0, "", n);
        return res;
    }

    public void generate(int left, int right, String target, int n) {
        if (target.length() == 2 * n) {
            res.add(target);
            return;
        }
        if (left < n) {
            generate(left + 1, right, target + "(", n);
        }
        if (right < left) {
            generate(left + 1, right, target + ")", n);
        }
    }
}

3.2、归并排序(Merge Sort)

3.2.1、归并排序算法完全遵循分治模式,直观操作如下:
  • 分解:把长度为 n 的输入序列,分为两个长度为 n/2 的子序列
  • 解决:对这两个子序列分别采用归并排序
  • 合并:将两个排序好的子序列合并成一个最终的排序序列

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

3.2.2、实现步骤图解(图片来源菜鸟教程)
3.2.3、伪代码
# A[] 下标从 1 开始
MERGE(A, p, q, r)
	n1 = q - p + 1
    n2 = r - q
    let L[1..n1 + 1] and R[1..n2 + 1] be new arrays
    # [0, q]
    for i = 1 to n1
    	L[i] = A[p + i - 1]
    
    # [q + 1, r]
    for i = 1 to n2
    	R[i] = A[q + i]
   	
    L[n1 + 1] = ∞
    R[n2 + 1] = ∞
    
    i = 1
    j = 1
    # [p, r]
    for k = p to r 
    	if L[i] <= R[j]
        	A[k] = L[i]
            i++
      	else 
        	A[k] = R[j]
            j++
MERGE-SORT(A, p, r)
	if p < r
     	q = (p + r) / 2
        MERGE-SORT(A, p, q)
        MERGE-SORT(A, q + 1, r)
        MERGE(A, p, q, r)
3.2.4、Java 实现
class Solution {

  public static void main(String[] args) {
        int[] array = {1, 24, 5, 56, 9, 23, 5, 16, 7};
        Solution.mergeSort(array, 0, array.length - 1);
        System.out.println(array);
    }

    /**
     * 参数入口 mergeSort(array, 0, array.length - 1)
     *
     * @param array 待排序数组
     * @param left  左边界
     * @param right 右边界
     */
    public static void mergeSort(int[] array, int left, int right) {
        if (right <= left) {
            return;
        }
        // (left + right) / 2
        int mid = left + ((right - left) >> 1);
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        merge(array, left, mid, right);
    }

    public static void merge(int[] array, int left, int mid, int right) {
        // 中间数组
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;

        while (i <= mid && j <= right) {
            temp[k++] = array[i] <= array[j] ? array[i++] : array[j++];
        }

        while (i <= mid) {
            temp[k++] = array[i++];
        }
        while (j <= right) {
            temp[k++] = array[j++];
        }

        for (int p = 0; p < temp.length; p++) {
            array[left + p] = temp[p];
        }
    }
}

4、总结

分治算法能解决的问题,一般需要满足下面这几个条件:

  1. 原问题与分解成的小问题具有相同的模式
  2. 原问题分解成的子问题可以独立求解,子问题之间没有相关性;这一点是分治算法跟动态规划的明显区别
  3. 具有分解终止条件,也就是说,当问题足够小时,可以直接求解
  4. 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了