日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

++递归 字符串全排列_超全递归技巧整理,这次一起拿下递归

發(fā)布時間:2024/9/27 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ++递归 字符串全排列_超全递归技巧整理,这次一起拿下递归 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

0. 前言

大家好,我是多選參數(shù)的程序鍋,一個正在 neng 操作系統(tǒng)、學數(shù)據(jù)結構和算法以及 Java 的硬核菜雞。本篇將主要介紹遞歸相關的內(nèi)容,下面是本篇的內(nèi)容提綱。

1. 遞歸基礎

★?

爭哥:從我自己學習數(shù)據(jù)結構和算法的經(jīng)歷來看,我覺得最難理解的知識點,一個是動態(tài)規(guī)劃,另一個是遞歸。好吧,在眾多不太熟練的數(shù)據(jù)結構和算法中,我也是這兩個。

**遞歸從編程形式上看是函數(shù)自己調(diào)用自己,是一種編程方法。**很多數(shù)據(jù)結構和算法的實現(xiàn)都會采用遞歸這種方式,比如 DFS 深度優(yōu)先搜索、前中后序二叉樹遍歷等等。那么怎么理解遞歸呢?遞歸其實分為兩個過程,去的過程叫過“遞”,回來的過程叫做"歸"。比如我們坐在電影院里看電影,想知道自己坐的是第幾排(別說電影票上有寫),那么我們會問前面一排的人,它是第幾排,這個過程叫過“遞”;之后前面一排的人同樣會問再前面一排的人他是第幾排,以此類推。當問到第一排的人之后,第一排的人向第二排的人回了個 1,以此類推;我們前面一排的人會給我們回了個第 n-1 排,那么這個過程叫做“歸”,從而得到我們是第 n 排。

1.1. 遞歸使用需要滿足的三個條件

要想使用遞歸一定要以下這三個條件,簡單來說就是可以分解成子問題,這些子問題的解法和原問題思路一樣,有終止條件。

  • 一個問題的解可以分成幾個子問題的解。子問題的意思是數(shù)據(jù)規(guī)模更小的問題,也就是說一個數(shù)據(jù)規(guī)模比較大的問題解可以由幾個數(shù)據(jù)規(guī)模比較小的問題的解組成。
  • 子問題除了數(shù)據(jù)規(guī)模不同之外,求解思路完全一樣。也就是子問題的求解方法和當前問題的求解方法是一樣。
  • 存在遞歸終止條件。當前問題會被分解成子問題,子問題又會被分解成更小的子問題,以此類推下去,顯然不能無限遞下去,一定要終止條件,從而有歸的過程。

1.2. 編寫遞歸代碼的技巧

  • 寫遞歸代碼最關鍵的是找到大問題分解為小問題的規(guī)律,并且基于此寫出遞推公式;之后再確定終止條件(也叫做基線條件);最后將這些翻譯成代碼即可

  • 另外在編程思考遞歸過程的時候,千萬不要鋪開模擬遞歸的過程,也就是千萬不要試圖想清楚整個遞和歸的過程,這種實際上會進入一個思維誤區(qū)。其實,**只需要考慮兩層即可,即假設子問題已有答案,然后思考原問題和子問題的解怎么聯(lián)系起來。**比如一個問題 A 可以分解為若干子問題 B、C、D,那么假設子問題 B、C、D 已經(jīng)解決,在此基礎上思考如何解決問題 A 即可。不要去想一層層調(diào)用關系,不要試圖用人腦分解遞歸的每個步驟,屏蔽掉這些細節(jié)。

1.3. 遞歸方式存在的弊端

