「奇技淫巧」 話遞歸

「To Iterate is Human, to RecursDivine.」 ---L. Peter Deutsch node

 

「迭代是人,遞歸是神」 第一次見有人這樣說,讓我受傷的心獲得些許安慰......ios

 

最近在琢磨算法,又見遞歸! 這是個繞不過去的坎! 當初,上大學時似懂非懂自欺欺人的矇混過關,再次引證了那句名言:「出來混,早晚都是要還的......」。好吧,那就直面它!因而搜遍海內外,加上日思夜想,被這「奇技淫巧」折魔得真掉了很多頭髮(主要是8皇后問題~)。算法

 

大神王垠在談程序語言最精華的原理時提到遞歸,並說遞歸循環表達能力強不少,並且效率幾乎同樣!!!沒有必定的內力,估計你很難理解他這句話,固然他說得沒錯!編程

 

務必要弄懂弄透它!抱着一股不服的擰勁和不死的初心,蒙生了收集全部經典遞歸案例寫法做一彙總的想法。數據結構

 

若是您不能像大神同樣一眼就看穿其本質並熟練的運用它(不自欺),不妨一塊兒來領略領略這「奇技淫巧」的各案之美, 由簡到難,慢慢的你必定會以爲它愈來愈美!框架

 

如發現還有文中沒收集到的經典例子,歡迎補充添加,以澤後人,算積功德一件~測試

 

做者承諾:全部代碼都通過測試,全部評論都是算法在腦中真真切切過了一遍以後的切膚反饋。ui

 

經典遞歸例子彙總與點評:spa

 

1. N!,求N的階乘設計

 

數學定義:

 

 

 

1 //求N! 
2 long int F( long int N)
3 {
4         if(N==0)
5                 return 1;
6         if(N>0)
7                 return N*F(N-1);
8 }

 

1  
2 int main( int argv, char** argc){
3         long int N;
4         cin>>N;
5         cout<<F(N)<<endl;
6 }

 

 

對着數學公式寫代碼是否是很容易?這個用遞歸比循環更易寫,更容易理解!閉着眼睛,想一想循環怎麼寫?...是否是有點囉嗦~

 

 

 

2. 1+2+3+.....+n,求前N項和

 

這個是編程用循環的入門思惟,用for語句太簡單,若是讓以遞歸方式寫,不少人可能又會卡一下子了。可是若是你把它按數學公式的方式定義一下,和N!同樣,遞歸就好寫多了!

數學定義:

 

 

 1 int Sum(int N)
 2 {
 3  
 4 if(N==1)
 5 return 1;
 6 if(N>1)
 7 return N+Sum(N-1);
 8 else
 9 return 0;
10 }
11  

 

按數學公式寫代碼,是否是讓生活更美好!~

 

 

 

3. Fibonacci數列,F(n)=F(n-1)+F(n-2)

 

數學定義:

 

1 int Fibonacci(int N)
2 {
3 if(N==1||N==2)
4 return 1;
5 if(N>2)
6 return Fibonacci(N-1)+Fibonacci(N-2);
7 else
8 return 0;
9 }

 

 

這個遞歸很好寫,若是閉眼用循環寫,估計要點時間,有幾個變量須要耐心引入。

 

 

 

4. GCD(a,b),求最大公約數

 

始祖毆基裏德給出了展轉相除的遞歸原則,知道這個寫起來就容易多了,但理解天才的想法是如何得來的仍是要費點腦子的。

 

我曾試圖去網上找找它的數學定義,很遺憾大都是些拗口的文字描述,代碼反而容易理解一些。

 

 1 int GCD(int a, int b)
 2 {
 3   if(a>0&&b>0){
 4         if(a%b==0){
 5                 return b;
 6         }else{
 7                 return GCD(b,a%b);
 8         }
 9    }
10 }

 

 

因而從代碼中反推出其數學公式~:

 

 

 

5. Hanoi塔,從A移到C

 

真是佩服那個出題的和尚,高僧!!!固然會解題的也是高人。

 

 

如上圖,要把A中全部圓盤經B輔助移到C,移動過程當中,要求園盤之上始終只能是比其小的圓盤。規則描述不復雜,但怎麼把它轉化成數學定義呢?懵!先從code入手?

 

 1  void Hanoi(int N, char source, char auxiliary, char target)
 2 {
 3  
 4 if(N==1){
 5 cout<<"Move disk: "<<N<<" from "<<source<<" to "<<target<<endl;
 6 }else {
 7 Hanoi(N-1,source,target,auxiliary);
 8 cout<<"Move disk: "<<N<<" from "<<source<<" to "<<target<<endl;
 9 Hanoi(N-1,auxiliary,source,target);
10 }
11 }
12  
13 Hanoi(5,'A','B','C');

 

 

仍是無法寫成數學恆等式,爲何?由於在其遞歸終點再也不是返回值以供調用者使用,而是執行了一次操做,依次返回過程當中都是再執行一次操做,不是數量表達關係,所以較難用數學定義語言描述這類問題。

 

 

 

 

6. 迴文數斷定

 

不用遞歸,夠你死一丟腦細胞的。迴文數是指先後對稱的數,如(112112321等)。

假設數字都以字符串的形式存儲,爲了大數判斷和通常的通用迴文判斷,採用這種形式

 

 1 bool isPali(string S, int startindex)
 2 {
 3 if(S.size()==1||startindex>=S.size()-1-startindex){
 4 return true;
 5 }
 6 if(S[startindex]!=S[S.size()-1-startindex]){
 7 return false;
 8 }
 9  
10 return isPali(S, startindex+1);
11 }
12  
13 int main(){
14 string S;
15 cin>>S;
16 cout<<isPali(S,0)<<endl;
17 }

 

 

試試把它數學定義式寫出來:

 

 

7. 楊輝三角

老祖宗蠻厲害,但這樣說,彷佛有點類我黨自詡之嫌~

 

把它轉化成(row,col)直角形式以下

 

要獲得數字的值, 遞歸算法以下:

 

 1 int GetVOfYangHui(int row, int col)
 2 {
 3 if(col<=row && col>=0){
 4 if(col==0||row==col){
 5 return 1;
 6 }else{
 7 return GetVOfYangHui(row-1,col-1)+GetVOfYangHui(row-1,col);
 8 }
 9 }else {
10 return 0;
11 }
12 }

 

 

試試把它數學定義式寫出來:

 

 

 

8. 快速排序,二路歸併

 

讓你悶頭寫一個,是否是也有點難度啊?

 

1. 快速排序

主框架用遞歸的思惟很簡單,稍麻煩一點的是分割DPart,須要用到一點編程的技巧。

 1 int DPart(int* A, int start, int end)
 2 {
 3         int key=A[end];
 4         int index=end;
 5         while(start<end){
 6  
 7                 while(A[start]<key){ start++;}
 8                 while(A[end]>=key){end--;}
 9                 if(start<end){ swap(A[start],A[end]);}
10                 else{
11                         break;
12                 }
13  
14         }
15         swap(A[start],A[index]);
16         return start;
17 }
18  
19 void QuickSort(int* A, int start, int end)
20 {
21         if(start>end||start<0||end<0){
22                 return;
23         } else {
24                 int index=DPart(A,start,end);
25                 QuickSort(A,start,index-1);
26                 QuickSort(A,index+1,end);
27         }
28  
29 }
30  

 

2. 二路歸併

主框架用遞歸的思惟也很簡單,關鍵在寫Merge時,須要用到一點點編程的技巧。

 

 1 void Merge(int A[], int low, int mid, int high)
 2 {
 3 int n1= mid-low+1;
 4 int n2= high-mid;
 5  
 6       int L[n1],R[n2];
 7 for(int i=0;i<n1;i++)
 8 L[i]=A[i+low];
 9  
10 for(int j=0;j<n2;j++)
11 R[j]=A[j+mid+1];
12  
13 int i=0,j=0,k=low;
14  
15 while(i!=n1 && j!= n2)
16      {
17          if(L[i] <= R[j])
18              A[k++] = L[i++];
19          else
20              A[k++] = R[j++];
21      }
22  
23         while(i < n1)
24            A[k++] = L[i++];
25         while(j < n2)
26           A[k++] = R[j++];
27  
28 }
29  
30 void MergeSort(int A[], int low, int high)
31 {
32 if(low<high){
33 int mid = (low+high)/2;
34 MergeSort(A,low,mid);
35 MergeSort(A,mid+1,high);
36  
37 Merge(A,low,mid,high);
38 }
39 }

 

 

 

 

9. Bs Tree(二叉樹的前,中,後序遍歷)

 

這個可算是搞數據結構設計的人把遞歸思想發揮到極致的經典案例吧?

 

這部分code 有些冗長,爲了完整性,仍是把它全貼出來。主要目的是體會其前,中,後序遍歷的遞歸寫法。爲了建樹方便,咱們以數據輸入順序按層序方式建樹,它須要用到隊列技巧(與遞歸無關,暫不討論),同時加入一個層序遍歷方法來驗證輸入。注意層序遍歷很難用遞歸方法實現,我思考了好久都沒有結果,若是你有想到,必定告知一聲,萬謝!mathmad@163.com)

 

  1 #include <iostream>
  2 #include <queue>
  3 #include <stdio.h>
  4  
  5 using namespace std;
  6  
  7 struct Node {
  8 int data;
  9 Node* left;
 10 Node* right;
 11 };
 12  
 13 class Btree
 14 {
 15     public:
 16         Btree();
 17         ~Btree();
 18  
 19 Node *createNode(int data);
 20 Node *GetRoot();
 21        void insert(Node *newNode);
 22        void destroy_tree();
 23     void levOrder(Node *root);
 24     void preOrder(Node *root);
 25     void midOrder(Node *root);
 26     void posOrder(Node *root);
 27     private:
 28         void destroy_tree(Node *leaf);
 29         Node *root;
 30 queue<Node *> q;
 31 };
 32  
 33 Btree::Btree()
 34 {
 35 root=NULL;
 36 }
 37 Btree::~Btree()
 38 {
 39 if(root!=NULL){
 40 destroy_tree(root);
 41 }
 42 }
 43  
 44 void Btree::destroy_tree(Node *leaf)
 45 {
 46 if(leaf!=NULL)
 47 {
 48 destroy_tree(leaf->left);
 49     destroy_tree(leaf->right);
 50     delete leaf;
 51   }
 52 }
 53 Node* Btree::createNode(int data)
 54 {
 55 Node* n=new Node;
 56 n->left=NULL;
 57 n->right=NULL;
 58 n->data=data;
 59 return n;
 60 }
 61 Node* Btree::GetRoot()
 62 {
 63 return root;
 64 }
 65  
 66 void Btree::insert(Node *newnode)
 67 {
 68 if(NULL==root){
 69   root=newnode;
 70   q.push(root);
 71 }else{
 72   Node *f=q.front();
 73   if(f->left&&f->right){q.pop(); f=q.front();}
 74   if(!f->left){ f->left=newnode;}
 75   else if(!f->right){ f->right=newnode;}
 76   q.push(newnode);
 77  
 78 }
 79  
 80 }
 81  
 82 void Btree::levOrder(Node* root)
 83 {
 84 if(root){
 85 queue<Node*> Q;
 86 Q.push(root);
 87  
 88 while(!Q.empty()){
 89 Node *d=Q.front();
 90 cout<<d->data<<" ";
 91 Q.pop();
 92 if(d->left){Q.push(d->left);}
 93 if(d->right){Q.push(d->right);}
 94 }
 95  
 96 }
 97  
 98 }
 99  
100 void Btree::preOrder(Node* root)
101 {
102 if(NULL==root) { return;}
103 else{
104 cout<<root->data<<" ";
105 preOrder(root->left);
106 preOrder(root->right);
107 }
108  
109 }
110  
111 void Btree::midOrder(Node *root)
112 {
113 if(NULL==root) { return;}
114 else{
115 midOrder(root->left);
116 cout<<root->data<<" ";
117 midOrder(root->right);
118 }
119  
120 }
121  
122 void Btree::posOrder(Node *root)
123 {
124 if(NULL==root) { return;}
125 else{
126 posOrder(root->left);
127 posOrder(root->right);
128 cout<<root->data<<" ";
129 }
130  
131 }
132  
133 int main( int argv, char** argc){
134  
135 Btree btree;
136 Node * newnode;
137 int data;
138 cout<<"Please input a sequences number to create a Complete Binary Tree:"<<endl;
139  
140 do{
141 cin>>data;
142 newnode=btree.createNode(data);
143 btree.insert(newnode);
144 }while(getchar()!='\n');
145  
146 cout<<"BTree Level order is:"<<endl;
147 btree.levOrder(btree.GetRoot());
148 cout<<endl<<"Pre Order is:"<<endl;
149 btree.preOrder(btree.GetRoot());
150 cout<<endl<<"mid Order is:"<<endl;
151 btree.midOrder(btree.GetRoot());
152 cout<<endl<<"pos Order is:"<<endl;
153 btree.posOrder(btree.GetRoot());
154 cout<<endl;
155 }

 

 

g++ 編譯上述代碼(g++ BTree.cpp),測試輸出以下:

 

 

 

 

 

10. 全排列

 

這個有點難想清楚!特別是第二個swap交換!

 

N個元素的全排列,有高中數學基礎的人都易知道它總共有N!(階乘)種。若要所有打印出來,當N>4時仍是有必定麻煩,特別是當用循環思路正面強攻時,會讓人陷入無盡的深淵!

 

下面以5個數字爲例簡述其原理:

假設數據集爲{1, 2, 3, 4, 5}如何編寫全排列的遞歸算法

1、首先看最後兩個數4, 5。 它們的全排列爲4 55 4, 即以4開頭的5的全排列和以5開頭的4的全排列。
因爲一個數的全排列就是其自己,從而獲得以上結果。
2、再看後三個數3, 4, 5。它們的全排列爲3 4 53 5 44 3 54 5 35 4 35 3 4 六組數。
3開頭的和4,5的全排列的組合、以4開頭的和3,5的全排列的組合和以5開頭的和4,3的全排列的組合,即k(k+1,..N)的全排列組合加上k與(k+1,...N)的全排列組合,其中kk(k+1,...N)的任一置換。

