递归与栈的转化 (迷宫 + 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) */
栈:
#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) */
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; }
栈:
#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; }
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 */
三,总结
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)
问题 ③:
为什么每次入栈时也只能入栈一个元素?
因为:
由于回溯时需要用到该路径上的所有元素,所以搜索时为了信息的完整,需要控制元素不能出栈,只有回溯时元素才能出栈。
又因为元素不能出栈,所以每次入栈时也只能入栈一个元素,不然就会错乱不同路径的元素。(就像递归函数一样,每次只调用一次自身)
问题 ④:
为什么八皇后中,需要取出栈顶下面的第二个元素?
因为:
在迷宫寻路中,我们需要要标记的是下一个要搜索的元素,所以可以在下一栈帧要搜索的元素,就是标记过的元素;
而在八皇后中,我们需要标记的是当前栈帧的元素,所以在下一栈帧中要搜索的元素再上一个元素,才是标记过的元素。
============================================
梦想是注定孤独的旅行,路上少不了质疑和嘲笑,但那又怎样,哪怕遍体鳞伤,也要活的漂亮
—— 魏晨