在遞歸實現(xiàn)代碼時,會遇到很多問題,比如堆棧溢出、重復計算、函數(shù)調(diào)用耗時多、空間復雜度高等問題。

  • 堆棧溢出

    因為遞歸的本質(zhì)是函數(shù)調(diào)用,而函數(shù)調(diào)用過程中會使用棧來保存臨時變量(棧中保存著未完成的函數(shù)調(diào)換用)。如果遞歸求解的數(shù)據(jù)規(guī)模很大,調(diào)用層次很深,一直壓入棧,就會有棧溢出的風險。

    那么如何避免棧溢出呢?可以設置遞歸的層次,一旦超過一定層次之后,就不在往下遞歸了,直接返回報錯。但是這種方式不能完全解決問題,因為可能層次設置太大,在未達到一定層次之前就已經(jīng)棧溢出了。因此,這種方式適合最大深度比較小的。

  • 重復計算

    在遞歸的過程中還會出現(xiàn)重復計算的問題,如下面這個遞歸過程中就存在大量的重復計算:想要計算f(5),需要先計算 f(4) 和 f(3) ,而計算 f(4) 又會計算 f(3),f(3) 就被重復計算了。

    為了避免重復計算,可以使用一個數(shù)據(jù)結構(比如散列表)來保存已經(jīng)求解過的 f(k) 值。當遞歸到 k 的時候判斷,f(k) 是否已經(jīng)求解過了,如果求解過了,那么直接返回,不需要重復計算。

  • 函數(shù)調(diào)用耗時多、空間復雜度高

    遞歸中會涉及到很多函數(shù)調(diào)用,當函數(shù)調(diào)用的數(shù)量比較多的時候,會使得耗時比較多。同時,由于調(diào)用一次就會在內(nèi)核棧中保存一次現(xiàn)場數(shù)據(jù),因此空間復雜度也會比較大。

1.4. 如何改寫為非遞歸代碼

針對上述遞歸存在的問題,可以將遞歸代碼轉(zhuǎn)化為非遞歸的形式。**一般來說,遞歸代碼都可以改寫成非遞歸代碼的形式。**因為遞歸本身就是借助棧來實現(xiàn)的,只不過遞歸使用的是系統(tǒng)棧或者虛擬機提供的。假如我們自己實現(xiàn)一個棧,模擬入棧、出棧的過程的話,那也是可以的(比如圖的深度優(yōu)先比例時可以使用棧和循環(huán)來實現(xiàn),一般情況都是使用遞歸)。

上述說到了模擬棧的方式,但是在有些遞歸代碼改為非遞歸代碼的形式中,不一定要那么做。**對于同一個問題而言,遞歸代碼是從最大的問題開始,先層層分解,分解完成之后會得到結果,再將結果層層返回,這是有一個有去有回的過程;假如我們知道子問題的答案的話,可以直接從子問題的答案開始,然后子問題求出大的問題的答案,這種相當于只取了歸的過程。**比如有這么個遞歸式:f(n)=f(n-1)+1,終止條件是f(1)=1,那么改為非遞歸的形式,如下所示。下面這種方式,其實就相當于從子問題的答案出發(fā),從而推得更大問題的解,比如 f(1) = 1,推得 f(2) = f(1)+1=2。

int f(int n) {
int ret = 1;
for (int i=2; i <= n; ++i) {
ret = ret + 1;
}
return ret;
}

從上述的例子中我們可以得出這樣一句話,使用遞歸可以讓解決方法更清晰,但是并沒有性能上的優(yōu)勢;而使用循環(huán)的性能更好。

2. 遞歸樹---遞歸代碼的復雜度分析

遞歸代碼的復雜度一般比較難分析,一般可以通過遞推公式推導的方式來求解復雜度。但是有時候遞推公式推導比較繁瑣,這個時候我們可以使用遞歸樹的方式來分析遞歸算法的復雜度。(個人認為其實掌握遞歸樹即可,遞推公式最終也可以轉(zhuǎn)換為遞歸樹,只是遞推公式時沒有顯式的樹過程)。

遞歸的思想就是將大問題分解為小問題來求解,然后再將小問題分解成小小問題。這樣一層層分解,直到問題的數(shù)據(jù)規(guī)模被分解得足夠小,不用繼續(xù)遞歸分解為止。那么將這個過程畫成一顆樹,這顆樹就叫做遞歸樹。

比如斐波那契使用遞歸的方式求解時,就可以畫出下面這樣一顆遞歸樹。節(jié)點里的數(shù)字表示數(shù)據(jù)的規(guī)模,一個節(jié)點的求解可以分解為左右子節(jié)點兩個子問題的求解。

下面通過舉幾個例子來講解遞歸樹求解的方法。

