【數據結構】45_遞歸的思想與應用 (下)

函數的調用

  • 程序運行後有一個特殊的內存區供函數調用使用ios

    • 用於保存函數中的形參,局部變量,臨時變量,等
    • 從起始地址開始往一個方向增加(如:高地址->低地址)
    • 有一個專用 "指針" 標識當前已使用內存的 "頂部"

程序中的棧區

  • 一段特殊的專用內存區

image.png

實例分析:逆序打印單鏈表中的偶數節點

image.png

編程實驗:函數調用棧分析

#include <iostream>

using namespace std;

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

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

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

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

        if (slider == nullptr)
        {
            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;
}

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

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

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

    print_list(list);

    r_print_even(list);

    destroy_list(list);
    
    cout << endl;

    return 0;
}

輸出:算法

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

函數調用棧.png
退棧打印的過程就是回溯的過程。
遞歸調用時,將數據保存在棧空間,棧返回時再使用。編程

八皇后問題

在一個 8x8 的國際象棋盤上,有 8 個皇后,每一個皇后佔一格;要求皇后間不會出現相互 "攻擊" 的現象(不能有兩個皇后處在同一行)。數組

image.png

關鍵數據結構定義

  • 棋盤:二維數組 (10 * 10)數據結構

    • 0 表示位置爲空,1表示皇后,2表示邊界
  • 方向ide

    • 水平: (-1, 0),(1, 0)
    • 垂直: (0, -1),(0, 1)
    • 對角線: (-1, 1),(-1,-1),(1,-1),(1,1)
  • 位置: Struct Pos;
struct Pos
{
    int x;
    int y;
};

算法思路

  1. 初始化: i = 1
  2. 初始化: j = 1
  3. 從第 j 行開始,恢復 i 的有效值(經過函數調用棧進行回溯), 判斷 i 的位置
    a. 位置 i 可放入皇后:標記位置(i, j), j++, 轉步驟 2
    b. 位置 i 不可放入皇后:i++, 轉步驟 a
    c. 當 i > 8 時, j--, 轉步驟 3
  4. 結束: 第 8 行有位置可放入皇后

編程實驗:八皇后問題的遞歸解法

#include <iostream>
#include <list>

using namespace std;

template <int SIZE>
class QueueSolution
{
protected:
    enum { N = SIZE + 2}; // 包含邊界元素

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

    int m_chessboard[N][N];  // 棋盤
    Pos m_direction[3];      // 3方向數組 (左下、正下、右下)
    list<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 (auto iter = m_solution.cbegin(); iter != m_solution.cend(); ++iter)
        {
            cout << "(" << iter->x << "," << iter->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 << hex << m_chessboard[i][j];
            }
            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)
    {
        if (j <= SIZE)  // 1. 位置查找
        {
            for (int i=1; i<=SIZE; ++i)               // 1.1 嘗試 j 行的每一列
            {
                if (check(i, j, 0) && check(i, j, 1) && check(i, j, 2))  // 1.1。1 檢查棋位是否可用
                {
                    m_chessboard[i][j] = 1;           // 1.1.2 棋位標記
                    m_solution.push_back(Pos(i, j));
                    run(j + 1);                       // 1.1.3 查找 j +1 行
                    
                    // 1.1.4 擦除棋位標記
                    // 當查找成功時,run 會不斷遞歸查找 j + 1 行,直到 j == SIZE 時,在出口退出;
                    // 當運行到這裏,說明 j + 1 行位置查找失敗,即 j 行的位置是錯誤的,擦除標記,繼續嘗試 j 行的下一列元素(for(...))
                    m_chessboard[i][j] = 0;           
       
                    m_solution.pop_back();
                }
            }
        }
        else  // 2. 遞歸出口,[1-SIZE] 找到解決方案
        {
            m_count ++;
            print();
        }
    }

public:
    QueueSolution()
    {
        init();
    }

    void run()
    {
        run(1);

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

int main()
{
    QueueSolution<8> qs;

    qs.run();

    return 0;
}

輸出:函數

...
...
...

(8,1) (2,2) (5,3) (3,4) (1,5) (7,6) (4,7) (6,8)
**********
*....#...*
*.#......*
*...#....*
*......#.*
*..#.....*
*.......#*
*.....#..*
*#.......*
**********

(8,1) (3,2) (1,3) (6,4) (2,5) (5,6) (7,7) (4,8)
**********
*..#.....*
*....#...*
*.#......*
*.......#*
*.....#..*
*...#....*
*......#.*
*#.......*
**********

(8,1) (4,2) (1,3) (3,4) (6,5) (2,6) (7,7) (5,8)
**********
*..#.....*
*.....#..*
*...#....*
*.#......*
*.......#*
*....#...*
*......#.*
*#.......*
**********

Total : 92
說明:棋位檢查

bool check(int x, int y, int d)spa

871853-20180919235552231-654775738.png
只檢查 3 個方向(左下,正下,右下),由於棋盤被初始化爲空,且起始爲有規律放置。指針

說明:回溯嘗試

void run(int j)
回溯.png
當發現 j 行的任意位置都不能放置,則說明 j -1 行的的位置是錯誤的,依次不斷進行回溯嘗試。code

參考鏈接

小結

  • 程序運行後的棧存儲區專供函數調用使用
  • 棧存儲區用於保存形參,局部變量,臨時變量,等
  • 利用棧存儲區可以方便的實現回溯算法
  • 八皇后問題是棧回溯的經典應用

以上內容整理於狄泰軟件學院系列課程,請你們保護原創!

相關文章
相關標籤/搜索