本文将讲解把递归通过栈转化为非递归形式的方法,初步设计为无返回值的递归,具体是否能携带返回值我们进一步考证。不过理论上多加一个参数就可以了。再次我们拿经典的汉诺塔问题进行讲解。下面是两种不同形式的代码样例:
#include <iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<functional>
using namespace std;
static int index=0;
void HanNuo(int n,string a,string b,string c)
{
if (n == 1)
{
cout << "把" << a << "移动到" << b << "上" << endl;
index++;
}
else
{
HanNuo(n - 1, a, c, b);
cout << "把"<<a << "移动到"<< b<< "上" << endl;
index++;
HanNuo(n - 1, c, b, a);
}
}
int main()
{
HanNuo(3,"a","b","c");
cout << endl;
cout << "移动次数" << index;
}
//递归实现形式
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <stack>
using namespace std;
typedef struct
{
int n;
char x, y, z;
int tag;
}NodeType;
void CommonFunction(NodeType& n)
{
cout << "把第" << n.n << "个盘片,从" << n.x << "移动到" << n.y << endl;
}//仅仅就是个公共方法而已。
void TaskOneForStack(stack<NodeType>& s, NodeType n)//这种方法是专门用来模拟递归的函数申请的。
{
n.n--;
if (n.n == 1)
{
n.tag = 0;
}
else
{
n.tag = 1;
}
char tem = n.z;
n.z = n.y;
n.y = tem;
s.push(n);
}//替代递归周围的操作
void TaskTwoForSentence(stack<NodeType>& s, NodeType n)//前面入站的还为执行,为了保证顺序正确,需要先入栈。
{//普通语句作为叶节点处理,即表明是不会继续展开的节点
n.tag = 0;
s.push(n);
}//替代递归之间被执行的操作
void TaskThreeForStack(stack<NodeType>& s, NodeType n)
{
n.n--;
if (n.n == 1)//if else 二选一,单独的if语句称之为“可选”
{
n.tag = 0;
}//碰壁之后还要携带一个信息,即需要进入终结阶段而不是继续走递归流程。
else
{
n.tag = 1;
}
char tem = n.z;
n.z = n.x;
n.x = tem;
s.push(n);
}
void Hannoil(int n, char x, char y, char z)//算法题目的研究不是做完就行的,问题是你学到了什么思想。
{
NodeType e;
stack<NodeType> st;
e.n = n;
if (n == 1) e.tag = 0;
else e.tag = 1;
e.x = x;
e.y = y;
e.z = z;
st.push(e);
while (!st.empty())
{
if (st.top().tag == 1)//确定到底是回归状态还是向下递的状态
{
NodeType node = st.top();
st.pop();
TaskThreeForStack(st, node);
TaskTwoForSentence(st, node);
TaskOneForStack(st, node);
}
else
{
CommonFunction(st.top());
st.pop();
}
}
}
int main()
{
Hannoil(3,'a','b','c');
}
//非递归版本
本次转换方法1.0(形式上还可以优化,更加优美的形式请见下篇)需要解决三个问题,首先如何控制程序何时回归何时递推,其次就是如何处理夹在两个递归语句中间的常规语句(夹在后面和在最前面的同理可以解决,但是非递归语句在最前面的构成尾递归,处理起来有更加简单的方法。),最后如何处理递归时的if else逻辑。
前排提示,在非递归写法中的函数都不存在递归现象,如果你想的话可以写成一个函数,分开只是为了模块分工明确。其次我们需要在前方简单的叙述一下预判式的递归和非预判的递归的区别。主要体现在递归树的形状和递归的写法上。其次这种预判式的递归如果有返回值得话写起来容易出错,具体请见未来的递归终止条件的写法的专题分析。下面用伪代码演示这两种递归形态上的区别:
普通递归:
if(递归终止条件)终止递归;
继续递归
继续递归
可能的语句
继续递归
可能的语句
递归函数结束
这种递归递归树一定不是链状的,必定有分叉。即递归树是完全展开然后再回归。 这种递归写法如果想要剪枝也必定会浪费至少一个节点,即先展开不符合条件再回退。
预判的递归(即提前向下看一步)
if(未到终止条件)继续递归
if(未到终止条件)继续递归
可能的语句
if(未到终止条件)继续递归
if(未到终止条件)继续递归
可能的语句
if(未到终止条件)继续递归
可能的语句
递归函数结束
这种形式的递归的递归树很有可能是链式的,即永远只进入一个递归分支,即展开之前提前剪去无用的分支。
但是如果是以下形式则必定是链式的递归树:
预判的递归(即提前向下看一步)
if(未到终止条件)继续递归 return;
if(未到终止条件)继续递归 return;
可能的语句
if(未到终止条件)继续递归 return;
if(未到终止条件)继续递归 return;
可能的语句
if(未到终止条件)继续递归 return;
可能的语句
递归函数结束
首先我们主要思想就是通过tag标志来表明当前流程是继续“递”还是进入了“归”的状态。如果tag==1则栈顶的元素仍然需要向下递推,未碰壁,如果tag==0则表明元素碰壁,需要回归。为什么要设置tag标签呢?其实这个问题很难回答,如果非要回答,我会告诉你如果不添加tag标签会产生什么问题。如果没有tag标签我们有两种解决方案,第一手动完成递推过程并入栈,然后通过while循环出栈;第二我们在栈处理方法中判断是否碰壁,如果碰壁返回false并出栈,此后的全部循环均出栈。
然而经过研究以上两种解决方案不过是tag法的特殊形式而已,第一种方法需要满足尾递归性质,第二种需要满足单递同时满足尾递归,或者下面这样的形式:
。。。。递归边界处理语句
if(分支控制) return fun();
if(分支控制) return fun();
结束递归函数体
即每次进入递归函数的时候仅仅进入了一个递归函数,而且还是在函数的末尾(即调用递归函数后没有其他代码了)。这种语句其实递归树是一个绝对的偏树,就是链状的一棵树,所以碰壁即代表进入回归模式。即当碰壁的时候返回的false被外围while(控制递归栈的大循环)中的逻辑判断语句捕获,跳出递推压栈操作,进入弹栈回归的那一条分支语句继续执行直到栈空。
简单的说就是上面那种递归生成的递归树是链状的所以我们只需要用一个变量(即用一个tag)记录是否触壁,然后其他的节点不需要判断,一旦链式的最低端触壁,剩下的一定没有其他分支可走(链式递归树,一旦开始回归,便不会走其他分支,因为没有其他分支),即进入回归模式。
而对于第一种方式,本质上其实就是将递归的递推和回归给分开写了,这样就要求递归必须具有尾递归的性质。因为如果是尾递归,递推的部分全部在递归语句之前,完全没有递归特性可以用循环来实现。而实现的效果就是将数据压入栈中,然后开始不断的弹栈处理,即回归。尾递归形式如下:
结束条件(可能的位置)
语句
语句
语句
语句
语句
结束条件(可能的位置)
fun()即递归调用自己
fun()即递归调用自己
fun()即递归调用自己
fun()即递归调用自己
结束递归函数函数体
如上,我们只需要将结束条件稍加处理作为循环的结束条件,然后把语句部分全部放入循环体中即可,并在循环的最后进行数据递推并做压栈处理。
既然前两种方法限制太大,所以我们只能用第三个方法,设置一个tag用来判断到底当前该继续递推还是进入回归分支。我们需要在每个分方法中加入碰壁检测,示例代码中就是n==1,并入栈一个tag=0的结构体。等返回大循环的时候就能通过这个tag决定是到达了叶节点,并开始回归。-------2021/4/12,有点累,剩下的明天写,保证质量。
2021/4/12开始写tag 的具体作用
如果我们把加载递归代码中间的普通代码作为递归树的叶节点的话,那么我们的递归树只在叶节点执行任务,即递归树执行的结果仅跟叶节点的线性排列顺序有关。因此我们的目标就是将tag=1的节点继续展开到tag=0的叶节点为止。这种递归的特点和普通的递归有所不同的是,我们是先压栈再弹栈,逻辑上我们是将某个非叶子节点上所有的要展开的分支一次性展开,连续入栈,然后再弹出并判断是否继续展开。逻辑上就是对于某一个节点是全部递推完成再开始尝试回归。(后面我有机会的话会配置动图)。
简单的说,就是我们直接递归操作的时候(不考虑底层实现)是顺着某一个分支到头然后返回到分支节点,再走另一条分支到头。而且分支扩展语句(就是自己调自己的递归语句)之间的普通语句将会在到达这个节点(可能处于递推状态,也可能处于回归状态,反正就是到了这个节点,具体跟语句的位置有关)的时候被执行。而我们用栈模拟递归的时候是类似层次搜索,当到达某个产生分支的节点的时候,会将全部的分支可能性全部做入栈处理,然后弹出一个分支继续搜索。即我们是展开整整一层然后挑一个进行继续处理,这样我们栈里面其实存了一整层的数据。而普通的递归逻辑上每层函数栈里面仅仅存有一个分支的数据信息。当然这是逻辑上的区别,具体编译器底层怎样处理递归这是另一个研究方向了。但是真正扩展起来都是一条分支扩展到头,再返回。当然用模拟的方法叶节点会多出好多,因为递归语句之间的普通语句都作为叶节点出现了。这是为了保证这些语句的执行顺序的正确。例如下面的代码结构:
fun();
普通代码
fun();
中间的普通代码需要等到上面的递归语句全部执行完毕才会执行。而我们模拟的时候上面的语句并不会执行而是入栈处理(这种方法是如此,后面将会研究直接调用的设计方法),如果普通语句在入栈阶段直接执行的话,就相当于先执行了普通语句再执行的fun递归函数。实际执行结构等效如下:
普通代码
fun()
fun()
从而出现逻辑错误,执行流程的错误。
通过上面的描述我们可以发现我们的方法将递推和递归拆开处理,而用tag表明push进去的节点能否继续扩展,并在下一次循环中进行具体处理。2021/4/13 8:29上课了剩下的问题,以后再写。