一些基础——递归、算法分析

本部分为数据结构与算法分析的基础部分,主要涉及递归(原则、组成、示例)、算法分析(几种表示方法、分析原则、检验等)。

递归

从定义上看,当一个函数用它本身来定义时就称该函数为递归的,即我定义我自己。需要注意的是,递归和循环不同,递归只是使用了自身作为定义的一部分,但其调用时是类似于数学的递推,而循环则类似于一直调用同样的参数值、函数。

  1. 递归结构的一般组成、执行过程

    · 一般组成
    递归一般由处理基本情形的语句、递归调用语句两部分组成。基本情形类似于数学中的递推公式的f(0)。如果没有基本情形,递归就会持续调用,导致栈空间溢出(stack overflow),使程序崩溃。

    斐波那契数列就可以用递归来实现(尽管这不是一个好想法,因为做了重复的工作使得效率低),示例代码如下:
     
    int fib(int a)//斐波那契数列
    {
    	if(a>0)
    	{
    		if(a==1 || a==2)//数列的基本情形
    			return 1;
    		else
    			return fib(a-1)+fib(a-2);
    	}
    	else
    		return -1;
    }
    · 执行过程​
    以上文的斐波那契数列fib(a)为例,假设调用fib(4),那么其将返回fib(3)+fib(2),此时将相继调用fib(3)、fib(2);调用fib(3)的过程同fib(4),返回fib(2)+fib(1);调用fib(2)、fib(1),将返回1。

    后再将fib(2)、fib(1)返回值拷贝到调用fib(3)的地址中,作为fib(3)的返回值;将fib(3)、fib(2)返回值拷贝到调用fib(4)的地址中,作为fib(4)的返回值。


     
  2. 递归调用与栈

    递归调用的实际上可以用一个栈来表示。假设F(x)为一个递归函数,其基准情况是F(1),则调用F(4)的过程如下:


    在实际调用过程中,计算机会分配一个栈空间来记录/储存调用信息以帮助返回值,若没有基准情况,就会导致调用超出栈大小,即栈溢出。
     
  3. 递归的基本法则

    若需要将函数设计成递归的,那么需要依照以下原则进行:

    #基准情况,即保证有不需要递归即可获得返回值的情况,
    #不断推进,即能够使得递归调用能朝着基准情形前进
    #假设所有的递归调用都能运行(因此设计不需考虑那么多关于递归调用序列的细节,尽管debug时还是需要考虑这些细节出现了什么差错。。)
    #合成效益,即求解同一个问题时不做重复的递归调用(例如斐波那契数列数列中,调用fib(4)实际上需要重复调用f(2)两次,降低了效率)。

    (一些感想:但这些也只是原则,实际中只有能想到函数执行过程是递归时这些原则才有用,这恐怕是个思维问题而不只是设计问题。多刷题吧!)
     
  4. 尾递归

算法分析

算法分析主要解决如何评价一个算法的优缺点,主要涉及算法所需的时间、空间开销,两者一般以时间复杂度、空间复杂度来表示。

  1. 函数的相对增长率——大O、小O、Ω、θ记法

    算法分析中使用函数的相对增长率来估计算法运行的开销的界限(上界与下界,一般需要使用的是上界)。其需要使用以下定义:(假定算法所需的开销为T(N),N为对时间、操作次数等量的计量数

    #大O记法O(N):若存在正常数c和n_{0},使得N\geqslant n_{0}时,T(N)≤cf(N),则记为T(N)=O(f(N))。
    #Ω记法Ω(N):若存在正常数c和n_{0},使得N\geqslant n_{0}时,T(N)≥cf(N),则记为T(N)=Ω(f(N))。
    #θ记法θ(N):当且仅当T(N)=O(f(N))且T(N)=Ω(f(N))时,则记为T(N)=θ(f(N))。
    #小o记法o(N):当且仅当T(N)=O(f(N))且T(N)≠θ(f(N))时,则记为T(N)=o(f(N))

    上述定义表明算法分析中只关注相对增长率及其趋势,而不是具体某一点上两个函数的相对大小。

    当我们说T(N)=O(f(N))时,实际上是在说T(N)以不高于f(N)的速度增长,f(N)实际上是T(N)的一个上界。同理T(N)=Ω(h(N))时,h(N)实际上是T(N)的一个下界。

    需要注意的是大O记法中,对f(N)可以进行适当的简化,例如舍弃常数、低阶项,只说明其增长的量级即可。(但这是N非常非常大时的简化。若N很小,这些常数项、低阶项造成了各种算法运行的差异)
     
  2. 一些有用的分析法则

    · O(N)记法的一些法则
    在实际运用过程中,可以使用数学方法进行函数增长率的比较(例如洛必达法则),但更常用的是使用以下法则:

    #若T1(N)=O(f(N)),且T2(N)=O(g(N)),那么:
    (a)T1(N)+T2(N)=O(max { f(N),g(N) } )。
    (a)T1(N) * T2(N)=O( f(N) * g(N)  )。

    #若T(N)是一个k次多项式,那么T(N)=O(N^k)。
    #对于任意常数k,(log N)^k=O(N),即对数增长的非常非常慢。

    · for循环、顺序、选择语句的运行时间分析法则
    #一次for循环
    :运行时间,最多为循环内语句的运行时间×迭代次数。
    #嵌套for循环:需要从里到外分析这些for循环,循环内的一条语句的总运行时间为该语句运行时间×所有for循环大小
    #顺序语句:运行时间即为各语句的时间之和,若均为大O记法,则以增长率最快的一项为最终的记法。
    #if/else语句:不超过判断的时间+max{ if语句块的运行时间,else语句块的运行时间 }。

    · 洛必达法则与各种记法的关系
    计算极限\lim_{n \to\infty }\frac{f(N)}{g(N)},可以根据其极限值c判断二者的关系:
    #c=0:f(N)=o(g(N))
    #c≠0:f(N)=θ(g(N))
    #c=∞:g(N)=o(f(N))

    #c为波动值:二者无关
  3. 一些典型的增长率

    以下是在算法分析中常用的增长率:(按照增长速度从慢到块排列)
    函数 名称
    c常数级
    logN对数级
    log^{2}N对数平方级
    N线性级
    NlogN
    N^{2}平方级
    N^{3}立方级
    2^{N}指数级
    对于输入量大的问题,开销为平方级、立方级、指数级的算法是需要避免的,一般NlogN、线性级、对数级甚至常数级的算法则是追求目标。
     
  4. 算法分析的模型——标准计算机

    算法分析的模型是基于标准计算机,在该机器中顺序执行指令(如+、*、比较、赋值等),并且该模型机做任意一件工作都恰好花费一个时间单元(但现实的计算机做不同工作所需的时间并不同)。此外,模型机还具有固定范围的整数、无限的内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值