資料結構和演算法躬行記(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