2.1. 歸并排序

歸并排序的每次分解都是一分為二,整個遞歸過程畫成遞歸樹之后如圖所示。m(n) 的時間復雜度為 m(n/2) 的時間復雜度乘以 2,加上合并所需要的時間復雜度。而 m(n/2) 的時間復雜度等于 m(n/4) 的時間復雜度乘以 2,加上合并所需要的時間度。以此類推,最終時間復雜度為 m(1) 乘以 n,再加上這過程的合并操作所需的時間復雜度。

每一層合并操作所需要的時間復雜度是 O(n),m(1) 的時間復雜度為 O(1)。合并的次數(shù)為高度(從 0 開始算),那么最終時間復雜度為?(高度+1)*O(n)。從歸并排序的原理和遞歸樹來看,歸并排序的遞歸樹是一顆滿二叉樹。那么這顆數(shù)的高度為?,因此最終時間復雜度為 O(nlogn)。

2.2. 快速排序

快速排序在最好情況下,每次分區(qū)都能一分為二,那么此時快速排序的遞歸樹和時間復雜度都和歸并排序一樣,都是 O(nlogn)。那么,針對不是一分為二的情況。比如很槽糕的情況,每次都是 1:9 的話。那么對應的遞歸樹如圖所示。

快速排序時,都需要先分區(qū),然后再遞歸。在分區(qū)時,需要遍歷區(qū)間內(nèi)的所有數(shù)據(jù)。因此,每一層的分區(qū)操作所遍歷的數(shù)據(jù)個數(shù)之和是 n。同樣,我們需要求出樹的高度,時間復雜度即為?高度*O(n)。由于每次分區(qū)并不是均勻地一分為二,因此此時的遞歸樹不是滿二叉樹。但是此遞歸樹最長高度可以求得,即最右邊的那個分支,最短高度也可以求得,即最左邊的那個分支。從根節(jié)點到 q(1),最左邊的的深度是?;最右邊的深度是?。因此總體的時間復雜度應該位于??和??之間,由于對數(shù)復雜度不管底數(shù)是多少都可以統(tǒng)一成?,因此快速排序的時間仍然是 O(nlogn)。

假如上述比例變成 1:99,那么類似 1:9 的分析方法,最終的時間復雜度還是 O(nlogn),只要比例是一個常量值之比,那么時間復雜度都是 O(nlogn)。那么平均時間復雜度也是 O(nlogn)。

2.3. 斐波那契

前文拿斐波那契數(shù)列舉了個簡單的例子,下面我們來完完整整地分析一下斐波那契數(shù)列的時間復雜度。斐波那契使用遞歸的方式實現(xiàn)如下所示

int f(int n) {
if (n == 1){
return 1;
}

if (n == 2) {
return 2;
}

return f(n-1) + f(n-2);
}

將整個遞歸的過程畫成遞歸樹,如圖所示。

f(n) 分解為 f(n-1) 和 f(n-2),那么在得到 f(n-1)、f(n-2) 的時間復雜度之后還需要做一個加法操作,該加法操作的時間為 1。那么 f(n-1) 分解成為 f(n-2)、f(n-3) 之后進行加法操作的時間復雜度也是 O(1),因此第二層所需的加法操作時間為 2。依次類推,第 k 層加法時間消耗需要 2^(k-1)。那么,整個算法的時間消耗就是每一層加法的時間消耗之和加上最后的 f(1)、f(2) 所需要的時間操作。

f(n) 分解為 f(n-1) 和 f(n-2),數(shù)據(jù)規(guī)模減少的快慢不一樣。最長路徑的層次應該是 n 層,最短路徑的層次差不多是 2/n 層。因此,最大的時間復雜度為 O(2^n-1),最小的時間復雜度為 O(2^(n/2)-1)。那么,這個算法的時間復雜度介于兩者之間,即時間復雜度是指數(shù)級的。雖然上述的計算過程不是特別精確,但是時間復雜度的數(shù)量級是沒有變的。

1 + 2 + ... + 2^(n-1) = 2^n-1

1 + 2 + ... + 2^(n/2-1) = 2^(n/2)-1

2.4. 全排列

