遞歸思想及應用(二十八)

        今天我們來看看遞歸,那麼我們爲什麼要講遞歸呢?在後面的數據結構的學習中會用到遞歸的思想。遞歸是一種數學上分而自治的思想,將原問題分解爲規模較小的問題進行處理。分解後的問題與原問題的類型完全相同,但規模較小;通過小規模問題的分解,能夠輕易求得原問題的解。

        但問題的分解是有限的(遞歸不能無限進行),當邊界條件不滿足時,分解問題(遞歸繼續進行);當邊界條件滿足時,直接求解(遞歸結束)。下來我們來看看遞歸模型的一般表示法,如下

圖片.png

        遞歸在程序設計中的應用 -- 遞歸函數。1、函數體中存在自我調用的函數;2、遞歸函數必須有遞歸出口(邊界條件);3、函數的無線遞歸將導致程序崩潰。下來我們來看看遞歸思想的應用:

        1、求解:Sum( n ) = 1 + 2 + 3 + ... + n

圖片.png

        源碼如下

#include <iostream>

using namespace std;

unsigned int sum(unsigned int n)
{
    if( n > 1 )
    {
        return n + sum(n-1);
    }
    else
    {
        return 1;
    }
}

int main()
{
    cout << "sum(100) = " << sum(100) << endl;

    return 0;
}

        我們來看看運行結果

圖片.png

        2、斐波拉契數列:數列自身遞歸定義:1、1、2、3、5、8、13、21、...,它的定義是後一個的數字是前兩個數字之和。

圖片.png

        源碼如下

#include <iostream>

using namespace std;

unsigned int fac(unsigned int n)
{
    if( n > 2 )
    {
        return fac(n-1) + fac(n-2);
    }

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

    return 0;
}

int main()
{
    for(int i=1; i<10; i++)
    {
        cout << i << " : " << fac(i) << endl;
    }

    return 0;
}

        運行結果如下

圖片.png

        我們看到前 9 個的數字是完全一樣的。

        3、用遞歸的方法編寫函數求字符串長度,方法如下

圖片.png

        源碼如下

#include <iostream>

using namespace std;

unsigned int _strlen_(const char* s)
{
    if( *s != '\0' )
    {
        return 1 + _strlen_(s+1);
    }
    else
    {
        return 0;
    }
}

int main()
{
    cout << _strlen_("abc") << endl;

    return 0;
}

        運行結果如下

圖片.png

        那麼我們上面的字符串長度的求解方法還有待優化,我們可以將上面的函數優化成下面的代碼

#include <iostream>

using namespace std;

unsigned int _strlen_(const char* s)
{
    return s ? (*s ? (1 + _strlen_(s+1)) : 0) : 0;
}

int main()
{
    cout << _strlen_("abcdef") << endl;

    return 0;
}

        我們來看看結果

圖片.png

        4、單向鏈表的轉置,如下

圖片.png

        源碼實現如下

#include <iostream>
#include <cstring>

using namespace std;

struct Node
{
    int value;
    Node* next;
};

Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

    for(int i=0; i<len; i++)
    {
        Node* n = new Node();

        n->value = v++;
        n->next = NULL;

        if( slider == NULL )
        {
            slider = n;
            ret = n;
        }
        else
        {
            slider->next = n;
            slider = n;
        }
    }

    return ret;
}

void destory_list(Node* list)
{
    while( list )
    {
        Node* del = list;

        list = list->next;

        delete del;
    }
}

void print_list(Node* list)
{
    while( list )
    {
        cout << list->value << "->";

        list = list->next;
    }

    cout << "NULL" << endl;
}

Node* reverse(Node* list)
{
    if( (list == NULL) || (list->next == NULL) )
    {
        return list;
    }
    else
    {
        Node* guard = list->next;
        Node* ret = reverse(list->next);

        guard->next = list;

        list->next = NULL;

        return ret;
    }
}

int main()
{

    Node* list = create_list(1, 5);

    print_list(list);

    list = reverse(list);

    print_list(list);

    destory_list(list);

    return 0;
}

        運行結果如下

圖片.png

        我們看到第一次單向鏈表是 1->2->3->4->5->NULL;經過轉置後的結果是 5->4->3->2->1->NULL

        5、單向排序鏈表的合併,如下

圖片.png

        源碼實現如下

#include <iostream>
#include <cstring>

using namespace std;

struct Node
{
    int value;
    Node* next;
};

Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

    for(int i=0; i<len; i++)
    {
        Node* n = new Node();

        n->value = v++;
        n->next = NULL;

        if( slider == NULL )
        {
            slider = n;
            ret = n;
        }
        else
        {
            slider->next = n;
            slider = n;
        }
    }

    return ret;
}

void destory_list(Node* list)
{
    while( list )
    {
        Node* del = list;

        list = list->next;

        delete del;
    }
}

