递归与分治策略-第一节:递归和典型递归问题

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了递归与分治策略-第一节:递归和典型递归问题相关的知识,希望对你有一定的参考价值。

文章目录

一:LeetCode中有关递归和分治的题目

递归题目链接

分治题目链接

二:递归与分治概述

递归与分治:任何可以用计算机求解的问题所需要的计算时间都和其规模有关,如果问题的规模越小,解题所需的计算时间往往也越短,从而也比较容易处理,例如

  • 对于 n n n个元素的排序问题:当 n = 1 n=1 n=1时不需要任何计算;当 n = 2 n=2 n=2时,只需要做一次比较即可排好序;当 n = 3 n=3 n=3时则两次。而当 n n n较大时,这个问题就不好处理了

所以要想直接解决一个较大的问题,有时是相当有困难的。所以分治法的设计思想是:将一个难以直接解决的大问题分割成一些规模较小的相同问题,以便各个击破,也即分而治之。如果原问题可以分割为 k k k个子问题, 1 < k ≤ n 1<k\\leq n 1<kn,且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题于原问题类型一致而其规模不断缩小,最终使子问题缩小到容易求出其解,由此自然引出递归算法。分治与递归像一对孪生兄弟,经常同时应用在算法设计中,并由此产生许多高效算法

三:递归基本概念

递归:递归算法是指直接或间接调用自身的算法;递归函数是指用函数自身给出定义的函数。在计算机算法设计与分析中,递归技术非常有用。使用递归技术往往可以使函数的定义和算法的描述简洁且易于理解,而且有些数据结构(例如二叉树)由于其本身具有递归特性,所以也特别使用递归的形式来求解

构造递归算法的基本步骤为

  • 确定递归边界
  • 描述递归关系
  • 构造递归函数

四:典型递归问题分析

(1)阶乘

阶乘 n ! = n × ( n − 1 ) × . . . × 1 n!=n×(n-1)×...×1 n!=n×(n1)×...×1,可递归定义如下

  • 第一个式子是递归边界,如果没有边界就有可能会引发无穷递归
  • 第二个式子是用较小自变量的函数来表示较大自变量的函数值,相当于问题规模减小

这个问题很简单,递归定义式很容易给出,所以编写代码如下

public class Factorial 

    public static int factorial_recurse(int n )
        if(n == 1)return 1;
        return n * factorial_recurse(n-1);
    

    public static void main(String[] args) 
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextInt())
            int n = scanner.nextInt();
            System.out.println(n+"!等于:" + factorial_recurse(n));
        
    

当然,递归解法也有其对应的非递归解法

public class Factorial 
    public static int factorial_none_recurse(int n)
        int ret = 1;
        for(int i = 1; i <= n; i++)
            ret *= i;
        
        return ret;
    

    public static void main(String[] args) 
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextInt())
            int n = scanner.nextInt();
            System.out.println(n+"!等于:" + factorial_none_recurse(n));
        
    

(2)Fibonacci数列

Fibonacci数列 :无穷数列【1 1 2 3 5 8 13 21 34 55…】称为Fibonacci数列,在Fibonacci数列中从第三个数字开始,每一个数字都是前两个数字之和,这是一个典型的递归问题,其递归定义式如下

很明显这个问题需要两个递归边界

问题还是比较简单,所以编写代码如下

public class Fibonacci 
    public static int fibonacci_recurse(int n)
        if(n <= 1)return 1;
        return fibonacci_recurse(n-1) + fibonacci_recurse(n-2);
    

    public static void main(String[] args) 
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextInt())
            int n = scanner.nextInt();
            System.out.println("第" + n+"个fibonacci数为:" + fibonacci_recurse(n));
        
    

fibonacci数列的纯递归写法有很多的重复子问题,所以其时间复杂度极高。所以对于其递归写法,我们可以进行一定优化,引入一个“备忘录”去解决这一部分重复子问题

public class Fibonacci 
    public static int fibonacci_recurse(int n)
        if(n <= 1)return 1;
        return fibonacci_recurse(n-1) + fibonacci_recurse(n-2);
    

    public static int fibonacci_recurse_optimize(int[] memo, int n)
        if(n <= 1)return 1;
        if(memo[n] != 0)return memo[n];//如果备忘录有这个元素那就不用递归了
        memo[n] = fibonacci_recurse(n-1) + fibonacci_recurse(n-2);//保存下来
        return memo[n];
    

    public static void main(String[] args) 
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextInt())
            int n = scanner.nextInt();
            int[] memo = new int[n+1];//备忘录,元素如果为0表示没有被记录
            long recurse_start_time = System.nanoTime();
            System.out.println("(递归)第" + n+"个fibonacci数为:" + fibonacci_recurse_optimize(memo, n));
            long recurse_end_time = System.nanoTime();
            long none_recurse_start_time = System.nanoTime();
            System.out.println("(非递归)第" + n+"个fibonacci数为:" + fibonacci_recurse_optimize(memo, n));
            long none_recurse_end_time = System.nanoTime();
            System.out.println("递归用时:" + (recurse_end_time - recurse_start_time));
            System.out.println("非递归用时:" + (none_recurse_end_time - none_recurse_start_time));
            System.out.println("----------------------------------------------------");
        
        scanner.close();
    

(3)排列问题

排列问题:设计一个递归算法生成 n n n个元素的全排列

采用递归解法,可以将规模为 n n n的全排列问题转换为规模为 n − 1 n-1 n1的全排列问题。所以他可以看作是固定 [ 0 , k ] [0, k] [0,k]位,对 [ k + 1 , n ] [k+1, n] [k+1,n]位进行全排列,当 k + 1 = n k+1=n k+1=n时,递归结束

如下为决策树,每一个子决策树都是一个全排列问题,


代码如下

public class Permutations

    public static void swap(int[] test, int k, int i)
        int temp = test[k];
        test[k] = test[i];
        test[i] = temp;

    

    public static void perm(int[] list, int k, int m)
        //只有一个元素,递归结束,这一种排列情况可以输出了
        if(k == m)
            for(int i = 0; i <= m; i++)
                System.out.print(list[i]);
            
            System.out.println();
        
        //否则开始递归
        else
            for(int i = k; i <= m; i++)
                swap(list, k, i);
                perm(list, k+1, m);
                swap(list, k, i);
            
        
    

    public static void main(String[] args) 
        int[] test = new int[]2, 3, 5, 7;
        perm(test, 0, 3);
    

(4)整数划分

整数划分:将正整数 n n n表示成一系列整数之和,即 n = n 1 + n 2 + . . . + n k n=n_1+n_2+...+n_k n=n1+n2+...+nk

在这里,正整数 n n n的这种表示称为正整数 n n n的划分。正整数 n n n的不同划分个数称为正整数 n n n的划分数,记为 p ( n ) p(n) p(n),例如6有如下11种不同的划分方法,所以 p ( 6 ) = 11 p(6)=11 p(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

在正整数 n n n的所有划分中,用 q ( n , m ) q(n,m) q(n,m)表示最大加数 n 1 n_1 n1不大于 m m m的划分个数,正整数 n n n的划分数 p ( n ) = q ( n , n ) p(n)=q(n,n) p(n)=q(n,n)