全排列的意思是指把 n 個數(shù)據(jù)的所有排列情況全都找出來。全排列可以采用遞歸的方式實現(xiàn):對于 n 個數(shù)據(jù)的全排列問題,假如我們確定了第一位數(shù)據(jù)(或者最后一位數(shù)據(jù)),那么就變成了剩下的 n-1 個數(shù)據(jù)的排列問題了。并且,第一位數(shù)據(jù)可以是 n 個數(shù)據(jù)中的任意一個,即第一位數(shù)據(jù)有 n 種情況。因此,n 個數(shù)據(jù)的全排列問題就分解成了 n 個 n-1 個數(shù)據(jù)全排列的問題了。因此這就滿足了遞歸的前兩個條件,即原問題的求解可以分解對成 n 個子問題的求解,并且對于這 n 個子問題的求解方式與原問題的求解方式一模一樣,只是數(shù)據(jù)規(guī)模不同。最后是否滿足遞歸的最后一個條件呢?當只剩下 1 個數(shù)據(jù)的時候,遞歸可以終止,因此是存在遞歸終止條件的。

我們將上述的過程寫成遞歸公式如下所示。

f(1, 2, 3, ..., n) = {第一位為1, f(2, 3, ..., n)} + {第一位為2,f(1, 3, ..., n)} + {第一位為n,f(1, 2, ..., n-1)}

將上述的遞歸公式轉(zhuǎn)化為 Java 代碼如下所示

public void fullPermutation(char[] list, int start) {
if (list.length == start) {
System.out.println(list);
}

for (int i = start; i < list.length; i++) {
swap(list, i, start);
fullPermutation(list, start + 1);
swap(list, i, start);
}
}

public void swap (char[] list, int i, int j) {
char temp;
temp = list[i];
list[i] = list[j];
list[j] = temp;
return;
}
public static void main(String[] args) {
new FullPermutation().fullPermutation(newchar[]{'1', '2', '3'}, 0);
}

接下去我們使用遞歸樹的方式來對這段代碼的時間復雜度進行分析。上述的過程可以畫出如下的遞歸樹。第一層有 ?n 個交換操作,第二層有 n 個節(jié)點,每個節(jié)點分解需要 n-1 次交換,所以第二層所需要進行交換的次數(shù)是 n(n-1)。依次類推,第三層所需要的交換次數(shù)是 n(n-1)(n-2),第 k 層所需要的交換次數(shù)是 n(n-1)...(n-k+1),最后一層的交換次數(shù)是 n(n-1)...2。

最終每一層的交換次數(shù)之和就是總的交換次數(shù)之和。最后一層的交換次數(shù)是 n! 次,而其他層的交換次數(shù)肯定小于 n! 次,因此最終的時間復雜度肯定是大于 O(n!),但小于 O(n*n!)。雖然具體的時間復雜度無法求出,但是通過這個范圍也可以知道全排列的時間復雜度是很大的。

★?

上述的三個例子,掌握遞歸樹的求解方式才是最重要的,不要糾結于精確的時間復雜度是多少。

另外個人覺得,遞歸的時間復雜度分析方式只有一種,雖然專欄中說還有遞歸公式,但是遞歸公式其實最終也可以轉(zhuǎn)換為遞歸樹,因此最終還是遞歸樹。

