• 数据结构与算法复习——2、递归分析入门


    2、递归分析入门

    一、引例

      上一篇介绍的最长子序列和问题的分治算法的分析中,提出了一个递推式,我们没有求解该递推式的上界。实际上,我们经常在递归算法的分析里遇到递推式,很显然这是由于递归本身的结构决定的。这一篇我们就简单地介绍一下怎么做分治算法下的递归分析。实际上递归有多种情况,除了分治算法,比较常见的还有搜索,这就不在本篇的讨论范围之内。

      在分析分治算法之前,先来分析一个不太“优”的递归算法。看下面的求解斐波那契数列第$N$项的算法:

    1 int Fib(int N) {
    2     if (N == 0 || N == 1)
    3         return 1;
    4     return Fib(N - 1) + Fib(N - 2);
    5 }
    Fibonacci

     这个算法经常被当作递归的“反面教材”,因为它的冗余递归太多:求解$Fib(N-1)$时,实际上已经求了$Fib(N-2)$,可是后面又调用了一次。这个算法有多坏呢?我们建立下面的关于这个算法时间上界的递推式:

    $T(0)=T(1)=1$

    $T(N)=T(N-1)+T(N-2)$

    可以很容易就发现$T(N)=Fib_N$,也就是时间以斐波那契数列级增长。斐波那契数列有很多研究,譬如前后两项之比的极限是黄金分割比$Phi = frac{sqrt{5}+1}{2}$,这告诉我们这个算法的时间是指数级增长!指数增长一般是难以忍受的,常见情况里仅快于阶乘增长。可见上面这个算法有多差。

      上面的分析已经告诉了我们怎么做递归分析,就是求其时间上界数列的递推式,从递推式里求解通项公式或者至少知道增长等级。这个算法一旦优化就不适合递归了,因此这里就不对它进行优化了。下面我们分析一个经典的分治递归算法。

    二、归并排序分析

      排序算法是非常重要而基础的,我们有好多种排序算法,而不管怎么考虑,归并排序都一定是其中最经典的之一。简要来说,归并排序的思路是这样的:递归地求解,假设我们已经有了两个排好序的$N$项序列,把它们合并成$2N$项的有序序列就好了。至于这两个序列的来源,我们可以把它们等分成两部分,让这两部分是各自排好序的,然后合并;可见这个过程应该递归下去。这个递归当然有基本情况:$N=1$时,无需再分即有序。归并排序的主算法可以这样实现:

    1 void merge_Sort(int A[], int l, int r) {
    2     if (r - l > 1) {
    3         merge_Sort(A, l, (l + r) / 2);  //分治
    4         merge_Sort(A, (l + r) / 2, r);
    5     }
    6     _merge_sort(A, l, (l + r) / 2, r);  //归并
    7     return;
    8 }
    merge sort

    其中第6行的归并函数的某种实现方式如下:

     1 int tempL[MXN], tempR[MXN];  //归并排序的临时数组
     2 
     3 void _merge_sort(int A[], int l, int mid, int r) {  //归并步骤
     4     for (int i = l; i < mid; i++) tempL[i - l] = A[i];  //将两侧的数记入临时数组
     5     for (int j = mid; j < r; j++) tempR[j - mid] = A[j];
     6     tempL[mid - l] = tempR[r - mid] = 0x7fffffff;
     7     int cnt = l, i = 0, j = 0;
     8     for (; l + i < mid && mid + j < r;cnt++) {  //比较归并
     9         if (tempL[i] <= tempR[j]) A[cnt] = tempL[i++];
    10         else A[cnt] = tempR[j++];
    11     }
    12     while (l + i < mid) A[cnt++] = tempL[i++];  //余项归并
    13     while (mid + j < r) A[cnt++] = tempR[j++];
    14     return;
    15 }
    merge

    当然归并排序的主算法和归并函数有很多种实现和优化的方式,这里就不展开了,但是总之,归并函数的时间复杂度上界不可能低于$O(N)$。

    下面我们就可以来分析归并排序的时间复杂度。根据主算法,写出它的时间上界的递推式:

    $T(N) = 2T(N/2) + N$

    为了方便分析,我们首先假设:$N = 2^k$,这样$N/2$就有一直都是有意义的。这种情况下,我们有两种处理方式:

    1、做这样的变换:

    $frac{T(N)}{N} = frac{T(N/2)}{N/2} + 1$

    可见${ frac{T(2^k)}{2^k} }$是一个关于$k$的等差数列,又由于$frac{T(1)}{1} = 1$,我们就可以求出:

    $frac{T(N)}{N} = k + 1 = log N + 1$

    $T(N) = N log N + N = O(N log N)$

    这是第一种分析方法。

    2、第二种分析方法比较“暴力”:有下面两个式子:

    $T(N) = 2T(N/2) + N$

    $T(N/2)=2T(N/4)+N/2$

    代入得:

    $T(N) = 4T(N/4)+2N$

    不断代入,直到最后得到:

    $T(N) = 2^k T(1) + kN = N + N log N = O(N log N)$

    这就是第二种分析方法。

    上面我们假设了$N=2^k$,如果不是这样的呢?通过刚刚的分析我们知道,只要$N = p2^k$,我们就可以把求解$T(N)$转化成求解$T(p)$,因此我们下面只分析奇数的情况。

    为了得到相同的结论,我们设$N = 2^k + m, 0 leq m < 2^k$,做数学归纳法:

    1°  首先,$k=0$的特殊情况,我们有$T(1) = 1$;$k = 1$时,我们有

    $T(2) = 2T(1) + 2 = 4 = N log N + N$、$T(3) = T(1)+T(2) + 3 = 8 = N log N + N$

    (当$log N$不是整数时我们向上取整,下面我们将看到最后一个约等号写成等号是不影响上界的,因为$2$是$2$的幂,它的$log N$不需要向上取整,但统一向上取整仍得到一个上界);

    2°  然后归纳假设对$0 leq k < n$,对任意的$m$,都成立$T(N) = N log N + N$,那么在$N = n = 2^n + m$时($m$仍然是任意的):

    若$m$是偶数,我们就有:$T(N) = 2T(2^{n-1} + m/2) + N$,根据归纳假设就有:

    $T(N) = 2[n(2^{n-1}+m/2) + 2^{n-1} + m/2] + N$

    $=(n + 1)(2^n + m) + N$

    $=(n + 1)N + N = N log N + N$

    若$m$是奇数,不妨设$m = 2q+1$,则有$T(N) = T(2^{n-1} + q) + T(2^{n-1}+q+1) + N$,根据归纳假设就有:

    $T(N) = n(2^{n-1}+q) + 2^{n-1} + q + n(2^{n-1} + q + 1) + 2^{n-1} + q + 1 +N$

    $=n(2^n + 2q + 1) + 2^n +2q + 1 + N$

    $=(n + 1)N + N = N log N + N$

    至此根据数学归纳法原理,得证$T(N) = N log N + N = O(N log N)$。

    以上就是解决分治算法的时间复杂度的思路。可以看到如果除了分治步骤,其余的步骤是$O(N)$的话,一般就会成为$O(N log N)$。如果不是,则要另行分析。

  • 相关阅读:
    iOS开发UI篇—Modal简单介绍
    iOS开发UI篇—APP主流UI框架结构
    A1081. Rational Sum
    A1049. Counting Ones
    A1008. Elevator
    A1104. Sum of Number Segments
    B1003. 我要通过!
    二分查找、two points、排序
    A1069. The Black Hole of Numbers
    A1101. Quick Sort
  • 原文地址:https://www.cnblogs.com/halifuda/p/14336067.html
Copyright © 2020-2023  润新知