【算法】递归

一、定义

递归是一种解决计算问题的方法,解决方案取决于同一类问题的更小子集。
特点:

  • 自己调用自己,如果说每个函数对应着一种解决方案,自己调用自己意味着解决方案是一样的(有规律的)
  • 每次调用,函数处理的数据会较上次缩减(子集),而且最后会缩减至无需继续递
  • 内层函数调用(子集处理) 完成,外层函数才能算调用完成

单链表递归遍历:

    private static void loop(SingleNode node) {
        if (node == null) {
            return;
        }
        System.out.println("before:" + node.value);
        loop(node.next);
        System.out.println("after:" + node.value);
    }

    public static void main(String[] args) {
        SingleLinkedList.addLast(1);
        SingleLinkedList.addLast(2);
        SingleLinkedList.addLast(3);
        loop(findNode(0));
    }
before:1
before:2
before:3
after:3
after:2
after:1

以上递归代码写成伪代码如下:

    private static void loop(node == 1) {
        System.out.println("before:" + node.value);  // 1
        private static void loop(node == 2) {
            System.out.println("before:" + node.value);  // 2
            private static void loop(node == 3) {
                System.out.println("before:" + node.value);  // 3
                private static void loop(SingleNode node) {
                    if (node == null) {
                        return;
                    }
                }
                System.out.println("after:" + node.value);  // 3
            }
            System.out.println("after:" + node.value);  // 2
        }
        System.out.println("after:" + node.value);  // 1
    }

二、解题思路

  • 确定能否使用递归求解
  • 推导出递推关系,即父问题与子问题的关系,以及递归的结束条件

如遍历链表的递推关系:
f ( n ) = { 停止 ,    n = n u l l f ( n . n e x t ) ,    x ≠ n u l l f(n) = \begin{cases} 停止,\,\,n=null\\ f(n.next),\,\,x\ne null\\ \end{cases} f(n)={停止,n=nullf(n.next),x=null

  • 深入到最里层叫递
  • 从最里层出来叫归
  • 在递的过程中,外层函数内的局部变量(以及方法参数)并未消失,归的时候还可以用到。

三、求阶乘

用递归方法求阶乘,

  • 阶乘的定义:n!=1·2·3···(n-2)·(n-1)·n,其中n为自然数,0!=1。
  • 递推关系:
    f ( n ) = { 1 ,    n = 1 n ∗ f ( n − 1 ) ,    n > 1 f(n) = \begin{cases} 1,\,\,n=1\\ n*f(n-1),\,\,n>1\\ \end{cases} f(n)={1,n=1nf(n1),n>1
public class Factorial {

    public static int f(int n) {
        if (n == 1) {
            return 1;
        }
        return n * f(n - 1);
    }

    public static void main(String[] args) {
        int factorial = f(3);
        System.out.println(factorial);
    }
}

伪代码如下:

f(int n=3){
  return 3*f(int n=2){
    return 2*f(int n=1){
      if(n == 1){
        return 1;
      }
    }
 }

四、反向打印字符串

用递归反向打印字符串,n为字符在整个字符串 str 中的索引位置

  • 递:n从0开始,每次n+1,一直递到n==str.length()-1
  • 归:从n== str.length()开始归,从归打印,自然是逆序的

递推关系
f ( n ) = { 停止 ,    n = s t r . l e n g t h f ( n + 1 ) ,    0 ≤ n ≤ s t r . l e n g t h − 1 f(n) = \begin{cases} 停止,\,\,n=str.length\\ f(n+1),\,\,0 \leq n \leq str.length-1\\ \end{cases} f(n)={停止,n=str.lengthf(n+1),0nstr.length1

    public static void reversePrint(String str,int index) {
        if (index == str.length()) {
            return;
        }
        reversePrint(str, index + 1);
        System.out.println(str.charAt(index));
    }
    public static void main(String[] args) {
        reversePrint("abcd",0);
    }

伪代码:

    reversePrint(String str,0) {
        reversePrint(str, 1) {
            reversePrint(str, 2) {
                reversePrint(str, 3) {
                    reversePrint(str, 4) {
                        if (3 == str.length()) {
                            return;
                        }
                    }
                    System.out.println(str.charAt(3));
                }
                System.out.println(str.charAt(2));
            }
            System.out.println(str.charAt(1));
        }
        System.out.println(str.charAt(0));
    }

五、使用递归实现冒泡排序

1.冒泡排序基本思想:

通过对待排序序列从前向后(从下标较小的元素开始),依次对相邻两个元素的值进行两两比较,若发现逆序则交换,使值较大的元素逐渐从前移向后移,就如果水底下的气泡一样逐渐向上冒。

2.步骤

待排序数组:[8,5,1,2,0]

第一轮排序(索引0~4中):[8,5,1,2,0]

  • 索引0和索引1比较,前者大,交换:[5,8,1,2,0]
  • 索引1和索引2比较,前者大,交换:[5,1,8,2,0]
  • 索引2和索引3比较,前者大,交换:[5,1,2,8,0]
  • 索引3和索引4比较,前者大,交换:[5,1,2,0,8]
    这一轮结束 ,最大的元素已经排在了最后

第二轮排序(索引0~3中):[5,1,2,0,8]

  • 索引0和索引1比较,前者大,交换:[1,5,2,0,8]
  • 索引1和索引2比较,前者大,交换:[1,2,5,0,8]
  • 索引2和索引3比较,前者大,交换:[1,2,0,5,8]
    这一轮结束 ,倒数第二大的的元素已经排在倒数第二个位置

第三轮排序(索引0~2中):[1,2,0,5,8]

  • 索引0和索引1比较,后者大,不交换:[1,2,0,5,8]
  • 索引1和索引2比较,前者大,交换:[1,0,2,5,8]
    这一轮结束 ,倒数第三大的的元素已经排在倒数第三个位置

第四轮排序(索引0~1中):[1,0,2,5,8]

  • 索引0和索引1比较,前者大,交换:[0,1,2,5,8]
    这一轮结束 ,倒数第四大的的元素已经排在倒数第四个位置
    一共5个元素,排序结束。

第一层for循环控制总轮次,第二层for循环控制交换的次数,因为每一轮都有一个大的排好了序在后面,所以内存循环的次数是随着每一轮排好的元素逐渐递减的。

迭代实现:

    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length - 1-i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }
    }
  
    public static void swap(int[] arr,int a ,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

递归实现:

    /**
     * 递归冒泡
     * @param arr
     * @param right
     */
    public static void bubbleRecursion(int[] arr,int right) {
        if (right == 0) {
            return;
        }
        for (int i = 0; i < right; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr, i, i + 1);
            }
        }
        bubbleRecursion(arr, right - 1);
    }

递归优化:在冒泡的过程中,如果两个相邻元素发生了交换,意味着有无序的情况,如果没发生交换,意味着有序;所以最后一次发生交换的时候,意味着后续的都是有序的了。所以最后一次交换,i索引的位置就是有序和无序的分界点,i的右侧一定都是有序的了,i的左侧可能还是无序的。使用一个标志位记录下这个位置,下一趟递归,直接从这个标志位开始,就可以减少无效的递归次数了(上面的写法是每次都减少1),现在缩减成直接从未排序的标志点开始递归(缩减的可能比1要多,所以可以减少递归的次数)。

    /**
     * 递归冒泡优化
     * @param arr
     * @param right
     */
    public static void bubbleRecursion1(int[] arr,int right) {
        if (right == 0) {
            return;
        }
        int flag = 0;
        for (int i = 0; i < right; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr, i, i + 1);
                flag = i;
            }
        }
        bubbleRecursion1(arr, flag);
    }

六、使用递归实现插入排序

1.插入排序基本思想

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

2.步骤

将第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)

待排序数组:[8,5,1,2,0]

  • 取出无序部分[1~4]的首个元素5,在有序部分从后向前比较,插入到合适的位置:[5,8,1,2,0]
  • 取出无序部分[2~4]的首个元素1,在有序部分从后向前比较,插入到合适的位置:[1,5,8,2,0]
  • 取出无序部分[3~4]的首个元素2,在有序部分从后向前比较,插入到合适的位置:[1,2,5,8,0]
  • 取出无序部分[3~4]的首个元素8,在有序部分从后向前比较,插入到合适的位置:[1,2,5,8,0]
  • 取出无序部分[4]的首个元素0,在有序部分从后向前比较,插入到合适的位置:[0,1,2,5,8]

迭代实现:

    public static void insert(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            //要插入的元素
            int current = arr[i];
            //要比较的索引,有序部分从后向前比较
            int j = i - 1;
            //寻找比当前元素小的元素的位置
            while (j >= 0 && arr[j] > current) {
                //依次空出一个位置
                arr[j + 1] = arr[j];
                j--;
            }
            //要插入的位置
            arr[j + 1] = current;
        }
    }

