第45课 递归的思想与应用(下)

1. 函数调用栈的回顾

(1)用于保存函数中的实参、局部变量、临时变量等。

(2)从起始地址开始往一个方向增长(如:高地址→低地址)

(3)有一个专用“指针”标识当前已使用内存的“顶部”

(4)当函数调用结束时,栈会恢复到被调用前的状态可以利用这个时机进行一些的回溯算法的设计

【实例分析】函数调用栈分析:逆序打印单链表中的偶数结点(void r_print_even(Node* list)),见本课后面的源码

2. 回溯求解八皇后问题

(1)回溯算法:实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。八皇后问题就是回溯算法的典型,第一步按照顺序放一个皇后,然后第二步符合要求放第2个皇后,如果没有位置符合要求,那么就要改变第一个皇后的位置,再重新放第2个皇后的位置,直到找到符合条件的位置,以此方法放其他皇后…

(2)八皇后问题

  ①在一个8×8的国际象棋棋盘上,有8个皇后,每个皇后占一格;

  ②要求皇后间不会出现相互“攻击”的现象(不能有两个皇后处在同一行、 同一列或同一对角线上

(3)关键数据结构定义

  ①棋盘:二维数组(8 × 8):0表示位置为空,1表示皇后

  ②位置:struct Pos; 其中的x字段表示行,y表示列。

  ③方向:

    水平:向左(-1,0),向右(1,0)

    垂直:向上(0,1),向下(0,-1)

    对角线:左上(-1,1),左下(-1,-1),右上(1,1),右下(1,-1)

(3)算法思路:

  ①从第一行开始,为皇后找到安全位置,然后跳到下一行,通过递归进行深度搜索

  ②如果在第n行出现死胡同,如果该行为第一行,棋局失败,否则后退到上一行,进行回溯,并从该行的下一列位置继续查找安全位置。

  ③如果在第8行上找到了安全位置,则棋局成功。

【编程实验】八皇后问题的递归解法

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

using namespace std;
using namespace DTLib;

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

//创建无表头结点的单链表(己填充数据):v结点的值,len链表长度
Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

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

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

        //每创建好一个节点,slider指向这个节点
        if( slider == NULL ){
            slider = node;
            ret = node;
        }else{
            slider->next = node;
            slider = node;
        }
    }

    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 != NULL){

        //深度搜索,直到最后一个节点
        r_print_even(list->next);

        //回溯算法设计:上一行递归结束,开始回溯并判断节点的值是否为偶数
        if(list->value % 2 == 0){
            cout << list->value << " ";
        }
    }
}

template<int N>
class QueenSolution : public Object
{
protected:

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

    int m_chessboard[N][N]; //棋盘: 原点0,0在左下角,x轴正方向向右,y轴正方向向上
    Pos m_direction[3];     //三个方向向量
    LinkList<Pos> m_solution;
    int m_count;

