在計算機科學中,分治法是一種很重要的算法。算法
字面上的解釋是分而治之
,就是把一個複雜的問題分紅兩個或更多的相同或類似的子問題。segmentfault
直到最後子問題能夠簡單的直接求解,原問題的解即子問題的解的合併。數組
分治法通常使用遞歸來求問題的解。數據結構
遞歸就是不斷地調用函數自己。併發
好比咱們求階乘1 * 2 * 3 * 4 * 5 *...* N
:數據結構和算法
package main import "fmt" func Rescuvie(n int) int { if n == 0 { return 1 } return n * Rescuvie(n-1) } func main() { fmt.Println(Rescuvie(5)) }
會反覆進入一個函數,它的過程以下:函數
Rescuvie(5) {5 * Rescuvie(4)} {5 * {4 * Rescuvie(3)}} {5 * {4 * {3 * Rescuvie(2)}}} {5 * {4 * {3 * {2 * Rescuvie(1)}}}} {5 * {4 * {3 * {2 * 1}}}} {5 * {4 * {3 * 2}}} {5 * {4 * 6}} {5 * 24} 120
函數不斷地調用自己,而且還乘以一個變量:n * Rescuvie(n-1)
,這是一個遞歸的過程。優化
很容易看出, 由於遞歸式使用了運算符,每次重複的調用都使得運算的鏈條不斷加長,系統不得不使用棧進行數據保存和恢復。code
若是每次遞歸都要對愈來愈長的鏈進行運算,那速度極慢,而且可能棧溢出,致使程序奔潰。協程
因此有另一種寫法,叫尾遞歸:
package main import "fmt" func RescuvieTail(n int, a int) int { if n == 1 { return a } return RescuvieTail(n-1, a*n) } func main() { fmt.Println(RescuvieTail(5, 1)) }
他的遞歸過程以下:
RescuvieTail(5, 1) RescuvieTail(4, 1*5)=RescuvieTail(4, 5) RescuvieTail(3, 5*4)=RescuvieTail(3, 20) RescuvieTail(2, 20*3)=RescuvieTail(2, 60) RescuvieTail(1, 60*2)=RescuvieTail(1, 120) 120
尾部遞歸是指遞歸函數在調用自身後直接傳回其值,而不對其再加運算,效率將會極大的提升。
若是一個函數中全部遞歸形式的調用都出如今函數的末尾,咱們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸函數的特色是在迴歸過程當中不用作任何操做,這個特性很重要,由於大多數現代的編譯器會利用這種特色自動生成優化的代碼。-- 來自百度百科。
尾遞歸函數,部分高級語言編譯器會進行優化,減小沒必要要的堆棧生成,使得程序棧維持固定的層數,不會出現棧溢出的狀況。
咱們將會舉多個例子說明。
斐波那契數列是指,後一個數是前兩個數的和的一種數列。以下:
1 1 2 3 5 8 13 21 ... N-1 N 2N-1
尾遞歸的求解爲:
package main import "fmt" func F(n int, a1, a2 int) int { if n == 0 { return a1 } return F(n-1, a2, a1+a2) } func main() { fmt.Println(F(1, 1, 1)) fmt.Println(F(2, 1, 1)) fmt.Println(F(3, 1, 1)) fmt.Println(F(4, 1, 1)) fmt.Println(F(5, 1, 1)) }
輸出:
1 2 3 5 8
當n=5
的遞歸過程以下:
F(5,1,1) F(4,1,1+1)=F(4,1,2) F(3,2,1+2)=F(3,2,3) F(2,3,2+3)=F(2,3,5) F(1,5,3+5)=F(1,5,8) F(0,8,5+8)=F(0,8,13) 8
在一個已經排好序的數列,找出某個數,如:
1 5 9 15 81 89 123 189 333
從上面排好序的數列中找出數字189
。
二分查找的思路是,先拿排好序數列的中位數與目標數字189
對比,若是恰好匹配目標,結束。
若是中位數比目標數字大,由於已經排好序,因此中位數右邊的數字絕對都比目標數字大,那麼從中位數的左邊找。
若是中位數比目標數字小,由於已經排好序,因此中位數左邊的數字絕對都比目標數字小,那麼從中位數的右邊找。
這種分而治之,一分爲二的查找叫作二分查找算法。
遞歸解法:
package main import "fmt" // 二分查找遞歸解法 func BinarySearch(array []int, target int, l, r int) int { if l > r { // 出界了,找不到 return -1 } // 從中間開始找 mid := (l + r) / 2 middleNum := array[mid] if middleNum == target { return mid // 找到了 } else if middleNum > target { // 中間的數比目標還大,從左邊找 return BinarySearch(array, target, 1, mid-1) } else { // 中間的數比目標還小,從右邊找 return BinarySearch(array, target, mid+1, r) } } func main() { array := []int{1, 5, 9, 15, 81, 89, 123, 189, 333} target := 500 result := BinarySearch(array, target, 0, len(array)-1) fmt.Println(target, result) target = 189 result = BinarySearch(array, target, 0, len(array)-1) fmt.Println(target, result) }
輸出:
500 -1 189 7
能夠看到,189
這個數字在數列的下標7
處,而500
這個數找不到。
固然,遞歸解法均可以轉化爲非遞歸,如:
package main import "fmt" // 二分查找非遞歸解法 func BinarySearch2(array []int, target int, l, r int) int { ltemp := l rtemp := r for { if ltemp > rtemp { // 出界了,找不到 return -1 } // 從中間開始找 mid := (ltemp + rtemp) / 2 middleNum := array[mid] if middleNum == target { return mid // 找到了 } else if middleNum > target { // 中間的數比目標還大,從左邊找 rtemp = mid - 1 } else { // 中間的數比目標還小,從右邊找 ltemp = mid + 1 } } } func main() { array := []int{1, 5, 9, 15, 81, 89, 123, 189, 333} target := 500 result := BinarySearch2(array, target, 0, len(array)-1) fmt.Println(target, result) target = 189 result = BinarySearch2(array, target, 0, len(array)-1) fmt.Println(target, result) }
不少計算機問題均可以用遞歸來簡化求解,理論上,全部的遞歸方式均可以轉化爲非遞歸的方式,只不過使用遞歸,代碼的可讀性更高。
我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook。