热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

迭代,递归好文章

先讲个故事吧。从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山

先讲个故事吧。

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”。

这个故事永远也讲不完,因为没有递归结束条件。老师讲递归时总是说,递归很简单,一个递归结束条件,一个自己调用自己。如果递归没有结束条件,那么就会无限递归下去。在编程的时候,没有递归结束条件或者递归过深,一般会造成栈溢出。

下面这个函数,可以利用栈溢出来估测栈的大小:


1
2
3
4
5
6
7
8
void stack_size()
{
    static int call_time = 0;
    char dummy[1024*1024];
    call_time++;
    printf("call time: %d\n",call_time);
    stack_size();
}

这个函数定义了1M的局部变量,然后调用自己。栈溢出时会崩溃,根据最后打印出的数字可以算一下栈的大小。

递归算法一般用于解决三类问题:

(1)数据的定义是按递归定义的。(Fibonacci函数)

(2)问题解法按递归算法实现。(回溯)

(3)数据的结构形式是按递归定义的。(树的遍历,图的搜索)

对于求1+2+3+…+n这种问题,大部分人不会用递归方式求解:


1
2
3
4
5
6
7
int sum1(int n)
{
    if(n == 0)
        return 0;
    else
        return n+sum1(n-1);
}

而是使用迭代的方式:


1
2
3
4
5
6
7
int sum2(int n)
{
    int ret = 0;
    for(int i &#61; 1;  i <&#61; n; i&#43;&#43;)
              ret &#43;&#61; i;
    return ret;
}


迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快、适合做重复性操作的特点&#xff0c;让计算机对一组指令&#xff08;或一定步骤&#xff09;进行重复执行&#xff0c;在每次执行这组指令&#xff08;或这些步骤&#xff09;时&#xff0c;都从变量的原值推出它的一个新值。

为什么使用迭代而不用递归呢&#xff1f;

很明显&#xff0c;使用递归时每调用一次&#xff0c;就需要在栈上开辟一块空间&#xff0c;而使用递归就不需要了&#xff0c;因此&#xff0c;很多时候设计出了递归算法&#xff0c;还要想法设法修改成迭代算法。

假如现在我们不考虑编程&#xff0c;我们仅仅看一下上面使用递归和迭代求1&#43;2&#43;3…&#43;n的过程。

使用递归&#xff1a;

sum(5)
5&#43;sum(4)
5&#43;4&#43;sum(3)
5&#43;4&#43;3&#43;sum(2)
5&#43;4&#43;3&#43;2&#43;sum(1)
5&#43;4&#43;3&#43;2&#43;1&#43;sum(0)
5&#43;4&#43;3&#43;2&#43;1&#43;0
5&#43;4&#43;3&#43;2&#43;1
5&#43;4&#43;3&#43;3
5&#43;4&#43;6
5&#43;10
15

使用迭代

0&#43;1&#61;1
1&#43;2&#61;3
3&#43;3&#61;6
6&#43;4&#61;10
10&#43;5&#61;15

上面两个计算过程所需的步骤都是O(n)。但是两个计算过程的形状不一样。

递归过程是一个先逐步展开而后收缩的形状&#xff0c;在展开阶段&#xff0c;这一计算过程构造起一个推迟进行的操作所形成的的链条&#xff08;这里是&#43;)&#xff0c;在收缩阶段才会实际执行这些操作。这种类型的计算过程由一个推迟执行的运算链条刻画&#xff0c;称为一个递归计算过程。要执行这种计算过程&#xff0c;就需要维护以后将要执行的操作的轨迹。在计算1&#43;2&#43;3&#43;…&#43;n时&#xff0c;推迟执行的加法链条的长度就是为了保存其轨迹需要保存的信息量&#xff0c;这个长度随着n值而线性增长&#xff0c;这样的过程称为线性递归过程。

迭代过程的形成没有任何增长或收缩。对于任意一个n&#xff0c;在计算的每一步&#xff0c;我们需要保存的就只有i,ret&#xff0c;这个过程就是一个迭代计算过程。一般来说&#xff0c;迭代计算过程就是那种其状态可以用固定数目的状态变量描述的结算过程。在计算1&#43;2&#43;…&#43;n时&#xff0c;所需的计算步骤与n成正比&#xff0c;这种过程称为线性迭代过程。

现在再回到编程语言中。

上面提到的推迟执行的运算链条就存在栈里&#xff0c;由于栈很小&#xff0c;如果链条太长&#xff0c;就会溢出了。

那我们再来看下面的函数


1
2
3
4
5
6
7
int sum3(int n, int acc)
{
    if(n &#61;&#61; 0)
        return acc;
    else
        return sum3(n-1,acc&#43;n);
}

调用的时候acc&#61;0&#xff0c;以sum(5,0)为例这是一个递归函数&#xff0c;我们来看看它的计算过程。

sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)
15

这个计算过程是递归的还是迭代的呢&#xff1f;

是迭代的&#xff01;

但是命名函数sum又调用了自己。

我们需要将递归计算过程与递归过程分隔开。

当我们说一个过程(函数)是递归的时候&#xff0c;论述的是一个语法形式上的事实&#xff0c;说明这个过程的定义中(直接或间接的)调用了自己。我们说一个计算过程具有某种模式时(例如线性递归)&#xff0c;我们说的是这一计算过程的进展方式&#xff0c;而不是过程说些上的语法形式。

一个递归过程&#xff0c;如果它的计算过程是迭代的&#xff0c;那么我们称这种递归为尾递归。尾递归不需要保存递归的推迟计算链&#xff0c;那么是不是就意味着不会造成栈溢出了&#xff1f;

我们来试一下


1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum3(int n, int acc)
{
    if(n &#61;&#61; 0)
        return acc;
    else
        return sum3(n-1,acc&#43;n);
}
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d\n",sum(n,0));
    return 0;
}


运行结果
2013062001.png

看来还是会栈溢出。

为啥呢&#xff1f;因为c语言默认不会对尾递归进行优化&#xff0c;即使你的程序是尾递归的&#xff0c;它还是按一般的递归进行编译。加上优化选项就可以对尾递归进行优化。

2013062002-300x130.png

下面哪些是尾递归呢&#xff1f;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int fib(int n)
{
    if(n &#61;&#61; 0 || n &#61;&#61; 1)
        return 1;
    else
        return fib(n-1) &#43; fib(n-2);
}
void qsort(int A, int p, int q)
{
    r &#61; partition(A,p,q);
    qsort(A,p,r-1);
    qsort(A,r&#43;1,q);
}
int gcd(int a, int b)
{
    if(b &#61;&#61; 0)
        return a;
    else
        gcd(b, a%b);
}

在函数式编程语言中&#xff0c;不存在变量&#xff0c;因此任何的循环都需要用递归实现。如果递归使用了尾递归&#xff0c;那么编译器或解释器能自动优化&#xff0c;如果不是尾递归&#xff0c;那么就存在栈溢出的风险。前面两个不是尾递归&#xff0c;第三个是尾递归。

任何递归都可以转化成迭代&#xff0c;那么任何递归都可以转化成尾递归。

斐波那契数列改成尾递归后如下


1
2
3
4
5
6
7
8
9
10
11
12
13
int fib(int n,int count, int a , int b)
{
    if(n &#61;&#61; 0 || n &#61;&#61; 1)
        return 1;
    else if (count > n)
        return b;
    else
        return fib(n,count&#43;1,b,a&#43;b);
}
int FIB(int n)
{
    return fib(n,2,1,1);
}

下面这段代码


1
2
3
i &#61; 1, ret &#61; 0
for(;i <&#61; n; i&#43;&#43;)
        ret &#43;&#61; i;

对应的递归形式就是


1
2
3
4
5
6
int fun(int i, int ret) {
    if(i > n)
        return ret;
    else
        return fun(ret&#43;i,i&#43;1);
}

fun(1,0)相当于给i和ret赋初值。

如果将快速排序改成迭代的话&#xff0c;那么需要一个栈&#xff01;它的变量个数是有限的吗&#xff1f;我们可以把栈看成一个变量就可以了。

先修改成迭代形式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void qsort_iterate(int a[],int p,int q)
{
        stack s;
        s.push(p);
        s.push(q);
        while(!s.empty())
        {
                int high &#61; s.top();
                s.pop();
                int low &#61; s.top();
                s.pop();
                if(high > low)
                {
                        int r &#61; partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r&#43;1);
                        s.push(high);
                }
        }
}

上面的迭代形式可以很容易的改成尾递归&#xff1a;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void qsort_tail(int a[],stack s)
{
        if(!s.empty())
        {
                int high &#61; s.top();
                s.pop();
                int low &#61; s.top();
                s.pop();
                if(high > low)
                {
                        int r &#61; partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r&#43;1);
                        s.push(high);
                }
                qsort_tail(a,s);
        }
}

那么在函数式编程语言里&#xff0c;快排是不是就是这样实现的&#xff1f;答案是No。函数式编程为什么不能用循环&#xff1f;就是因为没有变量&#xff0c;所以在函数式编程语言里不能进行原地排序的。


1
2
3
4
5
6
7
8
(define (qsort s)
  (cond ((null? s) s)
        ((null? (cdr s)) s)
        (else
         (let ((h (car s))
               (left (filter (lambda (x) (<&#61; x (car s))) (cdr s)))
               (right (filter (lambda (x) (> x (car s))) (cdr s))))
           (append (qsort left) (list h) (qsort right))))))

我们把这段代码翻译成Python(翻译成C或者C&#43;&#43;挺啰嗦的)上面这段代码是用Lisp的方言Scheme实现的&#xff0c;不是尾递归的。


1
2
3
4
5
6
7
8
9
10
11
12
13
def qsort_lisp(A):
    if len(A) &#61;&#61; 0 or len(A) &#61;&#61; 1:
        return A
    left &#61; []
    right &#61; []
    pivot &#61; A[0]
    for i in range(1,len(A)):
        if A[i]             left.append(A[i]);
        else:
            right.append(A[i]);
    return qsort_lisp(left) &#43; [pivot] &#43; qsort_lisp(right)
x &#61; [3,4,5,6,2,34,6,2,2,5,7,2,7]
print qsort_lisp(x)

其实刚才我说谎了&#xff0c;大部分函数式编程语言&#xff0c;例如Scheme,Erlang,Clojure等都提供可变的变量&#xff0c;数据库里有上G的数据&#xff0c;不能把它拷贝一份在写回去&#xff0c;这时候就需要使用真正的变量了。函数式编程语言都是比较高级的语言&#xff0c;排序时一般使用自带的sort函数就行了。上面这段代码没有对变量做修改的操作&#xff0c;所以可以看做是函数式编程。这个函数能改成尾递归吗&#xff1f;应该是可以的&#xff0c;但是挺麻烦的&#xff0c;我是没找到好办法。到网上找了找也没找到好的方法。

总结一下尾递归&#xff1a;
(1)计算过程是迭代的
(2)在函数最后一步调用自己&#xff0c;而且是仅有调用语句&#xff0c;或者是一句fun()&#xff0c;或者是return fun()&#xff0c;不存在x &#61; fun(&#xff09;这样的情况
(3)函数执行最后一句调用自己的语句时&#xff0c;将状态变量以参数形式传递给下一次调用&#xff0c;自己的栈没用了&#xff0c;形象的说&#xff0c;它告诉下一次被调用的函数&#xff0c;我已经死了&#xff0c;你干完活后直接向我的上级报告就行了&#xff0c;不需要和我说了
(4)gcc开启优化选项后可以对尾递归进行优化&#xff0c;大部分函数式编程语言会对尾递归进行优化


本文出自 “牛哥的博客” 博客&#xff0c;请务必保留此出处http://nxlhero.blog.51cto.com/962631/1231228


推荐阅读
  • 本文介绍了贝叶斯垃圾邮件分类的机器学习代码,代码来源于https://www.cnblogs.com/huangyc/p/10327209.html,并对代码进行了简介。朴素贝叶斯分类器训练函数包括求p(Ci)和基于词汇表的p(w|Ci)。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文介绍了[从头学数学]中第101节关于比例的相关问题的研究和修炼过程。主要内容包括[机器小伟]和[工程师阿伟]一起研究比例的相关问题,并给出了一个求比例的函数scale的实现。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • EPPlus绘制刻度线的方法及示例代码
    本文介绍了使用EPPlus绘制刻度线的方法,并提供了示例代码。通过ExcelPackage类和List对象,可以实现在Excel中绘制刻度线的功能。具体的方法和示例代码在文章中进行了详细的介绍和演示。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
author-avatar
jiajian123
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有