递归与栈的转化 (迷宫 + n 皇后 + 汉诺塔)

参考:

https://www.bilibili.com/video/av21776496?from=search&seid=14795429927506117804

《数据结构教程》第五版 李春葆

 

 

一,递归到非递归的转换

1,递归的分类

   尾递归 

     ① 如果一个递归过程会 递归函数中的递归调用语句是最后一条语句,则称这种递归调用为尾递归

     ② 一般情况下,尾递归可以通过循环或者迭代方式转化为等价的非递归算法

   非尾递归

     ① 对于非尾递归算法 ,在理解递归调用实现过程的基础上可以用栈来模拟递归执行过程

2,递归的功能

   递归调用函数前面的语句的功能:搜索

   递归调用函数后面的语句的功能:回溯

3,递归与栈 (BFS) 的比较

  栈帧:单个函数调用操作所使用的函数调用栈

  一个栈帧 相当于 BFS中的一层循环

    包含对出栈元素处理的操作 和 其它元素的进栈(因为每个栈帧都会保存当前的数据,所以严格来讲递归没有进栈操作,它只需要通过回溯控制出栈顺序即可)

  调用函数 相当于 出栈操作

4,综合

  尾递归需要只用到递归函数的搜索功能,所以可以用循环替换;非尾递归除了循环部分,还用到了递归函数的回溯功能,所以只能用栈模拟

 

 

二,举例

1,迷宫寻路

递归:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 110
int n, m, cnt;
int map[N][N];
int dx[] = { -1,0,1,0 }, dy[] = { 0,1,0,-1 };
struct Node {
    int x, y;
}way[N];
void show()
{
    printf("(0,0)");
    for (int i = 0; i < cnt; i++)
        printf("->(%d,%d)", way[i].x, way[i].y);
    puts("");
}
void DFS(int x, int y)
{
    if (x == n - 1 && y == m - 1)
    {
        show();
        return;
    }
    for (int i = 0; i < 4; i++)
    {
        Node next = { x + dx[i], y + dy[i] };
        if (next.x < 0 || next.y < 0 || next.x >= n || next.y >= m)
            continue;
        if (map[next.x][next.y] == 0)
        {
            map[x][y] = 1;
            way[cnt].x = next.x, way[cnt].y = next.y;
            cnt++;
            DFS(next.x, next.y);
            cnt--;
            map[x][y] = 0;
        }
    }
}
int main(void)
{
    // 入口是 map[0][0], 出口是 map[m-1][n-1]
    while (scanf("%d%d", &n, &m) != EOF)
    {
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                scanf("%d", &map[i][j]);
        cnt = 0;
        DFS(0, 0);
    }
    system("pause");
    return 0;
}
/*
测试数据
第一组
6 5
0 0 1 1 1
0 0 0 0 1
1 0 1 0 1
1 0 1 0 1
1 1 1 0 1
1 0 1 0 0
结果
(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)

第二组
6 5
0 0 1 1 1
0 0 0 0 1
1 0 1 0 1
1 0 1 0 1
1 0 1 0 1
1 0 0 0 0
结果
(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(0,1)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
*/
View Code

栈:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 101
#define MaxSize 10086
int dx[] = { -1, 0, 1, 0 }, dy[] = { 0, 1, 0, -1 };
int map[N][N];
typedef struct Box
{
    int x, y;
    int di;    // 点的编号
}bx;
typedef Box any;       // 可修改数据类型
typedef struct SqStack // 顺序栈
{
#define MaxSize 666
    any a[MaxSize];
    int pt;          // 栈顶指针
    SqStack() {
        pt = -1;
    }

    void push(any e) {  // 入栈
        a[++pt] = e;
    }
    void pop() {        // 出栈
        pt--;
    }
    any top() {         // 取栈顶元素
        return a[pt];
    }
    bool empty() {       // 判断栈是否为空
        return pt == -1;
    }
    int size() {        // 返回栈的大小
        return pt + 1;
    }
}st;
void show(st s)
{
    st t;
    while (!s.empty())
    {
        bx v = s.top();
        s.pop();
        t.push(v);
    }
    int cnt = 0;
    while (!t.empty())
    {
        bx v = t.top();
        t.pop();
        if (cnt++ == 0)
            printf("(%d,%d)", v.x, v.y);
        else
            printf("->(%d,%d)", v.x, v.y);
    }puts("");
}
int n, m;
void dfs(int xi, int yi)
{
    st s;    // 定义栈
    bx start = { xi,yi,0 };
    map[xi][yi] = -1;
    s.push(start);     // 起点进栈

    int cnt = 1;   // 记录路径数
    while (!s.empty())
    {
        // 1,取栈顶元素,相当于函数形参
        bx vertex = s.top(); 

        // 2,找到终点,回溯(关键点:消除搜索的痕迹)
        if (vertex.x == n - 1 && vertex.y == m - 1)
        {
            show(s);
            s.pop();
            map[vertex.x][vertex.y] = 0;
            continue;
        }

        // 3,如果有路可走,就继续搜索
        int find = 0; // find = 1 表示下一步可走,0 表示下一步不可走
        for (int i = vertex.di; i < 4; i++)
        {
            bx next{ vertex.x + dx[i], next.y = vertex.y + dy[i] ,0 };
            if (next.x < 0 || next.y < 0 || next.x >= n || next.y >= m)
                continue;
            
            if (map[next.x][next.y] == 0)
            {
                find = 1;
                vertex.di = i + 1;   // 标记这个方块 已经走过的方向,手动确定回溯时不会重复回溯。
                s.pop();
                s.push(vertex);

                // 继续搜索
                s.push(next);
                map[next.x][next.y] = 1;
                break;   // 一次只入栈一个元素,同一层的其他元素在回溯时再入栈
            }
        }
        // 4,走到死胡同,回溯(关键点:消除搜索的痕迹)
        if (!find)
        {
            s.pop();
            map[vertex.x][vertex.y] = 0;
        }
    }
}
int main(void)
{
    while (scanf("%d%d", &n, &m) != EOF)
    {
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                scanf("%d", &map[i][j]);
        dfs(0, 0);
    }
    system("pause");
    return 0;
}
/*
测试数据
第一组
6 5
0 0 1 1 1
0 0 0 0 1
1 0 1 0 1
1 0 1 0 1
1 1 1 0 1
1 0 1 0 0
结果
(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)

第二组
6 5
0 0 1 1 1
0 0 0 0 1
1 0 1 0 1
1 0 1 0 1
1 0 1 0 1
1 0 0 0 0
结果
(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(0,1)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)
(0,0)->(1,0)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
*/
View Code

 

2,八皇后

八皇后的实质:求特定排列顺序的排列序列问题

递归:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int queen[10]; // queen[i] 代表第 i 行的皇后的位置
int vis[10];   // vis[i]   代表第 i 列的皇后位置是否确认
int n, cnt;
bool judge(int n)
{
    for (int i = 1; i < n; i++)  // 比较 任意两个皇后的位置
    {
        for (int j = i + 1; j <= n; j++)
            if (i - j == queen[i] - queen[j] || j - i == queen[i] - queen[j])
                return false;
    }
    return true;
}
void show()
{
    printf("第%d种情况:\n", ++cnt);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
            if (j == queen[i])
                printf("Q ");
            else
                printf(". ");
        puts("");
    }puts("");
}
void DFS(int k) // 循环行
{
    if (!judge(k-1)) { // 剪枝
        return;
    }
    if (k > n) {
        show();
        return;
    }
    for (int i = 1; i <= n; i++) // 循环列
    {
        if (vis[i] > 0)
            continue;

        vis[i] = 1; queen[k] = i;
        DFS(k + 1);
        vis[i] = queen[k] = 0;
    }
}
int main(void)
{
    while (scanf("%d", &n) != EOF)
    {
        cnt = 0;
        DFS(1);
    }
    return 0;
}
View Code