    void init()
    {
        m_count = 0;

        //初始化棋盘
        for(int i=0; i<N; i++){
            for (int j=0; j<N; 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()
    {
        typename LinkList<Pos>::iterator iter = m_solution.begin();

        while(iter != m_solution.end()){
            cout << "(" << (*iter).x <<"," <<(*iter).y <<") ";
            iter++;
        }

        cout << endl;

        for(int y=0; y<N; y++){
            for (int x=0; x<N; x++){
                switch (m_chessboard[x][y]) {
                    case 0:cout << " ."; break;
                    case 1:cout << " #"; break;
                }
            }
            cout << endl;
        }

        cout << endl;
    }

    bool isInner(int x, int y)
    {
        return (0<=x) && (x<N) && (0<=y) && (y<N);
    }

    //检测在棋盘的(x,y)位置是否可以放置皇后
    bool check(int x, int y, int d)
    {
        bool ret = true;

        while(ret && isInner(x, y))
        {
            ret = ret && (m_chessboard[x][y] == 0);
            x += m_direction[d].x;
            y += m_direction[d].y;
        }

        return ret;
    }

    //在第row行尝试放置皇后
    void place(int row)
    {
        int y = row;
        if(row < N){
            for(int x=0; x<N; x++){
                if(check(x, y, 0) && check(x, y, 1) && check(x, y, 2)){
                    m_chessboard[x][y] = 1; //放置皇后
                    m_solution.insert(Pos(x,y)); //将该位置加入链表

                    place(row + 1); //在下一行放置皇后。

                    //当place(row+1)函数返回时,表示第row+1行无法找到放置皇后的地方(或找到解决方案后正常返回)
                    m_chessboard[x][y] = 0; //回溯,将当前位置的皇后移除。
                    m_solution.remove(m_solution.length() - 1);
                }
            }
        }else{
            //由于采用深度优先搜索,所以放置到第SIZE行时,表示己经找到解决方案
            m_count++;  //每找到一行方案,m_count加1。
            print();    //打印解决方案
        }
    }

public:
    QueenSolution(){init();}

    void solve()
    {
        place(0); //从第0行开始放置
        cout << "total : " << m_count << endl;
    }

};

int main()
{
    //测试:逆序打印单链表中的偶数结点
    Node* list = create_list(1, 10);
    print_list(list);

    r_print_even(list);

    destroy_list(list);

    cout << endl;

    //测试:八皇后解决方案
    QueenSolution<8> qs;
    qs.solve();

    return 0;
}
/*测试结果:
1->2->3->4->5->6->7->8->9->10->NULL
10 8 6 4 2
(0,0) (4,1) (7,2) (5,3) (2,4) (6,5) (1,6) (3,7) //显示时,使用的坐标系(原点在左上角,x向右为正,y向下为正)
 # . . . . . . .
 . . . . # . . .
 . . . . . . . #
 . . . . . # . .
 . . # . . . . .
 . . . . . . # .
 . # . . . . . .
 . . . # . . . .

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

 //省略其余解决方案...

total : 92
*/

3. 小结

(1)程序运行后的栈存储区专供函数调用使用

(2)栈存储区用于保存实参、局部变量和临时变量等。

(3)利用栈存储区能够方便的实现回溯算法

(4)八皇后问题是栈回溯的经典应用。

时间: 2024-10-27 04:43:26

第45课 递归的思想与应用(下)的相关文章

第四十五课 递归的思想与应用(下)

g函数返回后,f函数对应的栈中的数据没有任何变化,这就是回溯算法的核心. 可以这样思考,先逆序打印从第二个节点开始的子表,最后再将第一个节点打印出来. 1 #include <iostream> 2 #include <cstring> 3 #include "DTString.h" 4 5 using namespace std; 6 using namespace DTLib; 7 8 struct Node 9 { 10 int value; 11 Nod

第44课 递归的思想与应用(中)

1. 单向链表的转置 [编程实验]单向链表的转置(Node* reverse(Node* list)) 2. 单向排序链表的合并 [编程实验]单向排序链表的合并(Node* merge(Node* list1, Node* list2)) 3. 汉诺塔问题 (1)游戏规则 ①将木块借助B柱由A柱移动C柱 ②每次只能移动一个土块 ③只能出现小土块在大木块之上 (2)递归求解的问题分解 ①将n-1个木块借助C柱由A移动到B柱. ②将最底层的唯一木块直接移动到C柱 ③将n-1个木块借助A柱由B柱移动到

第四十四课 递归的思想与应用(中)

将大问题分解,先将第一个节点拿出来,将其它的节点看成一个整体. 1 #include <iostream> 2 #include <cstring> 3 #include "DTString.h" 4 5 using namespace std; 6 using namespace DTLib; 7 8 struct Node 9 { 10 int value; 11 Node* next; 12 }; 13 14 Node* create_list(int v

第45课 函数参数的秘密(下)

参数入栈顺序: 调用约定: gcc语言默认使用__cdecl调用约定.调用约定不是语言的一部分,是编译器的一部分. 调用约定使用的最典型的地方就是库函数的调用.因为库的编译方式我们的自己的程序的编译方式可能不一样. 小问题: 如何编写一个计算n个数平均值的函数? 示例程序: 1 #include <stdio.h> 2 3 float average(int array[], int size) 4 { 5 int i = 0; 6 float avr = 0; 7 8 for(i=0; i&

用分治和递归的思想——寻找最大子数组

寻找最大子数组的问题可描述为 输入: 一个数组,一个低位,一个高位 输出: 数组中从低位到高位中,连续和最大是多少 首先能想到的最直接的办法是暴力解决,遍历所有可能的序列组合,如果有n个元素,则需遍历的子序列有,复杂度为n2,稍有些经验的就能马上意识到,有很多重复计算在里面,比如最长的子序列计算,包含了之前所有子序列的计算.接下来我们使用分治的思想求解这个最大子序列,前一篇博文提过,分治的思想是将大问题分解为同等类型的子问题,再将子问题求解,然后合并子问题得出原问题的解,其中用到了递归的思想,因

数据-第19课-递归的应用实战一

第19课-递归的应用实战一 1. 递归的数学思想 (1)      递归是一种数学上分而自治的思想. (2)      递归将大型复杂问题转化为与原问题相同但规模较小的问题进行处理. (3)      递归需要有边界条件. l  当边界条件不满足时,递归继续进行. l  当边界条件满足时,递归停止. 2 . 递归的数学表示 n > 1 n==1 (1)斐波拉契数列递归解法 #include <stdio.h> int fibonacci(int n) { if( n > 1 ) {

数据--第20课-递归的应用实战二

第20课-递归的应用实战二 1. 递归与回溯 (1)递归在程序设计中也常用于需要回溯算法的场合. (2)回溯算法的基本思想. ① 从问题的某一种状态出发,搜索可以到达的所有状态. ② 当某个状态到达后,可向前回退,并继续搜索其它可达状态 ,并继续搜索其它可达状态. ③ 当所有状态都到达后,回溯算法结束. (3)程序设计中可利用函数的活动对象保存回溯算法的状态数据,因此可以利用递归完成回溯算法 2. 八皇后问题 在一个8×8国际象棋盘上,有8个皇后,每个皇后占一格:要求皇后间不会出现相互“攻击”的

漫谈递归:递归的思想 --转载

为什么要用递归 编程里面估计最让人摸不着头脑的基本算法就是递归了.很多时候我们看明白一个复杂的递归都有点费时间,尤其对模型所描述的问题概念不清的时候,想要自己设计一个递归那么就更是有难度了. 很多不理解递归的人(今天在csdn里面看到一个初学者的留言),总认为递归完全没必要,用循环就可以实现,其实这是一种很肤浅的理解.因为递归之所以在程序中能风靡并不是因为他的循环,大家都知道递归分两步,递和归,那么可以知道递归对于空间性能来说,简直就是造孽,这对于追求时空完美的人来说,简直无法接接受,如果递归仅

数据结构开发(15):递归的思想与应用

0.目录 1.递归的思想 2.递归的应用 2.1 单向链表的转置 2.2 单向排序链表的合并 2.3 汉诺塔问题 2.4 全排列问题 2.5 逆序打印单链表中的偶数结点 2.6 八皇后问题 3.小结 1.递归的思想 递归是一种数学上分而自治的思想: 将原问题分解为规模较小的问题进行处理 分解后的问题与原问题的类型完全相同,但规模较小 通过小规模问题的解,能够轻易求得原问题的解 问题的分解是有限的 ( 递归不能无限进行 ) 当边界条件不满足时,分解问题 ( 递归继续进行 ) 当边界条件满足时,直接