寫在前面:大家好K。首先爲你點進這篇有趣的文章點贊👍!文章在撰寫過程中難免有疏漏和錯誤,歡迎你在下方留言指出文章的不足之處;如果覺得這篇文章對你有用,也歡迎你點贊和留下你的評論。更多內容請點進👉我的博客K。👈閱覽。
問題:求1、2、3三個數的全排列
1. 分析遞歸問題
老師:同學們好。今天我們來學習一道全排列的例題,首先請問同學們,遞歸函數有那兩個部分?
同學:老師好。遞歸函數包含 遞歸出口和遞歸體。
老師:沒錯,那麼全排列問題的遞歸出口是什麼?
同學:遞歸出口是最簡單的情況,不用計算過程,計算機能秒出答案。全排列中最簡單的情況,就是分解成只有“1個數”的情況:比如,“1”的全排列就是“1”,“2”的全排列就是“3”……
老師:說得對。但是把一個大規模問題的遞歸過程全展開來學習代碼,是十分複雜的,通常我們只選擇最簡單的過程來分析(當然,只有一個數的過程是最簡單的過程,但是這是能直接出答案的過程,對分析問題來說沒有意義,這裏語境中已經排除掉遞歸出口的情形),那麼,全排列問題中,最簡單的過程是什麼呢?
同學:最簡單的過程是隻有兩個數的過程,比如只有兩個數1、2,我們很容易想出來答案是12和21。通過簡單例子來學習遞歸代碼,遞歸層數不會很深,會更有助於理解代碼。老師,您先用兩個數的例子來給我們講解吧。
老師:好。這位同學對於遞歸的基礎已經掌握得很好了,那現在我們通過這道例題來學習一下,看起來很複雜的遞歸代碼,該如何來理解。學會後,對以後學習快速排序、合併排序、動態規劃等經常用到遞歸來解決的問題會很有幫助。現在,我們開始講解吧!
2. 通過分析兩個數的簡單情況寫代碼
老師:我們現在假設有2個數:1、2,它倆的全排列經歷了三步,你們來說一說過程呢?
同學:有下面三個步驟:
----------------------------------------------------------------
|①步,對程序來說,原先的大數組有倆元素:1、2; |
|②步,第一次交換結果僅僅是恰好與初始狀態相同,但也是交換了的。 |
|③步,拆分成兩個小數組:只有一個元素“1”的子數組,只有一個元素“2”的子數組;|
----------------------------------------------------------------
1) 求第一個結果 | 2) 求第二個結果
①初始倆數:12; | ①初始倆數:12;
②交換順序:12; | ②交換順序:21;
③拆分得:1、2; | ③拆分得:2、1;
④組合輸出結果:12; | ④組合輸出結果:21;
老師:沒錯,不管有多少個數,都有三個大步驟:拆分大數組----交換順序----組合輸出結果。那麼來試一下寫出倆數全排列的代碼吧。
同學:老師,我發現組合輸出結果的過程,是最簡單的。我們先寫這部分代碼:
// 數組名爲list
// 首下標是k,尾下標是m
// 當首下標就是尾下標時,說明這是隻有一個數的數組
// 換句話說,原大數組已經被拆分到最小(②③步),可以直接輸出結果了
if (k==m) {
// 循環輸出結果
for (int i = 0; i <= m; i++) {
cout << list[i] << " ";
}
cout << endl;
}
老師:沒錯,這是1個數時,即最簡單的情況下要做的事情,這也是遞歸函數的出口。
同學:老師,交換兩數的代碼可以這樣寫,對嗎?
swap(list[k], list[m]);
老師:這個代碼在只有兩個數時是沒問題的,但是如果數多了以後,比如,給一組數:1、2、3。k是首下標,m是尾下標。如果僅僅交換這一頭一尾的值,交換是不徹底的,永遠是123、321,“2”沒有做過第一位。所以要通過引入第三個變量,來使數組中所有數都做過第一位。
同學:老師,我們可以通過for循環的控制變量i,來遍歷所有情況:
// k永遠都是數組的第一個下標,以後拆分成小數組後,k是各小數組的第一位
// i值從k開始,直到和m相等,意味着所有數都做過第一位
for (int i = k; i <= m; i++) {
swap(list[k], list[i]);
}
老師:這就對了!現在我們已經把交換順序和組合輸出結果的代碼都寫出來了:
void perm(int list[], int k, int m) {
// 組合輸出結果
if (k == m) {
for (int i = 0; i <= m; i++) {
cout << list[i] << " ";
}
cout << endl;
}
// 交換順序
else {
for (int i = k; i <= m; i++) {
swap(list[k], list[i]);
}
}
}
那麼拆分的代碼怎麼寫呢?
同學:將大數組拆分成小數組,是重複的過程,並且每次範圍都要變小(遞歸,代碼如下)。其實變量i值從k開始循環的過程,就已經隱含了拆分的步驟。小數組從大數組中來,編程中實際一直是用一個數組來存儲原數組,但是人爲通過k值的變化(遞歸函數調用時k+1),將大數組拆分成想象的小數組。因此循環並不是每次都從原數組的首下標開始。每次循環的首下標都是新的小數組的首下標(即是從i=k開始而不是0開始)。
void perm(int list[], int k, int m) {
// 組合輸出結果
if (k == m) {
for (int i = 0; i <= m; i++) {
cout << list[i] << " ";
}
cout << endl;
}
// 交換順序
else {
for (int i = k; i <= m; i++) {
swap(list[k], list[i]);
// 這裏是遞歸,函數自己調用自己
// 每次調用該函數時,list(數組名)、m(尾下標)倆值不變
// k值每次都往m靠攏1位,到最後k會與m相等,到達遞歸出口
perm(list, k + 1, m);
}
}
}
3. 消滅多個數的“特殊”情況
老師:做到這一步已經很棒了。但是運行一下會發現,只有兩個數的時候結果是正確,到三個數以上就不行了。這是哪兒出問題了呢?我們看一下同學們以兩個數爲例描述的步驟:
1) 求第一個結果 | 2) 求第二個結果
①初始倆數:12; | ①初始倆數:12;
②交換順序:12; | ②交換順序:21;
③拆分得:1、2; | ③拆分得:2、1;
④組合輸出結果:12; | ④組合輸出結果:21;
我們可以看到,1)的初始數是12,2)的初始數也是12,說明每次開始處理的初始狀態是相同的,但是爲啥兩個數的時候運行正確呢?原因是,2個數的情況太簡潔了,沒有體現出這種錯誤。1)的②步交換順序的結果,恰好是與初始狀態相同的,當2)又用到原數組時,自然拿到的數組的順序貌似就沒有變過。
老師:如果是123三個數的話(如下圖描述),1)大步①②交換順序時,同樣與初始狀態相同的,這一步正確✅,③步拆分得到1和23,還沒完,23還需要拆分,然後23被拆分再組合成23和32(就是倆數全排列的情形)✅,即1)大步執行結果就是123、132。但是我們發現了,原數組123經過這幾個交換組合後,變成了132。再拿去給2)大步執行,就錯了
1) 分而治之
①初始仨數:123;
②交換順序:123;
③拆分得:1、23;
④“1”已最簡,“23”需再拆分
******************************
*注:對計算機來說,到這裏數組是123*
******************************
2) 求第一個結果 | 3) 求第二個結果
①初始倆數:23; | ①初始倆數:23;
②交換順序:23; | ②交換順序:32;
③拆分得:2、3; | ③拆分得:3、2;
④組合輸出結果:123; | ④組合輸出結果:132;
******************************
*注:對計算機來說,到這裏數組是123*
******************************
******************************
*注:對計算機來說,到這裏數組是132*
******************************
同學:老師,我明白了,3)大步之後,如果再不對數組順序進行恢復,之後的結果就會出錯。事實上,1)2)大步也是需要對數組恢復原順序的,只是剛好交換完成的順序與初始狀態相同,僥倖逃過了錯誤。所以我們應該加一行代碼,讓順序再換回來:
void perm(int list[], int k, int m) {
// 組合輸出結果
if (k == m) {
for (int i = 0; i <= m; i++) {
cout << list[i] << " ";
}
cout << endl;
}
// 交換順序
else {
for (int i = k; i <= m; i++) {
swap(list[k], list[i]);
perm(list, k + 1, m);
// 上面交換後這裏再交換,順序恢復如初
swap(list[k], list[i]);
}
}
}
老師:沒錯,同學們真聰明!
4. 完整代碼
同學:這就是我們的全部代碼:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}
void perm(int list[], int k, int m) {
if (k == m) {
for (int i = 0; i <= m; i++) {
cout << list[i] << " ";
}
cout << endl;
} else {
for (int i = k; i <= m; i++) {
swap(list[k], list[i]);
perm(list, k + 1, m);
swap(list[k], list[i]);
}
}
}
int main() {
int list[] = {1, 2, 3, 4};
perm(list, 0, 3);
return 0;
}
代碼運行結果: