数据结构与算法——递归简论
2016-05-08 11:04
274 查看
本文简述了基于C语言的递归(recursive)和使用递归的四条基本法则
我们先用数学语言来描述一下什么是递归,如:
F(X)= F(X) =
{0,2F(X−1)+x2, X=0 X≠0\begin{cases}
0, & \text{ $X=0$} \\
2F(X-1)+x^2, & \text{ $X\neq0$}
\end{cases}
当一个函数使用它自己来定义时就称为是递归。在C中,函数F(X)F(X)的实现如下:
实际上,递归调用在处理上与其他的调用没什么不同。如果以
我们可以看到,除00之外,对于任意的NN,程序都不可能算出结果。因此,我们可以得出递归的两个基本准则:
基准情形。设计递归时总要有某些基准的情形,它们不用递归就可以求解,如上面的N==0N==0的情况。
不断推进(making progress)。既然有了基准情形,那么对于那些需要递归求解的情形,递归调用必须总能够朝着基准情形的方向推进。
下面我们再来看看使用递归打印一个正整数NN的例子(假设现在的I/O只能处理单个数字并将其输出到终端,
? - 也许这里会产生疑问,“上面所说的基准情形如何定义?”。如果0≤N<100 \le N \lt 10,我们就使用
过程代码如下:
证明(前方高能)
首先,如果N只有一位数字,那么程序显然是正确的,因为它只需调用一次
然后,设
因此,我们可以得出递归的第三条法则:
3. 设计法则(design rule)。假设所有的递归调用都能运行(这也是上面为什么要证明的原因)。当设计递归程序时一般没有必要知道簿记管理的细节,因为有时追踪实际的递归调用序列是非常困难的。另一方面,这也体现了递归的好处——计算机能够算出复杂的细节。
递归的主要问题是隐含的簿记开销,虽然这些开销几乎总是合理的(既简化了算法设计,又给出了更加简介的代码),但要注意的是,不要尝试用递归来代替简单的for循环。
最后,递归的第四条法则是:
4. 合成效益法则(compound interest rule)。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
Reference:
[1]. Data Structures and Algorithm Analysis in C Second Edition, Mark Allen Weiss
我们先用数学语言来描述一下什么是递归,如:
F(X)= F(X) =
{0,2F(X−1)+x2, X=0 X≠0\begin{cases}
0, & \text{ $X=0$} \\
2F(X-1)+x^2, & \text{ $X\neq0$}
\end{cases}
当一个函数使用它自己来定义时就称为是递归。在C中,函数F(X)F(X)的实现如下:
int F( int X ){ if( X == 0 ) return 0; else return 2*F(X-1)+X*X; }
实际上,递归调用在处理上与其他的调用没什么不同。如果以
F(4)调用函数
F(int x),那么程序就会计算2F(3)+4∗42F(3)+4*4,紧接着调用
F(3)……此时,F(0)F(0)必须被赋值,否则,程序将会不断地执行下去,直至崩溃。我们把F(0)=0F(0)=0的情况叫做基准情形(base case)。我们再来看一个错误使用递归的例子:
int Bad( unsigned int N){ if (N == 0) return 0; else return Bad(N/3+1)+N-1; }
我们可以看到,除00之外,对于任意的NN,程序都不可能算出结果。因此,我们可以得出递归的两个基本准则:
基准情形。设计递归时总要有某些基准的情形,它们不用递归就可以求解,如上面的N==0N==0的情况。
不断推进(making progress)。既然有了基准情形,那么对于那些需要递归求解的情形,递归调用必须总能够朝着基准情形的方向推进。
下面我们再来看看使用递归打印一个正整数NN的例子(假设现在的I/O只能处理单个数字并将其输出到终端,
Printout(N)为处理单个数字的输出函数),例如,
PrintDigit(4)就是将“4”输出到终端。现在,我们需要实现将“12345”输出到终端,首先需要打印出‘1’,然后是‘2’……假设我们已经打印出了”1234”,再打印’5’时,使用语句
PrintDigit(N%10)就可以完成。对于前面的情况,我们可以用同样的方法解决。因此,我们可以使用语句
PrintOut(N/10)递归地解决这个问题。
? - 也许这里会产生疑问,“上面所说的基准情形如何定义?”。如果0≤N<100 \le N \lt 10,我们就使用
PrintDigit(N)直接输出NN,所以
PrintDigit(N)就是基准情形。而对于一个正整数NN,我们可以通过
PrintOut(N)来用较小的正整数定义它,这样也保证了递归的不断推进。
过程代码如下:
void PrintOut( unsigned int N ){ if(N >= 10) printOut( N/10 ); PrintDigit( N%10 ); }
证明(前方高能)
首先,如果N只有一位数字,那么程序显然是正确的,因为它只需调用一次
PrintDigit(N)。
然后,设
PrintOut(N)对所有kk位或者位数更少的数都有效。对于k+1k+1位的数,可以通过前kk位数字和最后一位数字来表示。前kk位数字就是程序中的
(int)(N/10),即N/10N/10后向下取整,而最后一位数字是Nmod10N mod 10(
N%10)。因此,该程序能够正确地打印出任意k+1k+1位数。于是,根据归纳法,所有的数都能被正确地打印出来。
因此,我们可以得出递归的第三条法则:
3. 设计法则(design rule)。假设所有的递归调用都能运行(这也是上面为什么要证明的原因)。当设计递归程序时一般没有必要知道簿记管理的细节,因为有时追踪实际的递归调用序列是非常困难的。另一方面,这也体现了递归的好处——计算机能够算出复杂的细节。
递归的主要问题是隐含的簿记开销,虽然这些开销几乎总是合理的(既简化了算法设计,又给出了更加简介的代码),但要注意的是,不要尝试用递归来代替简单的for循环。
最后,递归的第四条法则是:
4. 合成效益法则(compound interest rule)。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
Reference:
[1]. Data Structures and Algorithm Analysis in C Second Edition, Mark Allen Weiss
相关文章推荐
- 【搜索引擎】搜索引擎索引数据结构和算法
- 第1章 概述
- 【Codevs】1082 线段树练习 3 && 线段树模板
- Java千百问_06数据结构(012)_如何遍历数组
- 数据结构上机测试1:顺序表的应用
- 数据结构中二叉树的三种遍历的非递归写法
- 排序算法(综述)
- 排序算法——直接插入排序
- 数据结构之队列
- 数据结构- 线段树
- 速查表:常用算法和数据结构的复杂度
- 数据结构—概述
- 数据结构——线性表概述
- 数据结构——顺序表
- redis底层数据结构之adlist
- 数据结构-链表-作业
- 基本数据结构:链表(list)
- 数据结构学习笔记06排序 (快速排序、表排序、基数排序)
- 清北学堂学习总结 day1 数据结构 练习
- Java千百问_06数据结构(011)_java中的数组是什么