數據結構與算法之“分治算法”

  • 2020-6-28
    • 卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。
      北島 – 《回答》

一、概述

分治算法(divide and conquer)的核心思想其實就是四個字,分而治之 ,也就是將原問題劃分成 n 個規模較小,並且結構與原問題相似的子問題,遞歸地解決這些子問題,然後再合併其結果,就得到原問題的解。

這個定義看起來有點類似遞歸的定義。關於分治和遞歸的區別,分治算法是一種處理問題的思想,遞歸是一種編程技巧。實際上,分治算法一般都比較適合用遞歸來實現。分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操作:

  • 分解:將原問題分解成一系列子問題;
  • 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解;
  • 合併:將子問題的結果合併成原問題。

分治算法能解決的問題,一般需要滿足下面這幾個條件:

  • 原問題與分解成的小問題具有相同的模式;
  • 原問題分解成的子問題可以獨立求解,子問題之間沒有相關性,這一點是分治算法跟動態規劃的明顯區別,等我們講到動態規劃的時候,會詳細對比這兩種算法;
  • 具有分解終止條件,也就是說,當問題足夠小時,可以直接求解;
  • 可以將子問題合併成原問題,而這個合併操作的複雜度不能太高,否則就起不到減小算法總體複雜度的效果了。

二、應用舉例分析

2.1、分治算法求解數據的有序度

在排序算法裏有數據的有序度、逆序度的概念,我們用有序度來表示一組數據的有序程度,用逆序度表示一組數據的無序程度。

假設我們有 n 個數據,我們期望數據從小到大排列,那完全有序的數據的有序度就是 n(n-1)/2,逆序度等於 0;相反,倒序排列的數據的有序度就是 0,逆序度是 n(n-1)/2。除了這兩種極端情況外,我們通過計算有序對或者逆序對的個數,來表示數據的有序度或逆序度。

在這裏插入圖片描述
現在的問題是,如何編程求出一組數據的有序對個數或者逆序對個數呢?因爲有序對個數和逆序對個數的求解方式是類似的,所以你可以只思考逆序對個數的求解方法。

最笨的方法是,拿每個數字跟它後面的數字比較,看有幾個比它小的。我們把比它小的數字個數記作 k,通過這樣的方式,把每個數字都考察一遍之後,然後對每個數字對應的 k 值求和,最後得到的總和就是逆序對個數。不過,這樣操作的時間複雜度是 O(n^2)。那有沒有更加高效的處理方法呢?

我們用分治算法來試試。我們套用分治的思想來求數組 A 的逆序對個數。我們可以將數組分成前後兩半 A1 和 A2,分別計算 A1 和 A2 的逆序對個數 K1 和 K2,然後再計算 A1 與 A2 之間的逆序對個數 K3。那數組 A 的逆序對個數就等於 K1+K2+K3。

我們前面講過,使用分治算法其中一個要求是,子問題合併的代價不能太大,否則就起不了降低時間複雜度的效果了。那回到這個問題,如何快速計算出兩個子問題 A1 與 A2 之間的逆序對個數呢?

這裏就要藉助歸併排序算法了。你可以先試着想想,如何藉助歸併排序算法來解決呢?

歸併排序中有一個非常關鍵的操作,就是將兩個有序的小數組,合併成一個有序的數組。實際上,在這個合併的過程中,我們就可以計算這兩個小數組的逆序對個數了。每次合併操作,我們都計算逆序對個數,把這些計算出來的逆序對個數求和,就是這個數組的逆序對個數了。

在這裏插入圖片描述
把這個過程翻譯成了代碼,你可以結合着圖和文字描述一起看下。

private int num = 0; // 全局變量或者成員變量
 
public int count(int[] a, int n) {
  num = 0;
  mergeSortCounting(a, 0, n-1);
  return num;
}
 
private void mergeSortCounting(int[] a, int p, int r) {
  if (p >= r) return;
  int q = (p+r)/2;
  mergeSortCounting(a, p, q);
  mergeSortCounting(a, q+1, r);
  merge(a, p, q, r);
}
 
private void merge(int[] a, int p, int q, int r) {
  int i = p, j = q+1, k = 0;
  int[] tmp = new int[r-p+1];
  while (i<=q && j<=r) {
    if (a[i] <= a[j]) {
      tmp[k++] = a[i++];
    } else {
      num += (q-i+1); // 統計 p-q 之間,比 a[j] 大的元素個數
      tmp[k++] = a[j++];
    }
  }
  while (i <= q) { // 處理剩下的
    tmp[k++] = a[i++];
  }
  while (j <= r) { // 處理剩下的
    tmp[k++] = a[j++];
  }
  for (i = 0; i <= r-p; ++i) { // 從 tmp 拷貝回 a
    a[p+i] = tmp[i];
  }

有很多同學經常說,某某算法思想如此巧妙,我是怎麼也想不到的。實際上,確實是的。有些算法確實非常巧妙,並不是每個人短時間都能想到的。比如這個問題,並不是每個人都能想到可以藉助歸併排序算法來解決,不誇張地說,如果之前沒接觸過,絕大部分人都想不到。但是,如果我告訴你可以藉助歸併排序算法來解決,那你就應該要想到如何改造歸併排序,來求解這個問題了,只要你能做到這一點,我覺得就很棒了。

2.2、練習

  • 二維平面上有 n 個點,如何快速計算出兩個距離最近的點對?

  • 有兩個 nn 的矩陣 A,B,如何快速求解兩個矩陣的乘積 C=AB?

三、參考資料

  • 王爭 – 《極客時間|數據結構與算法之美》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章