創造演算法的演算法:分治法
有多少人學習演算法時,都是從排序入手,然後就沒有然後了,不是演算法有多難,而是演算法對程式設計師來說,有點類似武俠世界裡的內功心法,練了半天,總覺得與功夫招式沒什麼大關係,和工作遇到的問題關係也不是那麼直接,實際上,基本上所有語言都內建了排序的方法,有多少人會自己寫個快排去排序的?!
那麼學習演算法到底有什麼用呢?這裡我提供一個角度:
學習演算法是為了用程式的思想,抽象實際問題,提供方便的解決方案。
分治法是程式設計界排名前5的重要演算法之一,但是它本身並不是一種具體的演算法,也沒有典型的資料結構,我們先從一個例子來看下它能解決什麼樣的問題:
《最大子序和》:
給定一個整數陣列 nums ,找到一個具有最大和的連續子陣列(子陣列最少包含一個元素),返回其最大和。
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。

分治法.001
問題中,最終的結果當然是9個數字中的最大值,問題是,9個數字的子陣列有多少呢?可以2兩個一組、3個一組,4個一組,這樣寫演算法必然繁瑣複雜,要想辦法簡化一下問題,先把9個數字分成兩部分:

分治法.002
假設我們已經求出了[-2...-5]部分的最大值,記為sumMax(0...7),問題就簡單了,只需在sumMax(0...7)、4、sumMax(0...7)+4 三者之間取最大值即可。
剩下的你可能已經知道了,[-2...-5]部分也需要求最大和,一樣分為兩部分,這樣一直分下去:

分治法.003
最後會發現,只剩下 -2 和 1,這時,只需要比較 (-2 + 1)和 1 的大小,顯然取1,這是為了滿足題目中“連續子陣列”的要求,靠右邊的數代表所在組,才能參與更大分組的計算。
由分析可見,用遞迴可以很方便的實現,只需要將去掉末尾的陣列再次呼叫相同方法即可,由於取子陣列很耗費計算時,這裡展開為迴圈,下面完整演算法(Swift描述):
func maxSubArray(_ nums: [Int]) -> Int { var maxAll:Int = 0 if (nums.count == 0) { maxAll = 0 return 0 } if (nums.count == 1) { maxAll = nums[0] return nums[0] } maxAll = nums[0] var subSumMax = 0 if (nums.count >= 2) { for v in nums { subSumMax += v subSumMax = max(subSumMax, v) maxAll = max(subSumMax, maxAll) } } return maxAll }
maxAll 用來記錄 曾經達到過的
最大和。
通過以上的詳細分析,可以總結出,分治法包括兩個步驟:
- 將大問題分拆成小問題。
- 找到小問題的解決方案。
以上過程是個互動過程,有時需要嘗試不同的分拆方法, 關鍵看分拆的小問題是否有一致的解決方案
。
在眾多演算法中,很多都是分治法的具體實踐:
- 二分查詢
- 歸併排序
- 快速排序
當你領悟了分治法的思想後,很多演算法都可以自行推導得出,現在是不是對學好演算法更有信心了呢?歡迎你的留言,我們一同修煉心法。