热门标签 | 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


推荐阅读
  • 本文详细介绍了MySQL数据库的基础语法与核心操作,涵盖从基础概念到具体应用的多个方面。首先,文章从基础知识入手,逐步深入到创建和修改数据表的操作。接着,详细讲解了如何进行数据的插入、更新与删除。在查询部分,不仅介绍了DISTINCT和LIMIT的使用方法,还探讨了排序、过滤和通配符的应用。此外,文章还涵盖了计算字段以及多种函数的使用,包括文本处理、日期和时间处理及数值处理等。通过这些内容,读者可以全面掌握MySQL数据库的核心操作技巧。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 深入解析:Synchronized 关键字在 Java 中对 int 和 Integer 对象的作用与影响
    深入探讨了 `Synchronized` 关键字在 Java 中对 `int` 和 `Integer` 对象的影响。尽管初看此题似乎简单,但其实质在于理解对象的概念。根据《Java编程思想》第二章的观点,一切皆为对象。本文详细分析了 `Synchronized` 关键字在不同数据类型上的作用机制,特别是对基本数据类型 `int` 和包装类 `Integer` 的区别处理,帮助读者深入理解 Java 中的同步机制及其在多线程环境中的应用。 ... [详细]
  • Linux CentOS 7 安装PostgreSQL 9.5.17 (源码编译)
    近日需要将PostgreSQL数据库从Windows中迁移到Linux中,LinuxCentOS7安装PostgreSQL9.5.17安装过程特此记录。安装环境&#x ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • poj 3352 Road Construction ... [详细]
  • 本文对SQL Server系统进行了基本概述,并深入解析了其核心功能。SQL Server不仅提供了强大的数据存储和管理能力,还支持复杂的查询操作和事务处理。通过MyEclipse、SQL Server和Tomcat的集成开发环境,可以高效地构建银行转账系统。在实现过程中,需要确保表单参数与后台代码中的属性值一致,同时在Servlet中处理用户登录验证,以确保系统的安全性和可靠性。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 开源实习机会 | Compiler SIG 正式发布实习任务,诚邀您加入申请!
    对编译技术充满兴趣却苦于无从入手?当前疫情形势下,外出实习变得困难重重?现在,Compiler SIG 正式发布了一系列实习任务,为有志之士提供了宝贵的机会。无论你是初学者还是有一定基础的学生,都能在这里找到适合自己的实践项目。我们诚挚邀请您的加入,共同探索编译技术的无限可能! ... [详细]
  • 如何利用正则表达式(regexp)实现高效的模式匹配?本文探讨了正则表达式在编程中的应用,并分析了一个示例程序中存在的问题。通过具体的代码示例,指出该程序在定义和使用正则表达式时的不当之处,旨在帮助读者更好地理解和应用正则表达式技术。 ... [详细]
  • 在CentOS 7上部署WebRTC网关Janus
    在CentOS 7上部署WebRTC网关Janus ... [详细]
  • 在 Windows 10 环境中,通过配置 Visual Studio Code (VSCode) 实现基于 Windows Subsystem for Linux (WSL) 的 C++ 开发,并启用智能代码提示功能。具体步骤包括安装 VSCode 及其相关插件,如 CCIntelliSense、TabNine 和 BracketPairColorizer,确保在 WSL 中顺利进行开发工作。此外,还详细介绍了如何在 Windows 10 中启用和配置 WSL,以实现无缝的跨平台开发体验。 ... [详细]
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社区 版权所有