Algorithms_算法思想_递归分治
文章目錄
- 引導案例
- 遞歸的定義
- 什么樣的問題可以用遞歸算法來解決
- 遞歸如何實現以及包含的算法思
- 遞歸的公式
- 斐波那契數列代碼實現
- 遞歸的時間復雜度和空間復雜度
- 遞 與 歸
- 遞歸的優化
- 優化方式一:不使用遞歸 ,使用循環---> O(n)
- 優化方式二: 利用緩存,避免重復計算---> O(n)
- 優化方式三(最佳方式): 尾遞歸
- 什么是尾遞歸?
- 尾遞歸的原理
- 理解遞歸的形式計算階乘為啥不是尾遞歸
- 尾遞歸重寫階層的算法
- 尾遞歸重寫斐波那契的算法
- 尾遞歸的重要性
- 分治
引導案例
案例一:
分銷系統的返利: 比如B是A的下線,C是B的下線,那么在分錢返利的時候A可以分B,C的錢,這時候我們是不是就要分別找B,C的最后上級。這個問題我們一般怎么來解決呢?
C–>B–>A
案例二: .斐波那契數列:
1 1 2 3 5 8 13 21 ......有什么特點?
從第三個數開始 就等于前面兩個數相加;
數論思想:利用數學公式或者定理或者規律求解問題;
算法思想中最難的點:遞歸+動態規劃
樹論中(比如二叉樹,紅黑樹)和遞歸密不可分,所以遞歸一定要弄明白了。
遞歸的定義
遞歸算法是一種直接或者間接調用自身函數或者方法的算法。
通俗來說,遞歸算法的實質是把問題分解成規模縮小的同類問題的子問題,然后遞歸調用方法來表示問題的解。
舉個生活中的例子
比如我們在某窗口排隊人太多了,我不知道我排在第幾個,那么我就問我前面的人排第幾個, 因為知道他排第幾我就知道我是第幾了。但前面的人也不知道自己排第幾那怎么辦呢?他也可以繼續往前面問,直到問到第一個人,然后從第一個人一直傳到我這里 我就很清楚的知道我是第幾了。
以上這個場景就是一個典型的遞歸。我們在這個過程中大家有沒有發現一個規律那么就是會 有一個問的過程,問到第一個后有一個回來的過程吧。這就是遞(問)加歸(回)。
那么這個過程我們是不是可以用一個數學公式來求解呢?那這個數學公式又是什么?
f(n)=f(n-1)+1- f(n):表示我的位置
- f(n-1):表示我前面的那個人;
說白了 一個一個往前問 就是自己調用自己,完成這個功能。
推導出公式: f(n) = f(n-1) + f(n-2)
什么樣的問題可以用遞歸算法來解決
需要滿足的條件才可以用遞歸來解決?
(1)一個問題的解可以分解為幾個子問題的解:
子問題,我們通過分治的思想可以把一個數據規模大的問題,分解為很多小的問題。
我們可以把剛剛那個問前面的那個人看為子問題。大化小
(2)這個問題與分解之后的子問題,求解思路完全一樣:
(3)一定有一個最后確定的答案,即遞歸的終止條件。
剛剛那個問題就是第一個人。第一個人是肯定知道自己排第幾吧即n=1的時候,如果沒有這個特性那么我們這個遞歸就會出現死循環,最后程序就是棧溢出;StackOverflowError
遞歸并不是馬上返回,而是一層一層的保存在Stack里邊,滿足結束條件后才一層一層的返回.
棧是用來存儲函數調用信息的絕好方案,然而棧也有一些缺點:
-
棧維護了每個函數調用的信息直到函數返回后才釋放,這需要占用相當大的空間,尤其是在程序中使用了許多的遞歸調用的情況下。
-
除此之外,因為有大量的 信息需要保存和恢復,因此生成和銷毀活躍記錄需要消耗一定的時間
遞歸如何實現以及包含的算法思
遞歸,回溯(歸的過程);
遞歸的關鍵相信大家已經知道了就是要(1)求出這個遞歸公式,(2)找到終止條件。
現在我們可以回到課堂前跟大家講的那個斐波那契數列數列:
1 1 2 3 5 8 13 這個的數列我們稱之為斐波那契數列
按照我們說的 兩個點 ,來分析一下:
求解公式:f(n)=f(n-1)+f(n-2)
終止條件:n<=2 也就是f(n)=1
遞歸的公式
遞歸代碼最重要的兩個特征:結束條件和自我調用。自我調用是在解決子問題,而結束條件定義了最簡子問題的答案。
int func(傳入數值) {if (終止條件) return 最小子問題解;return func(縮小規模); }斐波那契數列代碼實現
分析一下,給定一個值n , 輸出 n個數字組成一個斐波那契數列
從n開始,一直往前遞歸,直到符合終止條件即可。
public class Fibonacc {public static int fab(int n){if(n <=2 ) return 1; // 終止條件 ,為啥return 1 ? 1 1 2 3 5 8 ... 前兩位是1啊 到頭了return fab(n-1) + fab(n-2); // 遞歸公式}public static void main(String[] args) {for (int i = 1; i <= 10; i++) {System.out.println(i + ":" + fab(i));}} }遞歸的時間復雜度和空間復雜度
代碼實現了,我們來分析下 遞歸的復雜度
以斐波那契數列為例為分析遞歸樹:
f(n)=f(n-1)+f(n-2)圖一畫,一目了然 ,斐波那契數列的 時間復雜度 O(2^n)
空間復雜度的話,遞歸并不是馬上返回,而是一層一層的保存在Stack里邊,因為還有一個 歸的過程,滿足結束條件后才一層一層的返回. 空間復雜度自然也是 O(2^n)
遞 與 歸
這個斐波那契數列 來演示 這個 遞 與歸 的過程,因為時間復雜度為2^n, 比較難畫圖。
我們換個常見的遞歸吧 -------------> 階乘( n!)
階乘的數學公式: n!=n×(n-1)×(n-2)……2×1
按照遞歸的兩個步驟:
以遞歸的方式計算4!
用代碼實現如下
遞歸的優化
時間復雜度和空間復雜度 是 O(2^n) , 隨著問題規模的擴大,這個性能將急劇下降,所以我們需要優化,看看能不能優化到 O(n) 或者O(nlogn)…
優化方式一:不使用遞歸 ,使用循環—> O(n)
記住: 任何使用遞歸的程序 ,都可以轉化為不是用遞歸 ,使用循環來代替 。
優化前我們簡單記下耗時
才計算到45 , 2^45 太大的計算量了。。。。。
行了,別糾結了,問題規模一旦上去,你這個遞歸是算不出來的…
既然 任何使用遞歸的程序 ,都可以轉化為不是用遞歸 ,那改吧
public static int noCyCle(int n) {int a = 1; // 斐波那契數列的第一位int b = 1; // 斐波那契數列的第二位int c = 0; // 初始值if (n <= 2) return 1; // 終止條件for (int i = 3; i <= n; i++) { // 為啥i =3 開始算? 因為 第三位 = 前兩位的和啊 , 所以從第三位算 。 從頭往后算 ,遞歸是從后往前算。long beginTime = System.currentTimeMillis();c = a + b; // 求第三位的和a = b; // 將第二位 置為第一位b = c; // 將第三位 置為第二位 ,循環求和System.out.println(i + ":" + c + " 計算耗時: " + (System.currentTimeMillis() - beginTime) + "ms");}return c;}public static void main(String[] args) {noCyCle(45);}比較下 遞歸和循環兩種寫法
遞歸的代碼: 很容易理解
循環的代碼,相比而言,不好理解。
優化方式二: 利用緩存,避免重復計算—> O(n)
既然,遞歸的代碼 易讀 ,那肯定是可以用的了,那繼續思考下, 該如何又能使用遞歸,而時間復雜度又沒有這么高呢?
先看下,遞歸是怎么玩的
有沒有發現,每個分支上,都得重復計算好幾個。
算f(8) = f(7)+f(6) , f(7)=f(6)+f(5) … 是不是需要重復計算
被計算次數最多的那個 肯定是 f(1) + f(2) ------> 如果我們把這個結算結果,緩存起來,是不是就只需要計算一次,剩下的直接從緩存里取就好了?
改造下
private static int[] data; // 數組 緩存計算結果 值初始化為0public static int fabWithCache(int n) {if (n <= 2) return 1; // 終止條件// 如果數組不為空 ,說明之前計算過,直接從數組緩存中獲取結果if (data[n] > 0) {return data[n] ;}int result = fabWithCache(n - 1) + fabWithCache(n - 2); // 遞歸的過程data[n] = result ; // 將結果存入數組緩存中return result;}public static void main(String[] args) {data = new int[46]; // 初始化數組的容量for (int i = 1; i <= 45; i++) {long beginTime = System.currentTimeMillis();System.out.println(i + ":" + fabWithCache(i) + " 耗時" + (System.currentTimeMillis() - beginTime) +" ms");}}為啥能這么改呢? 歸結到底還是我們分析了遞歸樹中有太多重復的值,所以我們把中間的計算結果保存起來 , 在 歸 的過程中,不需要重復計算,直接從第一次計算后緩存的那個結果中取即可。
分析下時間復雜度和空間復雜度 —> O(n)
優化方式三(最佳方式): 尾遞歸
什么是尾遞歸?
尾遞歸就是調用函數一定出現在末尾,沒有任何其他的操作了。
如果一個函數中所有遞歸形式的調用都出現在函數的末尾,我們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最后執行的語句且它的返回值不屬于表達式的一部分時,這個遞歸調用就是尾遞歸
因為我們編譯器在編譯代碼時,如果發現函數末尾已經沒有操作了,這時候就不會創建新的棧,而且覆蓋到前面去。
尾遞歸的原理
當編譯器檢測到一個函數調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去創建一個新的。編譯器可以做到這點,因為遞歸調用是當前活躍期內 最后一條待執行的語句,于是當這個調用返回時棧幀中并沒有其他事情可做,因此也就沒有保存棧幀的必要了。
通過覆蓋當前的棧幀而不是在其之上重新添加一個, 這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。
理解遞歸的形式計算階乘為啥不是尾遞歸
為了理解尾遞歸是如何工作的,那我們先以遞歸的形式計算階乘。
首先,這可以很容易讓我們理解為什么之前所定義的遞歸不 是尾遞歸。
回憶之前對計算n!的定義:在每個活躍期計算n倍的(n-1)!的值,讓n=n-1并持續這個過程直到n=1為止。
這種定義不是尾遞歸的,因為 每個活躍期的返回值都依賴于用n乘以下一個活躍期的返回值,因此每次調用產生的棧幀將不得不保存在棧上直到下一個子調用的返回值確定。
尾遞歸重寫階層的算法
現在讓我們考慮以尾 遞歸的形式來定義計算n!的過程。
這種定義還需要接受第二個參數a,除此之外并沒有太大區別。
a(初始化為1)維護遞歸層次的深度。這就讓我們避免了每次還需要將返回值再乘以n。然而,在每次遞歸調用中,令a=na并且n=n-1。繼續遞歸調用,直到n=1,這滿足結束條件,此時直接返回a即可。
每次都是把 上一個的計算結果傳遞下去,這樣就避免了歸的過程,這樣的話,就不用開辟那么多的占空間。
上面的方法是尾遞歸的,因為對tailFact的單次遞歸調用是函數返回前最后執行的一條語句。
在tailFact中 碰巧最后一條語句也是對tailFact的調用,但這并不是必需的。換句話說,在遞歸調用之后還可以有其他的語句執行,只是它們只能在遞歸調用沒有執行時 才可以執行。
尾遞歸重寫斐波那契的算法
/**** @param pre 上上一次運算的結果* @param res 上一次運算的結果* @param n* @return*/public static int fabTail(int pre, int res , int n ) {if (n <= 2) return res; // 終止條件 不能反悔1哈,已經把結果傳遞下來了,直接返回res即可,因為沒有歸的過程了。 return fabTail(res,pre+res, n-1); // 遞歸公式}public static void main(String[] args) {data = new int[46]; // 初始化數組的容量for (int i = 1; i <= 45; i++) {long beginTime = System.currentTimeMillis();System.out.println(i + ":" + fabTail(1,1,i) + " 耗時" + (System.currentTimeMillis() - beginTime) +" ms");}}尾遞歸的重要性
尾遞歸就是把當前的運算結果(或路徑)放在參數里傳給下層函數
不用尾遞歸,函數的堆棧耗用難以估量,需要保存很多中間函數的堆棧。
比如f(n, sum) = f(n-1) + value(n) + sum; 會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留后一個函數堆棧即可,之前的可優化刪去。
分治
分治算法可以分三步走:分解 -> 解決 -> 合并
1. 分解原問題為結構相同的子問題。
2. 分解到某個容易求解的邊界之后,進行遞歸求解。
3. 將子問題的解合并成原問題的解。
歸并排序 ,典型的分治算法; 分治,典型的遞歸結構。
該函數的職即 對傳入的一個數組排序 。
那么這個問題能不能分解呢?-----------------> 給一個數組排序,不就等于給該數組的兩半分別排序,然后合并。
void merge_sort(一個數組) {if (可以很容易處理) return;merge_sort(左半個數組);merge_sort(右半個數組);merge(左半個數組, 右半個數組); }分治算法的套路是 分解 -> 解決(觸底)-> 合并(回溯)
總結
以上是生活随笔為你收集整理的Algorithms_算法思想_递归分治的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Algorithms_基础数据结构(04
- 下一篇: 白话Elasticsearch64-ze