栈:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 66
int n, cnt;
int queen[N], vis[N];
typedef struct Position
{
    int row; // 标记循环到第几行
    int col; // 标记循环到第几列
}pi;
typedef Position any;       // 可修改数据类型
typedef struct SqStack // 顺序栈
{
#define MaxSize 666
    any a[MaxSize];
    int pt;          // 栈顶指针
    SqStack() {
        pt = -1;
    }

    void push(any e) {  // 入栈
        a[++pt] = e;
    }
    void pop() {        // 出栈
        pt--;
    }
    any top() {         // 取栈顶元素
        return a[pt];
    }
    bool empty() {       // 判断栈是否为空
        return pt == -1;
    }
    int size() {        // 返回栈的大小
        return pt + 1;
    }
}st;
bool judge(int n)
{
    for (int i = 1; i < n; i++)  // 比较 任意两个皇后的位置
    {
        for (int j = i + 1; j <= n; j++)
            if (i - j == queen[i] - queen[j] || j - i == queen[i] - queen[j])
                return false;
    }
    return true;
}
void show()
{
    printf("第%d种情况:\n", ++cnt);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
            if (j == queen[i])
                printf("Q ");
            else
                printf(". ");
        puts("");
    }puts("");
}
void DFS()
{
    st s;
    s.push({1, 1}); // 起始于第一行第一列
    while (!s.empty())
    {
        // 1,取栈顶元素,相当于函数形参
        pi vertex = s.top();

        // 取上一次搜索的元素
        s.pop();
        pi last = {};
        if (!s.empty())
            last = s.top();
        s.push(vertex);

        // 2,剪枝,,回溯(关键点:消除搜索的痕迹)
        if (s.size() > 1 && !judge(last.row)) // 已经确定了大于一个皇后的位置再检查
        {
            s.pop();
            vis[last.col - 1] = 0;
            queen[last.row] = 0;
            continue;
        }

        // 3,搜索完毕,回溯(关键点:消除搜索的痕迹)
        if (vertex.row > n) // 判断是否符合八皇后的条件
        {
            show();

            s.pop();
            vis[last.col - 1] = 0;
            queen[last.row] = 0;
            continue;
        }

        // 4,如果有路可走,就继续搜索
        int find = 0;
        for (int i = vertex.col; i <= n; i++) // col 初始化为 1
        {
            pi next = { vertex.row + 1, 1 };
            if (vis[i] == 1)
                continue;

            find = 1;
            s.pop();
            s.push({ vertex.row, i + 1 });  // 标记这个行 已经走过的列数,手动确定回溯时不会重复回溯。
            
            vis[i] = 1;
            queen[vertex.row] = i;
            s.push(next);  // 继续搜索
            break; 
        }
        // 5,走到死胡同,回溯(关键点:消除搜索的痕迹)
        if (s.size() > 1&&!find)
        {
            s.pop();
            vis[last.col - 1] = 0;
            queen[last.row] = 0;
            continue;
        }
    }
 }
int main(void)
{
    while (scanf("%d", &n) != EOF)
    {
        cnt = 0;
        DFS();
    }

    system("pause");
    return 0;
}
View Code

 

3,汉诺塔

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 666
int cnt;
void move(int n, char a, char b)
{
    printf("第 %2d 步:将第 %d 个盘子从 %c 移动到 %c\n", ++cnt, n, a, b);
}
void Hanoi1(int n, char a, char b, char c)
{
    if (n == 1)
    {
        move(n, a, c);
        return;
    }
    Hanoi1(n - 1, a, c, b);
    move(n, a, c);
    Hanoi1(n - 1, b, a, c);
}
typedef struct ElemType
{
    // 入栈的信息:a 借助 b 移动 n 个盘子到 c
    int n;
    char a, b, c;
    bool flag;
}et;
typedef struct SqStack
{
    et data[N];
    int pt;
    SqStack() {
        pt = -1;
    }
    void push(et e) {
        data[++pt] = e;
    }
    void pop() {
        pt--;
    }
    et top() {
        return data[pt];
    }
    bool empty() {
        return pt == -1;
    }
}st;
void Hanoi2(int n, char a, char b, char c)
{
    if (n <= 0)
        return;
    st s; //
    et e = { n, a, b, c, false };
    s.push(e);
    while (!s.empty())
    {
        et vertex = s.top();
        s.pop();

        if (vertex.n == 1)
        {
            move(vertex.n, vertex.a, vertex.c);
            continue;
        }
        if (vertex.flag == false) // 注意栈是先进后出的,所以顺序要反过来
        {
            // 相当于 Hanoi1(n - 1, b, a, c);
            et next3 = { vertex.n - 1, vertex.b, vertex.a, vertex.c, false };
            s.push(next3);

            // 相当于 move(n, a, c);
            et next2 = { vertex.n, vertex.a, vertex.b, vertex.c, true };
            s.push(next2);

            // 相当于 Hanoi1(n - 1, a, c, b); 
            et next1 = { vertex.n - 1, vertex.a, vertex.c, vertex.b, false };
            s.push(next1);
        }
        else
            move(vertex.n, vertex.a, vertex.c);
    }
}
int main(void)
{
    int n;
    while (scanf("%d", &n) != EOF)
    {
        cnt = 0;
        printf("递归算法:\n");
        Hanoi1(n, 'a', 'b', 'c');

        cnt = 0;
        printf("非递归算法:\n");
        Hanoi2(n, 'a', 'b', 'c');
        puts("");
    }
    return 0;
}
/*
3 7
4 15
5 31
6 63
*/
View Code

 