3. 總結

  • 首先需要知道可以使用遞歸的三個條件:問題可以分解成子問題,這些子問題的解法和原問題思路是一樣,最后還需要有終止條件。

  • 其次,在編寫遞歸代碼時,記得先找出遞歸公式以及終止條件,這是第一步。之后再根據(jù)遞歸公式和終止條件寫出代碼,此時很容易。

  • 之后,在思考遞歸問題的時候。一定要注意不要將遞歸想下去,只考慮一層遞歸即可,即假設子問題都已經(jīng)解決。這是我學習該篇內(nèi)容中學到最為有用的一點,也將我之前的那些做法給拋棄掉了。

    在剛接觸遞歸的時候,腦子很容易跟著機器執(zhí)行的順序一層一層套用下去,就像 Debug 一個很深的函數(shù)調(diào)用鏈一樣。這樣往往只有遞的過程,沒有歸的過程,然后在這個過程你也不知道你在哪了。所以,剛接觸的時候遞歸往往讓我覺得很難分析。可能你會覺得在紙上畫圖分析會好一點,其實也會很亂,因為一旦層次一深,你紙上也會很糊涂。所以,在處理遞歸問題時,不一定要 follow 機器的執(zhí)行。在寫遞歸函數(shù)時,可以假設下一層調(diào)用已經(jīng)能夠正確返回了,即子問題已經(jīng)解決掉了。此時調(diào)用自身函數(shù)就像調(diào)用其他函數(shù)那樣,我不管那個函數(shù)怎么執(zhí)行,反正調(diào)用之后給我返回了正確的結果。然后基于這個正確的返回,我只需要考慮怎么將其組合獲得最終問題的解即可。同時,還需要確保最深一層的邏輯,也就是遞歸的終止條件爭取即可。而這樣,中間的所有過程都可以不用考慮。因為不管在中間的哪一層,都是在執(zhí)行同一份代碼,只是數(shù)據(jù)狀態(tài)不同。因此,只要保證了一層的結果正確性,那么整個遞歸過程就是正確的了。

    因此,回到第二點。在寫遞歸代碼之前一定要先正確地寫出遞歸條件和終止條件。根據(jù)寫出來的遞歸公式和終止條件寫出來的代碼。那么就符合上段話中提到的,只思考第一層和最后一層的思想。

    這句話是我從一位那邊大佬借鑒來并結合了自己的理解。機器執(zhí)行遞歸代碼的過程對應的是深度優(yōu)先的方式,而我們思考遞歸的過程應該采用廣度優(yōu)先的方式,個人理解也就是在第一層的時候,我先將其子問題都當做得到了正確的解,然后基于這個我解決第一層的問題。解決完之后,我再解決其中一個子問題的過程。其實,我們在畫上面的遞歸樹時,采用的比較 nice 的方式也是這樣。

  • 碎碎念,來自同一位大佬說的也結合了自己的理解。遞歸這種編程方式的背后,其實是樹和堆棧這兩種看似關聯(lián)不大的數(shù)據(jù)結構。遞歸樹相當于遞歸過程的完整示意圖,也就是說當遞歸完成之后,將它的過程畫出來之后是遞歸樹那樣子的形狀。那么,遞歸樹從根節(jié)點到樹中任意節(jié)點的路徑,都對應著某個時刻的函數(shù)調(diào)用鏈組成的堆棧。遞歸越深的節(jié)點月靠近棧頂,也就越早返回。因此可以說,遞歸的背后是一顆樹,遞歸的執(zhí)行過程其實是在這棵樹上做深度遍歷的過程,每次進入下一層就是壓棧,每次退出當前層就是出棧。而所有入棧出棧的過程就形成了我們上面說的遞歸樹的形態(tài)。遞歸樹是遞歸的靜態(tài)邏輯背景,而當前堆棧的內(nèi)容是動態(tài)運行前景。

    ★?

    在計算某個長度為 n 的入棧序列可以有多少中出棧序列和包含 n 個節(jié)點的二叉樹有多少形狀時,這兩道題的答案其實是相等就是卡特蘭數(shù)。這是因為 n 個節(jié)點形成的一棵二叉樹的后序遍歷對應的就是這 n 個節(jié)點的出棧順序(個人理解是后序遍歷,不是這位大佬說的中序遍歷)。進一步就是說 n 個節(jié)點形成的二叉樹有 x 棵,那么這 x 棵的后序遍歷就對應著 x 種出棧順序。

  • 其他

    對遞歸代碼進行調(diào)試時,可以以下這幾種方式:1. 打印日志發(fā)現(xiàn),遞歸值;2. 結合條件斷點進行調(diào)試。

    另外在數(shù)據(jù)規(guī)模大的情況下請使用非遞歸代碼,使用遞歸代碼很容易造成棧溢出。

- 獲取23套完整的編程視頻資料?-明天見(。・ω・。)ノ?

總結

以上是生活随笔為你收集整理的++递归 字符串全排列_超全递归技巧整理,这次一起拿下递归的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。