1. 程式人生 > >資料結構和演算法躬行記(7)——分治演算法

資料結構和演算法躬行記(7)——分治演算法

  分治演算法(Divide-and-Conquer Algorithm),就是分而治之,把一個複雜問題分成兩個或更多個相同或相似的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。

  分治演算法比較適合用遞迴來實現,而每一層遞迴都會涉及三個操作:

  (1)分解:將原問題分解為若干個規模較小,相對獨立,與原問題形式相同的子問題,縮小問題規模。

  (2)求解:若子問題規模較小且易於解決時(找出基線條件),則直接解。否則,遞迴地解決各子問題。其中基線條件(base case)通常是陣列為空或只包含一個元素。

  (3)合併:將各子問題的解合併為原問題的解。

  分治演算法是一種處理問題的思想和技巧,是很多高效演算法的基礎,例如排序演算法(歸併和快排)、最大公因數等。

  LeetCode的169. 多數元素,可將陣列一分為二,左邊遞迴最大值(left),右邊也一樣(right),當兩者相同,就是找到了;當不同時,比較誰的計數多。

  與動態規劃不同,分治演算法分解的子問題可以獨立求解,並且它們之間沒有相關性。

  在《劍指Offer》一書中曾提到,解決複雜問題的3種方法:

  (1)畫圖,涉及連結串列、二叉樹等資料結構時,畫幾張草圖,可將隱藏的規律變得直觀。

  (2)舉例,將抽象問題具體化,模擬執行過程,說不定能發現其中規律。

  (3)分解,如果問題很大,則嘗試把大問題分解成小問題,然後遞迴解決,分治法、動態規劃等方法都是分解複雜問題的思路。

一、歸併排序

  利用遞迴與分治技術將資料序列劃分成越來越小的半子表,再對半子表排序,最後用遞迴方法將排好序的半子表合併成為越來越大的有序序列,如下所示,思路如圖8所示。

function mergeSort(arr) {
  let len = arr.length;
  //基線條件
  if (len < 2) {
    return arr;
  }
  //分解
  let middle = Math.floor(len / 2),
    left = mergeSort(arr.slice(0, middle)),
    right = mergeSort(arr.slice(middle));
  //合併
  return merge(left, right);
}
function merge(left, right) {
  let result = [];
  //求解
  while (left.length && right.length) {
    //小的在左,大的在右
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
  while (left.length)
    result.push(left.shift());
  while (right.length)
    result.push(right.shift());
  return result;
}

圖 8

  面試題51 陣列中的逆序對。先統計子陣列中的逆序對,然後統計兩個相鄰陣列之間的逆序對,在統計的過程中還需要對陣列進行歸併排序。

二、快速排序

  採用“分而治之”的思想,把大的拆分為小的,小的再拆分為更小的。

  將原序列分為兩部分,其中前一部分的所有記錄均比後一部分的所有記錄小,然後再依次對前後兩部分的記錄進行快速排序,遞迴該過程,直到序列中的所有記錄均有序為止。

  程式碼實現如下所示,思路如圖9所示。

function quickSort(arr) {
  var length = arr.length;
  //基線條件
  if (length <= 1) {
    return arr;
  }
  var base = arr[0],
    left = [],             //儲存小於基準元素的記錄
    right = [];            //儲存大於基準元素的記錄
  //求解
  for (let i = 1; i < length; i++) {
    if (base > arr[i]) {        //放入左邊陣列
      left.push(arr[i]);
    } else {                    //放入右邊陣列
      right.push(arr[i]);
    }
  }
  //分解
  left = quickSort(left);
  right = quickSort(right);
  //合併
  return left.concat([base], right);
}

圖 9

  面試題39 陣列中出現次數超過一半的數字。問題轉換為查詢中位數,受快速排序的啟發,當基準值的下標剛好是n/2時,那麼就是中位數,否則在另外兩部分中查詢。

  面試題40 最小的 k 個數。採用快速排序思想,基於陣列第 k 個數字來調整,比 k 個數小的在左邊,大的在右邊。

&n