先序递归与后序递归
递归整理及几个经典题目
一文读懂递归算法
怎样更好地终极理解递归算法
递归算法总结
int factorial(int n)
{
if(n == 1)
return 1;
else
return n * factorial(n-1);
}
int factorial(int n)
{
int res = 1;
for(int i=1; i<=n; i++)
{
res *= i
}
return res;
}
递归问题都是循环问题,递归本身自带循环属性。该递归需要子模块的返回值来进行计算,有递有归。递的过程是对模块之间的关系进行描述-“n的阶乘值等于n-1的阶乘值乘以n",利用模块间的关系进行逐步调用和循环,本质应该是利用压栈的特点进行计算的暂时存储实现延时计算,等找到子模块的终点时,将延迟的计算进行重新计算,也就是利用模块之间的关系进行归操作。这是计算机的处理过程。网上有两种观点,一种是将递归黑盒化,根据表现在外面的特性为依据进行递归使用,毕竟递归太麻烦了,找到一个模型进行描述要简单清晰的多。另一种观点是将递归白盒化,理清递归的实质,从根源上理解递归,认识递归,从而使用递归,这种方法更深刻透彻一些,但是也更为复杂一些。n的阶乘等于n-1的阶乘乘以n,我们不知道n-1的阶乘,可以先将计算暂时存储,依次操作,直到知道某一个数的阶乘,然后回去将暂存的计算重新计算。该过程有两个要点,1处理的问题必须有一个已知的解,第二,模块之间有求解关系,并且求解过程可以通过暂存延后计算。
int fibonacci(int n)
{
if(n<=2)
return 1;
else
return fabonacci(n-1) + fibonacci(n-2);
}
int fibonacci(int n)
{
int a = 0;
int b = 1;
int c = 0;
for(int i=0; i<n; i++)
{
a = b;
b = c;
c = a + b;
}
return c;
}
斐波那契数列的求解和阶乘求解之间有一个共同的特点就是上一个模块的值需要下一个模块上午值来进行求解,不同的一点是对应关系不同,使用了向下两个模块的值,进行计算的延迟进行,函数体中进行的工作是求出当前模块的值,一方面是通过其他模块求出本模块的值,另一方面是直接给出本模块的值。这是函数体在形式上的统一性或者说是特点。
在字符串中查找指定字符
int stringSearch(char* a, int k, int m, char c)
{
if(k<=m)
{
if(a[k] == c)
return k;
else
return stringSearch(a, k+1, m, c);
}
else
return -1;
}
int stringSearch(char* a, int k, int m, char c)
{
while(k <= m)
{
if(c == a[k])
return k;
else
k++;
}
return -1;
}
前面两个例子都是一层模块依赖于另一个模块的值,是具体的数值,而且函数本身也是求的数值,该例子有点特殊,但是同样存在依赖关系。函数的结果与规模并没有直接的联系,也就是与当前的变量没有直接联系,并不像前两个例子一样。递归的利用更偏重于循环和模式套用。并不知道某个初始值的结果,或者说存在分支并不一定可以追溯到我们直到的初始值上。两个模块之间依赖关系并没有太强,上一个模块的值等同与下一个模块的值,或者说上个模块上午值等于下个模块乘1,当然这是形式上的统一,于感性认识上不统一。这属于一个尾递归。由于形式简单可以很容易的改写为循环语句形式。
int binarySearch(int* a, int k, int m, int t)
{
if(k <= m)
{
int mid = (k + m) >> 1;
if(t == a[mid])
return mid;
if(t > a[mid])
return binarySearch(a, mid + 1, m, t);
if(t < a[mid])
return binarySearch(a, k, mid - 1, t);
}
else
return -1;
}
int binarySearch(int* a, int k, int m, int t)
{
while(k <= m)
{
int mid = (k + m) >> 1;
if(t == a[mid])
return mid;
if(t > a[mid])
k = mid + 1;
if(t < a[mid])
m = mid - 1;
}
return -1;
}
二分查找是在有序数列的基础上加快元素查找。该例子也是一个尾递归的形式,不需要归操作或者是说是有归操作但是操作期间什么也没有做。递归可以分为几种情况,从实际问题分解上,递归的定义是将规模大的问题分解为规模小的问题进行求解,分解也分为号几种情况,分解的角度不同,使用递归的方式也不同。有的分解如,阶乘和斐波那契数列,我们求第n项的结果,我们知道第1项的结果,第n项依赖于第n-1项,我们可以将问题依据n的规模进行分解,通过线求解小规模的问题得到结果在进行求大规模的问题,而大规模的问题就是我们所追求的结果,递找到最小可解决的问题,归利用大小或者上下之间的关系,依次解决问题直至结果。第二种分解方法或者角度,如字符查找和二分查找,从n个对象中找到特定的对象的位置,该位置与n的规模并没有函数推导关系或者说没有直接联系。这时并不能像前面两个例子中可以依据n的规模进行分解或者说是这样分解可以解决问题,但是不是很合适。二分查找分解的方式是将整个字符串分为前后两段,利用已经排序完成数列的特点排除一半数据,并利用递归的循环特点进行逐步分解求解。在分解的过程中可能会发生一些感官概念的改变。查找元素变成了对拆分时中间元素的判定和是否不存在的验证。或者说模块之间的依赖关系变成了即对本身模块的依赖有对下一级模块的依赖。对于那些并不是一直递归到终点而是中途返回的情况,因为结果即依赖自己又依赖其他层次模块导致了这个特点。问题的性质不同,一个是计算某个值,另一个是查找某个值,可能可以根据该点的不同进行分开处理。退出方式和分解角度不同。
例如给出正整数 n=12345,希望以各位数的逆序形式输出,即输出54321。
数学表达:
void printDigital(int n)
{
cout<<n%10;
if(n>=10)
printDigital(n/10);
}
void printDigital(int n)
{
while(n >= 10)
{
cout<<n%10;
n = n/10;
}
cout<<n<<endl;
}
这也是一个尾递归,并且在递的过程中已经完成了任务,有递无归,主要是借助递归的循环特性。这个的分解没有什么可以遵循的,不像之前的层次之间存在函数关系,可以依据这个特点进行层次划分,这个层次之间没有关系,单纯的可以用递归的循环方式进行求解。这类问题比较难以分辨,只能根据递归本身的层次感和循环性进行契合。记住该例子,进行类比联想。
void printDigital(int n)
{
if(n>=10)
printDigital(n/10);
cout<<n%10;
}
void printDigital(int n)
{
int tmp[10];
int count=-1;
while(n>=10)
{
tmp[++count] = n%10;
n = n / 10;
}
tmp[++count] = n;
for(int i=count; i>=0; i--)
cout<<tmp[i];
cout<<endl;
}
该例子有递有归,递的过程单纯的递,没有操作,归的过程进行操作。层次划分同样有点无迹可寻,两个层次之间并没有明显的依赖关系,上层并不一定非要依赖下层的结果进行计算,可能需要的仅仅是递归。有递有归,递可扒皮探底,归可回溯返回。递归只是一种形式,跟递的过程是否进行操作,归的过程是否进行操作无关,都可以用递归的形式描述。非递归实现有点模拟递归的特点。找到一种方式,可以满足指定要求。
正读和倒读都一样的字符串称为回文字符串
bool isPalindromeString(const char* a, int k, int m)
{
if(k<m)
{
if(a[k] != a[m])
return false;
else
return isPalindromeString(a, k+1, m-1);
}
return true;
}
bool isPalindromeString(char* a, int k, int m)
{
while(k<=m)
{
if(a[k] != a[m])
return false;
else
{
k++;
m--;
}
}
return true;
}
回文字检测和字符查找基本雷同,都是在递的过程中解决问题,字符查找要递到底,回文字不一定需要到底。属于尾递归。
递归思想:
1.将x上的n-1个借助y移动到z上;
2.将x上的n圆盘放到y上;
3.将z上的n-1个圆盘借助x移动到y上;
void hanoi(int n, char from, char tmp, char to)
{
if(n>0)
{
hanoi(n-1, from, to, tmp);
cout<<"take "<<n<<" from "<<from<<" to "<<to<<endl;
hanoi(n-1, tmp, from, to);
}
}
多次使用递归,情况比较复杂,多次使用递归跟单次使用递归应该会有一些概念上的差异。递归的第一个式子会执行到底,递归的第二个式子会从底部开始在增加中递归到底。从函数内容上是完成了一个层面的处理。
将长度为n的不重复字符串进行全排列
递归思想:
从n个字符串中挑选出一个字符放在开始,剩下的n-1个字符进行全排列
void permutation(char* a, int k, int m)
{
if(k==m)
{
cout<<a<<endl;
}
else
{
for(int i=k; i<=m; i++)
{
char tmp = a[k];
a[k] = a[i];
a[i] = tmp;
permutation(a, k+1, m);
tmp = a[k];
a[k] = a[i];
a[i] = tmp;
}
}
}
这两个例子使用递归的方法不是很明朗,当然也与例子本身的复杂有关系。数学上计算全排列的公式:
先从m个元素中选出一个放到首位,这时有m种选择方法,然后再从m-1个元素中选出一个放在第二位,这时有m-1种方法,依次类推,直到第n位时,我们从m-n+1个元素中选出一个元素来,这时有m-n+1种方法,总共排列的个数如公式所示。如公式所示,我们构造排列的时候是按照从头到尾的顺序进行的,每一位的选取的过程都是类似的,都是从指定的元素中选取一位。该例子将循环和递归结合在了一起,从而具备了新的特点。
void combination(int* a, int k, int m, int n, vector<int>& res)
{
if(n>0)
{
for(int i=k; i<=m-n+1; i++)
{
res.push_back(a[i]);
combination(a, i+1, m, n-1, res);
res.pop_back();
}
}
else
{
for(int i=0; i<res.size(); i++)
cout<<res[i];
cout<<endl;
}
}
组合的递归实现和排列的递归实现很是类似,都是循环和递归相结合。组合的选择过程,在m个元素中选择n个元素进行组合,先选出一个元素,再从剩下的m-1个元素中选择n-1个元素,依次类推,直至从m-n个元素中选择0个元素。其中排列中选择的元素是在所有的元素中进行遍历,而组合并不是对所有的元素进行遍历,已经遍历过的元素将不在进行遍历。排列和组合不同的一点是相对顺序进行了改变,如果相对顺序不进行改变就不会出现重复的问题。