三,总结

  1,栈里要存放的元素如何确定?

    答:栈里存放的元素应要对应递归函数的形参

 

  2,递归可以回溯到之前的栈帧,从而利用当前栈帧中调用函数没有放入形参的信息,但栈在使用出入栈模拟回溯时,却无法利用上一栈帧独有的信息,如何解决?

    答:将需要利用的之前栈帧中的数据保存在栈的元素里面。

    如:

    ① 对于迷宫寻路

      在递归的回溯时,需要利用变量 i 从 0 到 4 循环控制搜索的方向,从而保证不会重复搜索

      所以,可以在栈中的元素里增加一个变量 di 等效于递归里的 i 

    ② 对于八皇后

      在递归的回溯时,需要利用变量 i 从 1 到 n 循环记录当前遍历到第 k 行的第几列,从而保证不会重复搜索

      所以,可以在栈中的元素里增加一个变量 col 等效于递归里的 i 

    ③ 对于汉诺塔

      move 在递归中,因为有栈帧保存数据,所以就可以在回溯中保证调用的顺序;而在栈中,当元素出栈时,是无法根据出栈元素的数据判断出是否要执行 move 操作

      所以可以在栈中的元素里增加一个变量 flag 判断是否需要执行 move 操作

 

  3,一个栈帧有多个递归函数调用时,在转换为栈的过程中需要注意出入栈的顺序是反着来的

    在递归函数中,在一个栈帧中出栈的顺序为 Hanoi(n-1, a, c, b),move(n, a, c),Hanoi(n-1, b, a, c)

    所以在非递归的栈中,入栈顺序就需要与出栈顺序相反,即 Hanoi(n-1, b, a, c),move(n, a, c),Hanoi(n-1, a, c, b)

 

  问题 ③:

    为什么每次入栈时也只能入栈一个元素?

  因为:

    由于回溯时需要用到该路径上的所有元素,所以搜索时为了信息的完整,需要控制元素不能出栈,只有回溯时元素才能出栈。

    又因为元素不能出栈,所以每次入栈时也只能入栈一个元素,不然就会错乱不同路径的元素。(就像递归函数一样,每次只调用一次自身)

 

  问题 ④:

    为什么八皇后中,需要取出栈顶下面的第二个元素?

  因为:

    在迷宫寻路中,我们需要要标记的是下一个要搜索的元素,所以可以在下一栈帧要搜索的元素,就是标记过的元素;

    而在八皇后中,我们需要标记的是当前栈帧的元素,所以在下一栈帧中要搜索的元素再上一个元素,才是标记过的元素。

 

 

 

============================================

梦想是注定孤独的旅行,路上少不了质疑和嘲笑,但那又怎样,哪怕遍体鳞伤,也要活的漂亮

                              —— 魏晨

 

posted @ 2020-03-14 11:35  叫我妖道  阅读(628)  评论(0编辑  收藏  举报
~~加载中~~