【算法分类概述二】--【递归与分治策略】

递归】

(1)定义:递归是指函数调用自身或调用其他函数序列时的情况名称,其中一个函数最终再次调用第一个函数。它在编程语言中用于定义语言语法,在数据结构中用于为列表和树结构开发搜索和排序算法。 

(2)递归过程与递归工作栈        

         递归过程在实现时,需要自己调用自己。

         第一次递归调用时,需要为过程中使用的参数,局部变量等另外分配存储空间。

         层层向下递归,退出时的次序正好相反:

                    递归次序-------------------------------->

                    n!    -->  (n-1)!  -->     (n-2)!  -->     1! -->    0!=1

                    <----------------------------------   返回次序

         因此,每层递归调用需分配的空间形成的递归工作记录按后进先出的栈组织       

(3)函数递归时的活动记录

                    

         如上图:

         每一层递归调用所需保存的信息构成一个工作记录,通常包括如下内容:

         ①返回地址:即上一层中调用本身语句的后继语句

         ②在本次过程调用时,与形参结合的实参值,包括函数名、引用参数与传值参数等

         ③本层的局部变量值

(4)递归小结

         解决方法:在递归算法中消除递归调用,使其转化为非递归算法。
        1.采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。
        2.用递推来实现递归函数,即用数组来保存结果。
        3.通过将一些递归转化为尾递归,从而迭代求出结果。
        后两种方法在时空复杂度上均有较大改善,但其适用范围有限。

(5)实例

例一:Fibonacci数列:

long Fibonacci ( long n)
{
     if(n==0||n == 1 )
          return 1;
     else
          return Fibonacci ( n-1 ) + Fibonacci ( n-2 );
}

例二:阶乘:     

long Factorial(long n)
{
     int temp;
     if(n==0)
        return 1;
     else
        temp=n*Factorial(n-1);
     return temp;
}

总结一下,其实递归挺简单的,就是自己调用自己,记得设置终止条件,比如说当n==0时就直接返回的时一个常数,其他时候就进行递归,所以一般用的是if-else语句来写。但总的来说,递归虽然逻辑简单,写起来也特别容易,但它实现起来很费时间和空间,它要一直调用自身并且一直为新的函数中的变量申请空间,费时费力,一般来说在大型工程里如果我们要追求代码的高效的话我们是不会使用递归的,而是用非递归方法来代替递归实现同样的功能。举个最简单的例子,我在main函数里写如下的代码:

array[0]=1;
array[1]=1;
for(int i=2;i<1000;i++)
    array[i]=array[i-1]+array[i-2];

在刚开始我们就直接把它算出来存到数组里面,以后要用直接用就可以了,而且根据数组之间的先后关系算的是非递归方法,算起来比递归快了好多倍,递归的方法复杂度是O(2^n),数组是O(n),不信的话大家可以去试一下,当你的n到30的时候二者的差距就已经很大了。

所以,只有一些小问题时,我们大多数倾向于用递归解决,因为它简单,但在大工程里面,我们会将其作为一个思想和解决办法应用,最后找到方法将其代替,以提升整个工程的效率。        

接下来再说一些关于递归的经典例子:

例三:排列问题        

设计一个递归算法生成n个元素 {r1,r2,r3,……,rn}的全排列。

解法:设R={r1,r2,…,rn}是要进行排列的n个元素,Ri=R-{ri}。 集合X中元素的全排列记为perm(X)。 (ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。R的全排列可归纳定义如下:

           当n=1时,perm(R)=(r),其中r是集合R中唯一的元素;

           当n>1时,perm(R)由(r1)perm(R1),(r2)perm(R2),…,(rn)perm(Rn)构成。   

void perm(int array[],int k,int m)
{//产生list[k,m]的所有排列
    if(k==m)//如果只剩最后一个元素没排
    {//排完直接从最一个顺序输出,此时序列已经顺序存在数组里
        for(int i=0;i<=m;i++)
            printf("%d",array[i]);
        printf("\n");
    }
    else//还有多个元素,递归产生排列
    {
        for(int i=k;i<=m;i++)
        {//每个元素都当一次头,剩下的产生排列
            swap(array,k,i);//第一位的与他后面的都交换,分别当一次头
            perm(array,k+1,m);
            swap(array,k,i);//换完记得要换回来,因为第一次换的要回归原位,下次的要与最初的开始位换
        }
    }
}

        例四:整数划分问题

        将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1, 正整数n的这种表示称为正整数n的划         分。 

       求正整数n的不 同划分个数。

       例如正整数6有如下11种不同的划分:    

               6;    

               5+1;    

               4+2,4+1+1;    

               3+3,3+2+1,3+1+1+1;    

               2+2+2,2+2+1+1,2+1+1+1+1;    

               1+1+1+1+1+1。

      解法:在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:将最大加数n1不大于m                  的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。

                其中正整数n的划分数p(n)=q(n,n)。

int q(int n,int m)
{
   if((n<1)||(m<1)) 
      return 0;
   if((n==1)||(m==1)) 
      return 1;
   if(n<m) 
      return q(n,n);
   if(n==m) 
      return q(n,m-1)+1;
   return q(n,m-1)+q(n-m,m);
}

        例五:Hanoi塔问题

        设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号         为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:

               规则1:每次只能移动1个圆盘;

               规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;

               规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。

 解法:不再赘述,用经典的递归思想,很容易解决,看图:

代码段如下:

int Hanoi(int disks)
{
    if(disks<=1)
        return 1;
    else
        return 1+2*Hanoi(disks-1);
}

上面这个代码只是算出来结果,要看具体过程可以用下面这个:

void move(int count, int start, int finish, int temp)
{
    if (count > 0) {
      move(count - 1, start, temp, finish);
      printf("Move disk %d from %d to %d\n",count,start,finish);
      move(count - 1, temp, finish, start);
  }
}

l

例题完结,下面进行对递归的小结吧:

(1)优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。 

(2)缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。


分治思想】     

   (1)分治法的设计思想是,将一个直接难以解决的大问题,分割成一些规模较小的相同问题,以便各个击破。

   (2)由分治法产生的子问题往往是原问题的较小规模,这就为递归技术提供了方便,在这种情况下,反复利用分治手段,可以使子问题与原问题类型一致但规模却不断缩小,最终缩小到很容易直接求出其解,这自然导致了递归过程的产生。

     注:其实在某种程度上,递归和分治是同一个东西,递归是实现方法,分治是思想。递归的本质就是将一个大问题一步一步求,最终求解,利用的正是分治思想。而分治这种方法的实现形式一般就是递归,也只能通过递归来实现,因为分治法使得大问题分割成的小问题都是一种类型的问题,而且每个小问题的结果都是要用到的,这也必然导致了分治思想的实现方法是递归。所以说:递归是分治的实现方法,分治是递归的本质思想。

    (3)分治法的适用条件:主要有这么几个,一是问题缩小到一定规模就能容易解决;二是可以分解成若干个规模较小的相同问题,这样这些问题都能用同一种方法解决,称为最优子结构性质;三是每个子问题都是相互独立的,这条特征涉及到分治法的效率,如果子问题不是相互独立的,则分治法就会做很多不必要的工作重复解决公共的子问题,此时虽可以用分治法,但用动态规划更好一些。动态规划和分治法都具有最优子结构性质,但动态规划分成的每步都会用到更小步骤的结果,所以动态规划会把它们存下来方便使用,但分治法不会。能用动态规划解决的问题一般都能用分治法解决,不过会麻烦一点,二者都能使用的情况下动态规划的效率是很明显要高于分治法的。  

      (4)说一下动态规划和分治法的差异。

       动态规划的基本思想是将问题分解成若干个子问题,但这些子问题往往不是相互独立的,最底一层的先算,最高的后算,高层一般会用到底层算到的结果,所以没有像分治法说的问题规模差不多之类的说法。正因为要用子问题的答案,所以动态规划能够保存子问题的答案,避免多次重复计算,且这种保存形式决定了动态规划算法实现的时候一般用数组来表示逻辑。而且从本质上来说,分治法是将问题分成若干个规模差不多的子问题,这些子问题都是平行而独立的,而且都是同一种方法算出来的,子问题的结果加出来就是问题的答案。举个例子,我要布置一个会场,我把它分割成几个小问题,一个人去搬凳子,一个人去扫地,一个人去擦桌子,一个人去贴横幅,这样就很快把一个场地布置好了,而且这四件事情都是同等级的没有依赖利用关系的。        

        分治法就是单纯的将大问题划分成小问题即最优子结构性质,然后递归解决。所以分治法一般使用递归解决,动态规划主要使用几层for循环来解决。          

       经典例题:

               例一:二分搜索技术           

                这个想必大家都很熟悉了,就是一组有序的数,从中找出指定的数在哪里。很容易可以想到它是将问题拆分成若干个规模相等的子问题进行解决,所以用分治法。给大家两中代码,递归和迭代的,思路简单不再赘述。  

int orderedList<Type> ::       //折半搜索算法
BinarySearch ( const Type & x, const int low, 
         const int high ) const {
//折半搜索的递归算法
      int mid = -1;
    if ( low <= high ) {
      mid = ( low + high ) / 2;
      if ( Element[mid].key < x )
         mid = BinarySearch ( x, mid +1, high );
      else if ( Element[mid].key > x )	
	       mid = BinarySearch ( x, low, mid -1 ); 
    }
    return mid;
}


template<class Type> int orderedList <Type>::   
BinarySearch ( const Type & x ) const {	      
//折半搜索的迭代算法 
    int high = CurrentSize-1,  low = 0,  mid;	      
    while ( low <= high ) {		 
        mid = ( low + high ) / 2;		 
        if ( Element[mid].key < x ) 
           low = mid + 1;		 //右缩搜索区间	 
        else if ( Element[mid].key ) > x ) 
                   high = mid - 1;  //左缩搜索区间
               else return mid;    //搜索成功
    }	  
    return -1;                        //搜索失败
}

          算法性能分析:每执行一次算法的while循环, 待搜索数组的大小减少一半。因此,在最坏情况下,while循环被执行了O(logn) 次。循环体内运算需要O(1) 时间,因此整个算法在最坏情况下的计算时间复杂性为O(logn) 。

          例二:归并排序          

           迭代的归并排序算法是利用两路归并过程进行排序,其基本思想是:设初始元素序列有 n 个元素,首先把它看成是 n 个长            度为 1 的有序子序列(归并项),做两两归并,得到n/2个长度为 2 的归并项(最后一个归并项的长度为1);再做                      两两归并,得到n/4个长度为 4 的归并项(最后一个归并项长度可以短些)…,如此重复,最后得到一个长度为 n 的                      有序序列。由此我们得到主算法为:

void MergeSort(int a[],int left,int right)
{
    if(left<right)//至少两个元素,若只剩一个就庭
    {
        int i=(left+right)/2;
        MergeSort(a,left,i);
        MergeSort(a,i+1,right);
        merge(a,b,left,i,right);//合并到数组b
    }
}

             接下来我们需要一个合并算法,将两组排好的数据合并排到一个数组里面:

void merge(int a[],int b[],int left,int mid,int right)
{
    int i=left,j=mid+1,k=left;
    while(i<=mid&&j<=right)
    {
        if (a[i]<=a[j])
            b[k++] = a[i++];
        else
            b[k++] = a[j++];
    }
    while(i<=mid) //若,没检查完,复制
        b[k++] = a[i++];
    while(j <= right)
        b[k++] = a[j++];
}

            例三:快速排序

            解法:一趟快速排序的具体做法是:附设两个指针low和high,它们的初值分别是一个序列的第一个和最后一个记录的位               置,设枢轴记录的关键字为pivotKey。     首先从high所指位置起向前搜索直到第一个关键字小于pivotKey的记录和枢轴记录交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotKey的记录和枢轴记录互相交换,重复交替这两步               直到low=high为止。

void quicksort(int a[],int low,int high)
{
    int temp=a[low];
    int i=low,j=high;
    if(i>=j)//注意点一:一定要有等于,要是只剩一个数不用排
        return ;//而且这里一般只有两种情况,一是上次排完low=i,于是现在i=j+1,而是上次排完传进来两个数,那么现在只剩一个数,所以不用排
    while(i<j)//结束条件也可以写成i!=j,因为结束时一定是i=j
    {
        while(a[j]>=temp&&i<j)//条件一定是两个同时成立
            j--;
        if(i<j)//其实这个可以不写,因为上边已经保证了i<=j成立,等于时交换也是可以的
            a[i++]=a[j];//找到一个比temp值小的交换到左边i处
        while(a[i]<=temp&&i<j)
            i++;
        if(i<j)
            a[j--]=a[i];//找打一个大的交换到右边
    }
    a[i]=temp;//把初始值放到中间
    quicksort(a,low,i-1);//这里需要注意了!一定是把中间值扔掉排两边
    quicksort(a,i+1,high);//否则要是left=i,则陷入了死循环出不去
}

补充:平均时间为Tavg(n) = knlnn。其中k为常数。经验表明,在所有同数量级的排序方法中,快速排序是常数因子最小                  的。即其平均性能是最好的,但是,若初始记录序列按关键字有序或基本有序时,快速排序将蜕化为冒泡排序。改进            的方法是用“三者取中”的方法来选取枢轴记录。经验表明该方法可以有效地改善在最坏情况下的性能。但即使如此也            不能使快速排序在待排记录有序的情况下达到O(n)的时间复杂度。

例五:棋盘覆盖

在一个2k×2k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

解法:当k>0时,将2^k×2^k棋盘分割为4个2^k-1×2^k-1 子棋盘,特殊方格必定位于四个小棋盘其中之一,其余三个棋盘无特殊方格,用一个L型骨牌置于三棋盘的交界处,这样三个普通棋盘也变成了特殊方格的棋盘,从而将问题转化为四个小棋盘的棋盘覆盖问题。递归的使用这种分割,直至棋盘简化为棋盘1*1.

void chessBoard(int tr,int tc,int dr,int dc,int size)
{
    if (size==1) 
        return;
    int t=tile++,  // L型骨牌号
        s=size/2;  // 分割棋盘
    // 覆盖左上角子棋盘
    if(dr<tr+s&&dc<tc + s)
         // 特殊方格在此棋盘中
         chessBoard(tr, tc, dr, dc, s);
      else {// 此棋盘中无特殊方格
         // 用 t 号L型骨牌覆盖右下角
         board[tr + s - 1][tc + s - 1] = t;
         // 覆盖其余方格
         chessBoard(tr, tc, tr+s-1, tc+s-1, s);}
      // 覆盖右上角子棋盘
      if (dr < tr + s && dc >= tc + s)
         // 特殊方格在此棋盘中
         chessBoard(tr, tc+s, dr, dc, s);
      else {// 此棋盘中无特殊方格
         // 用 t 号L型骨牌覆盖左下角
         board[tr + s - 1][tc + s] = t;
         // 覆盖其余方格
         chessBoard(tr, tc+s, tr+s-1, tc+s, s);
         }
        // 覆盖左下角子棋盘
      if (dr >= tr + s && dc < tc + s)
         // 特殊方格在此棋盘中
         chessBoard(tr+s, tc, dr, dc, s);
      else {// 用 t 号L型骨牌覆盖右上角
         board[tr + s][tc + s - 1] = t;
         // 覆盖其余方格
         chessBoard(tr+s, tc, tr+s, tc+s-1, s);}
      // 覆盖右下角子棋盘
      if (dr >= tr + s && dc >= tc + s)
         // 特殊方格在此棋盘中
         chessBoard(tr+s, tc+s, dr, dc, s);
      else {// 用 t 号L型骨牌覆盖左上角
         board[tr + s][tc + s] = t;
         // 覆盖其余方格
         chessBoard(tr+s, tc+s, tr+s, tc+s, s);}
   }

【本章总结】:

            总的来说, 递归与分治还是有一点差别。递归求解一个大问题,求解过程中不断调用自身,求解一个较为简单的问        题,直到最后求解一个最小的问题,但差别就在于:比如递归求解的问题为5,那么他接下来求解的是4,求4的时候又通      过求解3来求解4,求解3的时候又求解2,直到1,是属于高低关系的一层一层。但分治法是我要求解16,我就求解8,然      后求解4,最后直到求解1得出答案。从例题中就可以看出来,递归的例题都是排列,汉诺塔,整数划分问题,求解q[n],      求解q[n-1],求解q[n-2]……求解q[1],q[0],最终求解出结果。分治法的例题,归并排序,二分搜索,都是中间划分,然后      再中间划分,直到最后。但这并不妨碍分治法的所有实现方法都是递归,递归是一种实现方法,分治是一种解题思想。   

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值