void print_list(Node* list)
{
    while( list )
    {
        cout << list->value << "->";

        list = list->next;
    }

    cout << "NULL" << endl;
}

Node* merge(Node* list1, Node* list2)
{
    if( list1 == NULL )
    {
        return list2;
    }
    else if( list2 == NULL )
    {
       return list1;
    }
    else if( list1->value < list2->value )
    {
        return (list1->next = merge(list1->next, list2), list1);
    }
    else
    {
        return (list2->next = merge(list1, list2->next), list2);
    }
}

int main()
{

    Node* list1 = create_list(1, 5);
    Node* list2 = create_list(2, 6);

    print_list(list1);
    print_list(list2);

    Node* list = merge(list1, list2);

    print_list(list);

    destory_list(list);

    return 0;
}

        運行結果如下

圖片.png

        我們看到經過合併後的鏈表如上圖所示,都是由小到大的排序進行輸出。

        6、漢諾塔問題:a> 將木塊藉助 B 柱由 A 柱移動到 C 柱;b> 每次只能移動一個木塊;c> 只能出現小木塊在大木塊之上。如下

圖片.png

        下來我們將漢諾塔問題進行分解,將 n-1 個木塊藉助於 C 柱由 A 柱移動到 B 柱,將最底層的唯一木塊直接移動到 C 柱,將 n-1 個木塊藉助 A 柱移動到 C 柱。如下圖所示

圖片.png

        源碼實現如下

#include <iostream>
#include <cstring>

using namespace std;

struct Node
{
    int value;
    Node* next;
};

Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

    for(int i=0; i<len; i++)
    {
        Node* n = new Node();

        n->value = v++;
        n->next = NULL;

        if( slider == NULL )
        {
            slider = n;
            ret = n;
        }
        else
        {
            slider->next = n;
            slider = n;
        }
    }

    return ret;
}

void destory_list(Node* list)
{
    while( list )
    {
        Node* del = list;

        list = list->next;

        delete del;
    }
}

void print_list(Node* list)
{
    while( list )
    {
        cout << list->value << "->";

        list = list->next;
    }

    cout << "NULL" << endl;
}

void HanoiTower(int n, char a, char b, char c)  // a ==> src  b ==> middle  c ==> dest
{
    if( n == 1 )
    {
        cout << a << "-->" << c << endl;
    }
    else
    {
        HanoiTower(n-1, a, c, b);
        HanoiTower(1, a, b, c);
        HanoiTower(n-1, b, a, c);
    }
}

int main()
{
    HanoiTower(3, 'A', 'B', 'C');

    return 0;
}

        運行結果如下

圖片.png

        7、全排列問題,如下圖所示

圖片.png

        源碼實現如下

#include <iostream>
#include <cstring>

using namespace std;

struct Node
{
    int value;
    Node* next;
};

Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

    for(int i=0; i<len; i++)
    {
        Node* n = new Node();

        n->value = v++;
        n->next = NULL;

        if( slider == NULL )
        {
            slider = n;
            ret = n;
        }
        else
        {
            slider->next = n;
            slider = n;
        }
    }

    return ret;
}

void destory_list(Node* list)
{
    while( list )
    {
        Node* del = list;

        list = list->next;

        delete del;
    }
}

void print_list(Node* list)
{
    while( list )
    {
        cout << list->value << "->";

        list = list->next;
    }

    cout << "NULL" << endl;
}

void permutation(char* s, char* e)
{
    if( *s == '\0' )
    {
        cout << e << endl;
    }
    else
    {
        int len = strlen(s);

        for(int i=0; i<len; i++)
        {
            swap(s[0], s[i]);
            permutation(s+1, e);
            swap(s[0], s[i]);
        }
    }
}

int main()
{
    char s[] = "abc";

    permutation(s, s);

    return 0;
}

        我們來看看運行結果

圖片.png

        不過我們來看看字符串 “abc”的輸出

圖片.png

        我們看到還是重複輸出了。那麼如果前面和後面每個字符是一樣的,我們就不用進行遞歸了。優化後的源碼如下

void permutation(char* s, char* e)
{
    if( *s == '\0' )
    {
        cout << e << endl;
    }
    else
    {
        int len = strlen(s);

        for(int i=0; i<len; i++)
        {
            if( (i == 0) || (s[0] != s[i]) )
            {
                swap(s[0], s[i]);
                permutation(s+1, e);
                swap(s[0], s[i]);
            }
        }
    }
}

        運行結果如下

圖片.png

        通過對遞歸的學習,總結如下:1、遞歸是一種將問題分而自治的思想;2、用遞歸解決問題首先要建立遞歸的模型;3、遞歸解法必須要有邊界條件,否則無解;4、不要陷入遞歸函數的執行細節,要學會通過代碼描述遞歸問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章