網上通用通常描述爲,設一組數p = {r1, r2, r3, ... ,rn}, 全排列爲perm(p)pn = p - {rn}
所以perm(p) = r1perm(p1), r2perm(p2), r3perm(p3), ... , rnperm(pn)。當n = 1perm(p} = r1
爲了更容易理解,將整組數中的全部的數分別與第一個數交換,這樣就老是在處理後n-1個數的全排列。(下面給出C++, 以字符串爲數據集的全排列算法)

 

 1 #include <iostream>
 2 using namespace std;
 3 /*
 4 全排列算法改進
 5 Author: Liang 2018-09-21
 6   
 7 */
 8  
 9 int Perm(string s, int k, int m) {
10 static int n=0;
11  
12 if (s!= ""&& m>=k && m<=s.size()-1 && k>=0){ 
13             if(k == m){ 
14             cout<<s<<endl;
15 n++;
16             }else {
17                 Perm(s, k+1, m); 
18  
19                 for(int i = k+1; i <= m; i++) {
20                     swap(s[k],s[i]); 
21                     Perm(s, k+1, m); 
22                     swap(s[k],s[i]); 
23                 }
24             }
25         }
26 return n;
27 }
28  
29 int main(){
30 string str;
31 cin>>str;
32 int n=str.size();
33 cout<<"total: "<<Perm(str,0,n-1)<<endl;
34 }

 

 

 

 

11. 8 Queen problem

 

國際象棋棋盤上放8個皇后,問有多少种放法(皇后的走法是水平垂直線和對角線位置能夠互相攻擊) 著名的8皇后問題,這個Gauss都只得出76種解(實際有92種),一想到這,我心理就平衡一點~

 

8 皇后問題後演變成N皇后問題,其中N=1時有一個解, 23無解,N=4時有兩個解,N=5時比N=6的解多。這說明什麼問題? 一個「老婆」最穩定,23個後宮無法處理,挺到4個時就會有辦法,5個「老婆」比6個老婆好處置,超過6個就是多多益善了,要想處理8個皇后,高斯都沒想清楚,可見多了後宮難治啊,哈哈,閒扯遠了~

 

 

 1 #include <iostream>
 2 #include <cstdlib>
 3 using namespace std;
 4 #define StackSize 8 // could be set to 1, 4, 5, 6, 7,....N                
 5  
 6 int ans=0;                          
 7 int top=-1;                         
 8 int ColOfRow[StackSize]; //index represent the row, value is col         
 9  
10 void Push(int col) 
11 {
12     top++;                  
13     ColOfRow[top]=col;
14 }
15  
16 void Pop()
17 {
18     top--;
19 }
20 // check put a Queen to position(row, col) is safe or not.
21 bool isPosSafe(int row, int col)
22 {
23     for(int i=0;i<row;i++)
24     { 
25 //check col is ok, row is increase invoke, same is impossible
26         if(col==ColOfRow[i]){
27 return false;
28 }
29 // Y=kX+b, k=1 or k=-1, the Queen will attack each other 
30 if(abs(col-ColOfRow[i])==(row-i)){ 
31 return false;  
32 }
33     }                            
34     return true;           
35 }
36  
37 void PrintBoard() 
38 {
39     cout<<"NO."<<ans<<":"<<endl;                                    
40     for(int i=0;i<StackSize;i++)
41     {
42         for(int j=0;j<ColOfRow[i];j++)
43             cout<<"_ "; 
44         cout<<"Q"; 
45         for(int j=StackSize-1;j>ColOfRow[i];j--)
46             cout<<" _";
47         cout<<endl; 
48     }
49     cout<<endl;
50 }
51 // Should be start from 0 row, since the start point will impact future steps desision
52 void PlaceQueen(int row) 
53 {
54     for (int col=0;col<StackSize;col++)    
55     {
56         Push(col);
57         if (isPosSafe(row,col))        
58         {
59             if (row<StackSize-1) 
60                 PlaceQueen(row+1); 
61             else
62             {
63                 ans++; 
64                 PrintBoard(); 
65             }
66         }
67         Pop();                     
68     }
69 }
70  
71  
72 int main()
73 {
74     // Since Recursion invoke, start point should be 0, the first row
75     PlaceQueen(0); 
76     cout<<"the total solutions is:"<<ans<<endl; 
77     return 0;
78 }
79  
80  

 

 

 

 

 

12. 約瑟夫環問題

 

沒有數學遞歸公式,循環怕是很難搞清

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息