程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 遞歸討論(三),遞歸(三)

遞歸討論(三),遞歸(三)

編輯:C++入門知識

遞歸討論(三),遞歸(三)


遞歸對於編程者來說,是比較難理解的,但當你完全清晰程序思路時,它會變得容易理解了。

遞歸思想成為解決一些編程難題所常用的,所以多多練習,多多理解它,會讓我們對之愛不釋手,作為初學者的我,每當理解如何遞歸的解決問題時,就會非常開心,這也許就是挑戰所帶來的成就感,所學即所得的滿足感吧。

下面以如下一道例題為例吧:

在數據結構(c語言版)一書中,頁碼p8,有如下一題

問題1:<全排列>對給定的n個元素的集合,輸出該集合所有可能的排列;

我們對比著看下面一道問題,可以先思考思考:

問題2:<全排列>對給定的n個元素的序列(可能含重復元素),輸出該序列所有可能的排列。(後面來解析該題。)

我們來先說說問題1吧:

即然是n個元素的集合,那麼由眾所周知的集合定義,我們可以明確得出:這n個元素是互不相同的。

那麼問題轉換為求n個不重復元素的全排列,同問題2有著顯著區別哦。

對於該問題:

首先對於全排列:

首字符是這n個元素中的其中一個,如果首字符不同,我們得到的排列也必是不同的。

其次:(核心思想)

我們可以先預定某個元素為首字符,再對剩余字符進行全排列(嘿嘿,此處是不是回歸到我們開始要求的問題了呀,只不過此處變成了對n-1的全排列了啊,這這。。。顯然明確是遞歸的結構啊)

最後:

我們循環的將首字符換成與之前不同的字符(這句話要好好理解,問題2的小核心),再對剩余進行全排列,直至首字符遍歷完n個元素。

文字太枯燥,來個例子,比如:對{a,b,c,d}進行全排列,

以a為頭,對{b,c,d}進行全排列

以b為頭,對{a,c,d}進行全排列

以c為頭,對{a,b,d}進行全排列

以d為頭,對{a,b,c}進行全排列

問題的規模,在遞歸的結構下,由規模4變成了規模3了,然後規模3又在遞歸的結構下變成了規模2.。。。。。最後遞歸到只有1個規模了,那麼此時,說明當前遞歸已到遞歸基上了,說明已進行了一次全排列。此時我們就可以輸出當前的排列了。

運行到此處,我們還要注意些程序實現的細節,不然很難實現最終的結果哦。

先上個代碼,代碼裡有相關說明,會讓你更明白些。

#include<iostream>

void perm(char*,int,int);
int main()
{
    using namespace std;
    char* p=new char[4];
    for(int i=0;i<4;i++)
        cin >> *(p+i);
    perm(p,0,3);
    cout << num;
    for(int i=0;i<4;i++)
        cout << p[i] << "--";
    delete[] p;
    return 0;
}

                                            //遞歸產生的全排列
void perm(char* p,int a,int b)              //a表示待全排的數組下標開始,b表示下標尾
{
    if(a == b)                              //遞歸基,說明只有一個元素了
    {
        for(int i=0;i<=b;i++)               //此時按下標順序輸出當前數組中的元素,因為數組通過交換元素的方式實現了排列。
        {
            std::cout << p[i];
        }
        std::cout << std::endl;
    }
    else
    {
        for(int j=a;j<=b;j++)               //該處的循環作用就是,讓首字符取遍所有可能字符,每次循環,都會將首字符為當前循環
                                                   //字符的所有可能排列全部輸出
        {
            std::swap(p[a],p[j]);           //此處就是通過交換元素的方式讓當前首字符更改,不同於之前循環,也惟有這樣處理,、
                                                   //首字符才可能取遍所有可能字符
              perm(p,a+1,b);                  //此處遞歸調用,因為首字符我們已經確定了,我們只需對剩余元素進行全排列,所以只需作a+1處理即可
              std::swap(p[a],p[j]);           //將數組還原到初始狀態,對於此處,開始我比較難理解,下面畫個調用圖,看看它是如何還原的
        }
    }
}

為了簡化時序分析,我們這裡分析的是3個元素的全排序,其調用如下圖所示:

我們可以在這裡清晰的看到調用的過程,以及相關變量的值變化。我們如果由3個元素推廣更多的元素,我們會發現其中很多自相似的調用圖譜。另外我們也注意到,在一個完整的for循環中,每個循環開始前,p數組的序列都是一樣的。

主要由於兩個swap的作用,也算是此處的小核心了。

也惟有保證每次循環前,p序列回歸原始狀態,我們才能保證下次循環開始,將下一個不同的字符放在首字符。

下面這個圖畫的略草,但還可以湊和著看了。

#include<iostream> void perm(char*,int,int); int main() { using namespace std; bool flag=true; while(flag) { cout << "input the number:"; int n; cin >> n; cout << "enter " << n << " character:"; char* p=new char[n]; for(int i=0;i<n;i++) cin >> *(p+i); perm(p,0,n-1); cout << "continue?(y/n)"; char ch; cin >> ch; if(ch!='y') { flag=false; } delete[] p; } return 0; } void perm(char* p,int a,int b) { if(a == b) { for(int i=0;i<=b;i++) { std::cout << p[i]; } std::cout << std::endl; } else { for(int j=a;j<=b;j++) { bool flag = false; for(int i=a;i<j;i++) { if(p[i]==p[j]) { flag = true; } } if(flag) { continue; } std::swap(p[a],p[j]); perm(p,a+1,b); std::swap(p[a],p[j]); } } }

運行結果如下:

#include<iostream> void perm(char*,int,int,int&); int main() { using namespace std; bool flag=true; while(flag) { int num=0; cout << "input the number:"; int n; cin >> n; cout << "enter " << n << " character:"; char* p=new char[n]; for(int i=0;i<n;i++) cin >> *(p+i); perm(p,0,n-1,num); cout << "the number of permutation:" << num << endl; cout << "continue?(y/n)"; char ch; cin >> ch; if(ch!='y') { flag=false; } delete[] p; } return 0; } void perm(char* p,int a,int b,int& num) { if(a == b) { num++; } else { for(int j=a;j<=b;j++) { bool flag = false; for(int i=a;i<j;i++) { if(p[i]==p[j]) { flag = true; } } if(flag) { continue; } std::swap(p[a],p[j]); perm(p,a+1,b,num); std::swap(p[a],p[j]); } } }

運行結果如下:

image

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved