1. 程式人生 > >資料結構開發(15):遞迴的思想與應用

資料結構開發(15):遞迴的思想與應用

0.目錄

1.遞迴的思想

2.遞迴的應用

3.小結

1.遞迴的思想

遞迴是一種數學上分而自治的思想:

  • 將原問題分解為規模較小的問題進行處理
    1. 分解後的問題與原問題的型別完全相同,但規模較小
    2. 通過小規模問題的解,能夠輕易求得原問題的解
  • 問題的分解是有限的 ( 遞迴不能無限進行 )
    1. 當邊界條件不滿足時,分解問題 ( 遞迴繼續進行 )
    2. 當邊界條件滿足時,直接求解 ( 遞迴結束 )

遞迴模型的一般表示法:

遞迴在程式設計中的應用:

  • 遞迴函式
    1. 函式體中存在自我呼叫的函式
    2. 遞迴函式必須有遞迴出口 ( 邊界條件 )
    3. 函式的無限遞迴將導致程式崩潰

遞迴思想的應用:

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

遞迴求和:

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

斐波拉契數列:
數列自身遞迴定義:1, 1, 2, 3, 5, 8, 13, 21, ...

斐波拉契數列:

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;
}

用遞迴的方法編寫函式求字串長度

用遞迴的方法編寫函式求字串長度:

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

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

2.遞迴的應用

2.1 單向連結串列的轉置

預備的單鏈表:

#include <iostream>

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 destroy_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;
}

int main()
{
    Node* list = create_list(1, 5);

    print_list(list);

    destroy_list(list);

    return 0;
}

執行結果為:

1->2->3->4->5->NULL

單向連結串列的轉置:

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);

    destroy_list(list);

    return 0;
}

執行結果為:

1->2->3->4->5->NULL
5->4->3->2->1->NULL

2.2 單向排序連結串列的合併

單向排序連結串列的合併:

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

        list1->next = list;

        return list1;
    }
    else
    {
        Node* list_2 = list2->next;
        Node* list = merge(list1, list_2);

        list2->next = list;

        return 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);

    destroy_list(list);

    return 0;
}

執行結果為:

1->2->3->4->5->NULL
2->3->4->5->6->7->NULL
1->2->2->3->3->4->4->5->5->6->7->NULL

程式碼優化:

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);
    }
}

2.3 漢諾塔問題

漢諾塔問題:

  • 將木塊藉助 B 柱由 A 柱移動到 C 柱
  • 每次只能移動一個木塊
  • 只能出現小木塊在大木塊之上

漢諾塔問題分解:

  • 將 n-1 個木塊藉助 C 柱由 A 柱移動到 B 柱
  • 將最底層的唯一木塊直接移動到 C 柱
  • 將 n-1 個木塊藉助 A 柱由 B 柱移動到 C 柱

漢諾塔問題:

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;
}

執行結果為:

a-->c
a-->b
c-->b
a-->c
b-->a
b-->c
a-->c

2.4 全排列問題

全排列問題:

void permutation(char* s, char* e) // 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;
}

執行結果為:

abc
acb
bac
bca
cba
cab

但是如果存在相同的元素,則會有重複結果,例如:

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

    permutation(s, s);

    return 0;
}

執行結果為:

aac
aca
aac
aca
caa
caa

程式碼優化:

void permutation(char* s, char* e) // e始終指向字元陣列的首元素
{
    if( *s == '\0' )
    {
        cout << e << endl;
    }
    else
    {
        int len = strlen(s);
        char mark[256] = {0};

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

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

    permutation(s, s);

    return 0;
}

執行結果為:

aac
aca
caa

2.5 逆序列印單鏈表中的偶數結點

遞迴還能用於需要回溯窮舉的場合。。。

函式呼叫過程回顧:

  • 程式執行後有一個特殊的記憶體區供函式呼叫使用
    1. 用於儲存函式中的實參,區域性變數,臨時變數,等
    2. 從起始地址開始往一個方向增長 ( 如 : 高地址 → 低地址 )
    3. 有一個專用“指標”標識當前已使用記憶體的“頂部”

程式中的棧區一段特殊的專用記憶體區

例項分析:逆序列印單鏈表中的偶數結點

逆序列印單鏈表中的偶數結點:

void r_print_even(Node* list)
{
    if( list != NULL )
    {
        r_print_even(list->next);

        if( (list->value % 2) == 0 )
        {
            cout << list->value << endl;
        }
    }
}

int main()
{
    Node* list = create_list(2, 5);

    print_list(list);

    r_print_even(list);

    destroy_list(list);

    return 0;
}

執行結果為:

2->3->4->5->6->NULL
6
4
2

2.6 八皇后問題

八皇后問題:

  • 在一個8x8的國際象棋棋盤上,有8個皇后,每個皇后佔一格;要求皇后間不會出現相互“攻擊”的現象 ( 不能有兩個皇后處在同一行、同一列或同一對角線上 )。

關鍵資料結構定義:

  • 棋盤:二維陣列 ( 10 * 10 )
    1. 0 表示位置為空,1 表示皇后,2 表示邊界
  • 位置:struct Pos;
  • 方向:

演算法思路:

八皇后問題:

#include <iostream>
#include "LinkList.h"

using namespace std;
using namespace StLib;

template <int SIZE>
class QueueSolution : public Object
{
protected:
    enum { N = SIZE + 2 };

    struct Pos : public Object
    {
        Pos(int px = 0, int py = 0) : x(px), y(py) { }
        int x;
        int y;
    };

    int m_chessboard[N][N];
    Pos m_direction[3];
    LinkList<Pos> m_solution;
    int m_count;

    void init()
    {
        m_count = 0;

        for(int i=0; i<N; i+=(N-1))
        {
            for(int j=0; j<N; j++)
            {
                m_chessboard[i][j] = 2;
                m_chessboard[j][i] = 2;
            }
        }

        for(int i=1; i<=SIZE; i++)
        {
            for(int j=1; j<=SIZE; j++)
            {
                m_chessboard[i][j] = 0;
            }
        }

        m_direction[0].x = -1;
        m_direction[0].y = -1;
        m_direction[1].x = 0;
        m_direction[1].y = -1;
        m_direction[2].x = 1;
        m_direction[2].y = -1;
    }

    void print()
    {
        for(m_solution.move(0); !m_solution.end(); m_solution.next())
        {
            cout << "(" << m_solution.current().x << ", " << m_solution.current().y << ") ";
        }

        cout << endl;

        for(int i=0; i<N; i++)
        {
            for(int j=0; j<N; j++)
            {
                switch (m_chessboard[i][j])
                {
                    case 0: cout << " "; break;
                    case 1: cout << "#"; break;
                    case 2: cout << "*"; break;
                }
            }

            cout << endl;
        }

        cout << endl;
    }

    bool check(int x, int y, int d)
    {
        bool flag = true;

        do
        {
            x += m_direction[d].x;
            y += m_direction[d].y;
            flag = flag & (m_chessboard[x][y] == 0);
        }
        while( flag );

        return (m_chessboard[x][y] == 2);
    }

    void run(int j) // 檢查第j行有沒有可以放置皇后的位置
    {
        if( j <= SIZE )
        {
            for(int i=1; i<=SIZE; i++)
            {
                if( check(i ,j, 0) && check(i ,j, 1) && check(i ,j, 2) )
                {
                    m_chessboard[i][j] = 1;

                    m_solution.insert(Pos(i, j));

                    run(j + 1);

                    m_chessboard[i][j] = 0;

                    m_solution.remove(m_solution.length() - 1);
                }
            }
        }
        else
        {
            m_count++;

            print();
        }
    }
public:
    QueueSolution()
    {
        init();
    }

    void run()
    {
        run(1);

        cout << "Total: " << m_count << endl;
    }
};

int main()
{
    QueueSolution<4> qs;

    qs.run();

    return 0;
}

測試四皇后問題的執行結果為:

(2, 1) (4, 2) (1, 3) (3, 4) 
******
*  # *
*#   *
*   #*
* #  *
******

(3, 1) (1, 2) (4, 3) (2, 4) 
******
* #  *
*   #*
*#   *
*  # *
******

Total: 2

(八皇后問題一共有92個解。)

3.小結

  • 遞迴是一種將問題分而自治的思想
  • 用遞迴解決問題首先要建立遞迴的模型
  • 遞迴解法必須要有邊界條件否則無解
  • 不要陷入遞迴函式的執行細節,學會通過程式碼描述遞迴問題
  • 程式執行後的棧儲存區專供函式呼叫使用
  • 棧儲存區用於儲存實參,區域性變數,臨時變數,等
  • 利用棧儲存區能夠方便的實現回溯演算法
  • 八皇后問題是棧回溯的經典應用