递归实现:

    /**
     * 递归插入排序
     * @param arr
     * @param insertEleIndex 要开始准备插入的元素的索引,未排序区域的左边界
     */
    public static void insertRecursion(int[] arr,int insertEleIndex) {
        if (insertEleIndex == arr.length) {
            return;
        }
        //要插入的元素
        int current = arr[insertEleIndex];
        //要比较的索引
        int j = insertEleIndex - 1;
        //寻找比当前元素小的元素的位置
        while (j >= 0 && arr[j] > current) {
            //依次空出一个位置
            arr[j + 1] = arr[j];
            j--;
        }
        //要插入的位置
        arr[j + 1] = current;
        insertRecursion(arr, insertEleIndex + 1);
    }

七、Leetcode509.斐波那契数

  • 每个递归函数只包含一个自身调用,称为单路递归 single recursion
  • 每个递归包含多个自身调用,称为多路递归 multi recursion

递推关系:
f ( n ) = { 0 ,    n = 0 1 ,    n = 1 f ( n − 1 ) + f ( n − 2 ) ,    n > 1 f(n) = \begin{cases} 0,\,\,n=0\\ 1,\,\,n=1\\ f(n-1)+f(n-2),\,\,n>1\\ \end{cases} f(n)= 0,n=01,n=1f(n1)+f(n2),n>1
数列的前几项:

F0F1F2F3F4F5F6F7F8F9F10F11F12
01123581321345589144

迭代方式:

    public static int fib(int n) {
        if (n <= 1) {
            return n;
        }
        int[] arr = new int[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        for (int i = 2; i <= n; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }

递归方式:

    public static int f(int n) {
        if (n <= 1) {
            return n;
        }
        return f(n - 1) + f(n - 2);
    }

递归过程:
在这里插入图片描述

  • 绿色代表正在执行(对应递),灰色代表执行结束(对应归)
  • 递不到头,不能归,对应深度优先搜索

八、兔子问题

古典问题:3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,求第n个月的兔子数

设第 n 个月兔子数为 f ( n ) f(n) f(n)

  • f ( n ) f(n) f(n) = 上个月兔子数 + 新生的小兔子数
  • 而【新生的小兔子数】实际就是【上个月成熟的兔子数】
  • 因为需要一个月兔子就成熟,所以【上个月成熟的兔子数】也就是【上上个月的兔子数】
  • 上个月兔子数,即 f ( n − 1 ) f(n-1) f(n1)
  • 上上个月的兔子数,即 f ( n − 2 ) f(n-2) f(n2)

因此本质还是斐波那契数列

代码实现:

    public static int sumRabbit(int month) {
        if (month <= 2) {
            return 1;
        }
        return sumRabbit(month - 1) + sumRabbit(month - 1);
    }

九、Leetcode70.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

分析

n跳法规律
1(1)暂时看不出
2(1,1) (2)暂时看不出
3(1,1,1) (1,2) (2,1)暂时看不出
4(1,1,1,1) (1,2,1) (2,1,1)
(1,1,2) (2,2)
最后一跳,跳一个台阶的,基于f(3)
最后一跳,跳两个台阶的,基于f(2)
5

因此本质上还是斐波那契数列,只是从其第二项开始

    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        return climbStairs(n - 2) + climbStairs(n - 1);
    }

此解法在力扣上超出时间限制

使用迭代:

    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        int[] arr = new int[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        arr[2] = 2;
        for (int i = 3; i <= n; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }

十、使用记忆法(备忘录)优化递归

在之前的递归过程中,存在很多重复的计算,为了减少这些重复的计算,可以把已经计算好的结果暂存起来,用到的时候直接取。

    public static int fibonacciImprove(int n) {
        int[] cache = new int[n + 1];
        for (int i = 0; i < n + 1; i++) {
            cache[i] = -1;
        }
        cache[0] = 0;
        cache[1] = 1;
        return fibonacciRecursion(n, cache);
    }

    public static int fibonacciRecursion(int n, int[] cache) {
        //用数组缓存已经计算好的结果
        if (cache[n] != -1) {
            return cache[n];
        }
        int lastOne = fibonacciRecursion(n - 1, cache);
        int lastTwo = fibonacciRecursion(n - 2, cache);
        cache[n] = lastOne + lastTwo;
        return cache[n];
    }

十一、爆栈问题

1.问题现象
    public static void main(String[] args) {
        long sum = sum(15000);
        System.out.println(sum);
    }
    
    public static long sum(long n) {
        if (n == 1) {
            return n;
        }
        return sum(n - 1) + n;
    }
Exception in thread "main" java.lang.StackOverflowError
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)
	at com.hcx.algorithm.recursion.RecursionSum.sum(RecursionSum.java:24)

递是压栈,归是弹栈:
在递的过程中,必须递到最里层,即n=1的时候,才开始归,即开始弹栈;每个方法的调用都要存储方法相关的信息,如参数,返回地址等,当调用的过程很深的时候,占用的内存逐渐增加,并且都没有释放,因为还没有开始归,只有拿到了返回值,对应的方法内存才释放,所以会导致栈内存占用太高,栈内存耗尽,抛出栈溢出错误。

2.尾递归与尾调用

定义
如果函数的最后一步是调用一个函数,那么称为尾调用

function(){
  return a()
}

反例:以下不是尾调用

function(){
  int result = a();
  return result; // 不是调用函数
}
function(){
  return a()+1; // 虽然调用了,但是又用到了外层函数的数值1,最后一步实际是加法操作,不是函数调用
}
function(){
  return a()+b; // 虽然调用了,但是又用到了外层函数的变量b,最后一步是加法操作
}

编译器的优化
一些语言的编译器能对尾调用做优化:

a(){
  return b();
}

b(){
  return c();
}

c(){
  return 100;
}

a();

没优化:最内层的c没有返回之前,a和b都不能结束。

a(){
  b(){
    c(){
      return 100;
    }
  }
}

优化后:平级调用,a调用完,不需要等待b返回,直接释放内存。

a()b();
c();

因为尾调用的操作,自己要做的事情已经全部执行完了,只需要等待调用方的结果就可以了,所以可以把自己的内存释放掉了。上面的例子中,return a()+1;这种情况,当拿到了a的返回值,函数本身还要拿着结果去执行+1的操作,所以不能提前结束。

尾递归:最后调用的函数是自己。

支持尾递归和尾调用优化的语言有scala和C++

3.使用尾递归避免爆栈问题
import scala.annotation.tailrec

object Main {
  def main(args: Array[String]): Unit = {
    println(sum(10000000, 0)) 
  }

  @tailrec
  def sum(n: Long, accumulator: Long): Long = {
    if (n == 1) {
      return 1 + accumulator
    }
    return sum(n - 1, n + accumulator)
  }

调用过程:

  sum(n=3, accumulator=0): Long = {
    return sum(2, 3)
  }
  sum(n=2, accumulator=3): Long = {
    return sum(1, 5)
  }
  sum(n=1, accumulator=5): Long = {
    if (n == 1) {
      return 1 + 5
    }
  }
4.使用迭代避免爆栈问题
    public static long sum(long n){
        long sum = 0;
        for (long i = 0; i <= n; i++) {
            sum = sum + i;
        }
        return sum;
    }

理论上所有的递归都能使用迭代写出来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值