C++抽象编程——递归简介(6)——相互递归与递归思想总结

Mutual recursion

在大多数的例子中,大多数的递归程序都会直接调用他们自己本身,或者是里面的函数体中包含了调用它自己的函数。作为递归函数,它必须在程序执行的某个过程调用它本身。如果一个函数,被分割成更小的函数,而递归调用发生在更小的函数上,这样的程序我们也称为递归。比如有一个函数f,里面调用了函数g,而函数g里面又调用了函数f。像这种函数f跟函数g,相互调用,那么我们就称这样的递归为相互递归Because the functions ƒ and g call each other, this type of recursion is called mutual recursion)。

一个最为简单的相互递归例子(尽管这个例子不高效率),就是判断一个数是是偶数,还是奇数。从我们所学的知识我们可以知道有下面的定义:

A number is even if its predecessor is odd.  一个数是偶数如果它的前一个数是奇数

A number is odd if is not even.   一个数不是偶数就是奇数
The number 0 is even by definition  定义0是偶数

显然,我们可以通过定义两bool类型的函数来判断是否为奇数还是偶数,代码如下:

#include 
using namespace std;
bool isOdd(unsigned int n);
bool isEven(unsigned int n);
int main() {
	int n;
	cin >> n;
	if(isOdd(n)) {
		cout << "this number is odd" << endl;
	}else {
		cout << "this number is even" << endl;
	}
	return 0;
}
bool isOdd(unsigned int n) {
	return !isEven(n);
}
bool isEven(unsigned int n) {
	if(n==0) {
		return true;
	}else{
		return isOdd(n-1);
	}
}


运行结果如图:

C++抽象编程——递归简介(6)——相互递归与递归思想总结_第1张图片C++抽象编程——递归简介(6)——相互递归与递归思想总结_第2张图片

unsigned类型在C++中代表的是int型中的大于等于0的数(非负数)。

7.7 Thinking recursively

递归思想,对于大多数人来说,并不是那么好接受的,因为它迫使我们以全新的思维方式去解决问题。成功的掌握这门技巧的关键是学会如何递归的思考问题。接下来的讨论就是让你达到这个目的,了解递归的思想。

Maintaining a holistic perspective

保持整体观念。简单的说,部分思想就是相信一个对象整体能够划分为一个个能被人理解的部分,然后再将他们重新组合起来。而与之相反的是整体思想,他们往往更加相信部分大于整体。当你学习编程的时候,它们有助于你交替使用这个两种思想。有时候我们侧重于关注一个程序的行为,而其他时间我们可能更加深入了解执行的具体的细节。而在你学习递归的时候,这种平衡就被打破了。因为递归得思考要求你从整体开始入手,在递归中,部分思想就是你理解递归的的敌人,它常常阻碍你的思考

为了维持你的整体观念,你要接受recursive leap of faith这个思想,每当你写一个递归程序或试图理解一个程序的行为时,你要知道,你要忽略每个细节和单个递归调用。 一旦你选择了正确的分解,确定了适当的simple case,并正确运行您的函数,那些递归调用将自动地工作。 你不需要考虑他们是怎么完成的。可惜,直到你能熟练应用递归之前应用recursive leap of faith并不是那么简单的,因为当你应用这个思想的时候,它要求你停止你对程序的怀疑,并且假设自己的程序是正确的。不过毕竟大多数情况下,你的程序都不会一次完美的运行,即便你是个很有经验的程序员。 这个时候,很可能是你选择了错误的分解,弄乱了simple case的定义又或者是程序运行过程中递归出现了错乱。出现这些情况你的程序都不会正常运行。

因此,当程序出现问题的时候,你要记得在正确的地方寻找错误。问题肯定出现在你的递归实现,递归机制本身是没有问题的。 如果有问题,你应该能够通过查看递归层次结构,一级一级的找到它,就像fact树状图那样。通过额外的递归调用去向下寻找通常没有帮助的。当你的分解无误,且你的simple case正确,他们就可以正确运行,否则,你就可以检查一下你的公式的正确性了。

Avoiding the common pitfalls

当你练习递归程序的时候,debug是不可避免的,但是对于初学者来说,在递归程序中找到需要修改的点并不容易,所以我们可以从下面的一些方面去考虑:

1.你的递归程序是从simple case开始检查的吗?(Does your recursive implementation begin by checking for simple cases?) 在你尝试将这个问题转化成子问题的时候,你必须考虑这个问题时候真的可以简单到不需要你分解就能执行的simple case。大多数情况下,程序都是从if语句开始的,如果不对,那么你就得认真检查一下你的程序了,搞清楚你在干什么。

2.你是否正确的解决了simple case?(Have you solved the simple cases correctly?)一个惊人的数量显示,大多数错误的递归程序错误来自于simple case的错误。如果你这一步错了,那么你的递归程序就会继承这个错误,导致终的结果总是错误。比如我们的fact(0)=0,如果你写成fact(0)=1,那么无论在哪调用fact函数,他都会返回0这个数值。

3.你的递归分解有让问题变得更加简单吗?(Does your recursive decomposition make the problem simpler?让递归工作,你得让问题变得更加简单。更为正式的是,这里还有一个指标(测量问题难度的标准),像fact和fib函数,输入的整数就是它的指标,每一次调用这个函数,n的值就相应的减小,问题也就变得更加简单了。对于isPalindrome函数,字符串的长度就是指标,因为每一次的递归调用,字符串的长度就相应的减短了。相反 如果问题实例不会变得更简单,分解过程只会保持越来越多的调用,产生无限的递归循环,这被称为非终止递归(nonterminating recursion.)

4.简化过程是否达到了simple case?或者说你是否遗漏了一些特殊情况?(Does the simplification process eventually reach the simple cases, or have you left out some of the possibilities?)一个常见的错误是没有包括simple case测试作为递归分解的结果。例如,在isPalindrome函数实现中,对于函数检查空字符和单字符的考虑,这是非常重要的,即使我们不打算输入空字符串调用isPalindrome 。 随着递归分解的进行,字符串参数在递归调用的每个级别上缩短两个字符。 如果原始参数字符串的长度是偶数,则递归分解将永远不会达到单字符大小写,最终为空字符。

5.函数中的递归调用表示真正的子问题是否在形式上与原来相同?(Do the recursive calls in your function represent subproblems that are truly identical in form to the original?) 当你使用递归来分解一个问题,子问题必须具有相同的形式。 如果递归调用改变了问题的本质或者违反了我们起初的一个假设,整个过程就会被瓦解。 在这里由定义导出的函数通常很有用,就比如我们用到的wrapper函数,调用更一般的递归函数来实现功能。 因为调用wrapper函数会有有一个更一般的形式,它是通常更容易分解原来的问题,并且有它同样适合递归结构。

6.当你使用 leap of faith 的时候,递归子问题的答案是否为原始的问题提供了一个完整的答案?(When you apply the recursive leap of faith, do the solutions to the recursive subproblems provide a complete solution to the original problem?)将问题分解为更小的递归实例只是递归过程的一小部分,一旦得到答案,你还必须能够重新组合它们来生成完整的答案。 检查这个过程是否实际实现的办法是遍历每一个分解,充分地应用leap of faith。遍历的时候假设每一个函数都能完美的调用,如果跟着这个进程走,你能得到正确的答案,那么程序就能正确的运行。

你可能感兴趣的:(抽象编程(C++),C++学习与基础算法)