定义
计算机科学中,递归是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集
In computer science, recursion is a method of solving a computational problem where the solution depends on solutions to smaller instances of the same problem.
比如单链表递归遍历的例子:
void f(Node node) {
    if(node == null) {
        return;
    }
    println("before:" + node.value)
    f(node.next);
    println("after:" + node.value)
}说明:
原理
假设链表中有 3 个节点,value 分别为 1,2,3,以上代码的执行流程就类似于下面的伪码
// 1 -> 2 -> 3 -> null  f(1)
void f(Node node = 1) {
    println("before:" + node.value) // 1
    void f(Node node = 2) {
        println("before:" + node.value) // 2
        void f(Node node = 3) {
            println("before:" + node.value) // 3
            void f(Node node = null) {
                if(node == null) {
                    return;
                }
            }
            println("after:" + node.value) // 3
        }
        println("after:" + node.value) // 2
    }
    println("after:" + node.value) // 1
}思路
例如之前遍历链表的递推关系为

用递归方法求阶乘

代码
private static int f(int n) {
    if (n == 1) {
        return 1;
    }
    return n * f(n - 1);
}
拆解伪码如下,假设 n 初始值为 3
f(int n = 3) { // 解决不了,递
    return 3 * f(int n = 2) { // 解决不了,继续递
        return 2 * f(int n = 1) {
            if (n == 1) { // 可以解决, 开始归
                return 1;
            }
        }
    }
}用递归反向打印字符串,n 为字符在整个字符串 str 中的索引位置
递推关系

代码为
public static void reversePrint(String str, int index) {
    if (index == str.length()) {
        return;
    }
    reversePrint(str, index + 1);
    System.out.println(str.charAt(index));
}拆解伪码如下,假设字符串为 “abc”
void reversePrint(String str, int index = 0) {
    void reversePrint(String str, int index = 1) {
        void reversePrint(String str, int index = 2) {
            void reversePrint(String str, int index = 3) { 
                if (index == str.length()) {
                    return; // 开始归
                }
            }
            System.out.println(str.charAt(index)); // 打印 c
        }
        System.out.println(str.charAt(index)); // 打印 b
    }
    System.out.println(str.charAt(index)); // 打印 a
}public static int binarySearch(int[] a, int target) {
    return recursion(a, target, 0, a.length - 1);
}
public static int recursion(int[] a, int target, int i, int j) {
    if (i > j) {
        return -1;
    }
    int m = (i + j) >>> 1;
    if (target < a[m]) {
        return recursion(a, target, i, m - 1);
    } else if (a[m] < target) {
        return recursion(a, target, m + 1, j);
    } else {
        return m;
    }
}public static void main(String[] args) {
    int[] a = {3, 2, 6, 1, 5, 4, 7};
    bubble(a, 0, a.length - 1);
    System.out.println(Arrays.toString(a));
}
private static void bubble(int[] a, int low, int high) {
    if(low == high) {
        return;
    }
    int j = low;
    for (int i = low; i < high; i++) {
        if (a[i] > a[i + 1]) {
            swap(a, i, i + 1);
            j = i;
        }
    }
    bubble(a, low, j);
}
private static void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}发生交换,意味着有无序情况
最后一次交换(以后没有无序)时,左侧 i 仍是无序,右侧 i+1 已然有序
public static void main(String[] args) {
    int[] a = {3, 2, 6, 1, 5, 7, 4};
    insertion(a, 1, a.length - 1);
    System.out.println(Arrays.toString(a));
}
private static void insertion(int[] a, int low, int high) {
    if (low > high) {
        return;
    }
    int i = low - 1;
    int t = a[low];
    while (i >= 0 && a[i] > i) {
        a[i + 1] = a[i];
        i--;
    }
    if(i + 1 != low) {
        a[i + 1] = t;
    }    
    insertion(a, low + 1, high);
}n 个人排成圆圈,从头开始报数,每次数到第 m 个人(m 从 1 开始)杀之,继续从下一个人重复以上过程,求最后活下来的人是谁?
方法1
根据最后的存活者 a 倒推出它在上一轮的索引号
| f(n,m) | 本轮索引 | 为了让 a 是这个索引,上一轮应当这样排 | 规律 | 
|---|---|---|---|
| f(1,3) | 0 | x x x a | (0 + 3) % 2 | 
| f(2,3) | 1 | x x x 0 a | (1 + 3) % 3 | 
| f(3,3) | 1 | x x x 0 a | (1 + 3) % 4 | 
| f(4,3) | 0 | x x x a | (0 + 3) % 5 | 
| f(5,3) | 3 | x x x 0 1 2 a | (3 + 3) % 6 | 
| f(6,3) | 0 | x x x a | 
方法2
设 n 为总人数,m 为报数次数,解返回的是这些人的索引,从0开始
| f(n, m) | 解 | 规律 | 
|---|---|---|
| f(1, 3) | 0 | |
| f(2, 3) | 0 1 => 1 | 3%2=1 | 
| f(3, 3) | 0 1 2 => 0 1 | 3%3=0 | 
| f(4, 3) | 0 1 2 3 => 3 0 1 | 3%4=3 | 
| f(5, 3) | 0 1 2 3 4 => 3 4 0 1 | 3%5=3 | 
| f(6, 3) | 0 1 2 3 4 5 => 3 4 5 0 1 | 3%6=3 | 






下面的表格列出了数列的前几项
| F0 | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | F13 | 
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 | 233 | 
实现
public static int f(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    return f(n - 1) + f(n - 2);
}执行流程

时间复杂度

1.更多 Fibonacci 参考[8][9][^10]
2.以上时间复杂度分析,未考虑大数相加的因素
变体1 - 兔子问题[^8]


分析
| 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 | … | … | 
Tower of Hanoi,是一个源于印度古老传说:大梵天创建世界时做了三根金刚石柱,在一根柱子从下往上按大小顺序摞着 64 片黄金圆盘,大梵天命令婆罗门把圆盘重新摆放在另一根柱子上,并且规定
下面的动图演示了4片圆盘的移动方法

使用程序代码模拟圆盘的移动过程,并估算出时间复杂度
思路




题解
public class E02HanoiTower {
    /*
             源 借 目
        h(4, a, b, c) -> h(3, a, c, b)
                         a -> c
                         h(3, b, a, c)
     */
    static LinkedList<Integer> a = new LinkedList<>();
    static LinkedList<Integer> b = new LinkedList<>();
    static LinkedList<Integer> c = new LinkedList<>();
    static void init(int n) {
        for (int i = n; i >= 1; i--) {
            a.add(i);
        }
    }
    static void h(int n, LinkedList<Integer> a, 
                  LinkedList<Integer> b, 
                  LinkedList<Integer> c) {
        if (n == 0) {
            return;
        }
        h(n - 1, a, c, b);
        c.addLast(a.removeLast());
        print();
        h(n - 1, b, a, c);
    }
    private static void print() {
        System.out.println("-----------------------");
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
    }
    public static void main(String[] args) {
        init(3);
        print();
        h(3, a, b, c);
    }
}
分析
把它斜着看
        1
      1   1
    1   2   1
  1   3   3   1
1   4   6   4   1

题解
public static void print(int n) {
    for (int i = 0; i < n; i++) {
        if (i < n - 1) {
            System.out.printf("%" + 2 * (n - 1 - i) + "s", " ");
        }
        for (int j = 0; j < i + 1; j++) {
            System.out.printf("%-4d", element(i, j));
        }
        System.out.println();
    }
}
public static int element(int i, int j) {
    if (j == 0 || i == j) {
        return 1;
    }
    return element(i - 1, j - 1) + element(i - 1, j);
}优化1
是 multiple recursion,因此很多递归调用是重复的,例如
recursion(2, 0) + recursion(2, 1)
recursion(2, 1) + recursion(2, 2)
这里 recursion(2, 1) 就重复调用了,事实上它会重复很多次,可以用 static AtomicInteger counter = new AtomicInteger(0) 来查看递归函数的调用总次数
事实上,可以用 memoization 来进行优化:
public static void print1(int n) {
    int[][] triangle = new int[n][];
    for (int i = 0; i < n; i++) {
        // 打印空格
        triangle[i] = new int[i + 1];
        for (int j = 0; j <= i; j++) {
            System.out.printf("%-4d", element1(triangle, i, j));
        }
        System.out.println();
    }
}
public static int element1(int[][] triangle, int i, int j) {
    if (triangle[i][j] > 0) {
        return triangle[i][j];
    }
    if (j == 0 || i == j) {
        triangle[i][j] = 1;
        return triangle[i][j];
    }
    triangle[i][j] = element1(triangle, i - 1, j - 1) + element1(triangle, i - 1, j);
    return triangle[i][j];
}
优化2
public static void print2(int n) {
    int[] row = new int[n];
    for (int i = 0; i < n; i++) {
        // 打印空格
        createRow(row, i);
        for (int j = 0; j <= i; j++) {
            System.out.printf("%-4d", row[j]);
        }
        System.out.println();
    }
}
private static void createRow(int[] row, int i) {
    if (i == 0) {
        row[0] = 1;
        return;
    }
    for (int j = i; j > 0; j--) {
        row[j] = row[j - 1] + row[j];
    }
}注意:还可以通过每一行的前一项计算出下一项,不必借助上一行,这与杨辉三角的另一个特性有关,暂不展开了
力扣对应题目,但递归不适合在力扣刷高分,因此只列出相关题目,不做刷题讲解了
| 题号 | 名称 | 
|---|---|
| Leetcode118 | 杨辉三角 | 
| Leetcode119 | 杨辉三角II | 
上述代码存在很多重复的计算,例如求 f ( 5 ) f(5)f(5) 递归分解过程


Memoization 记忆法(也称备忘录)是一种优化技术,通过存储函数调用结果(通常比较昂贵),当再次出现相同的输入(子问题)时,就能实现加速效果,改进后的代码
public static void main(String[] args) {
    int n = 13;
    int[] cache = new int[n + 1];
    Arrays.fill(cache, -1);
    cache[0] = 0;
    cache[1] = 1;
    System.out.println(f(cache, n));
}
public static int f(int[] cache, int n) {
    if (cache[n] != -1) {
        return cache[n];
    }
    cache[n] = f(cache, n - 1) + f(cache, n - 2);
    return cache[n];
}优化后的图示,只要结果被缓存,就不会执行其子问题

注意
1.记忆法是动态规划的一种情况,强调的是自顶向下的解决
2.记忆法的本质是空间换时间
爆栈
用递归做 n + ( n − 1 ) + ( n − 2 ) . . . + 1
public static long sum(long n) {
    if (n == 1) {
        return 1;
    }
    return n + sum(n - 1);
}
在我的机器上 n = 12000 时,爆栈了
Exception in thread "main" java.lang.StackOverflowError
	at Test.sum(Test.java:10)
	at Test.sum(Test.java:10)
	at Test.sum(Test.java:10)
	at Test.sum(Test.java:10)
	at Test.sum(Test.java:10)
	...为什么呢?

long sum(long n = 3) {
    return 3 + long sum(long n = 2) {
        return 2 + long sum(long n = 1) {
            return 1;
        }
    }
}尾调用
如果函数的最后一步是调用一个函数,那么称为尾调用,例如
function a() {
    return b()
}下面三段代码不能叫做尾调用
function a() {
    const c = b()
    return c
}function a() {
    return b() + 1
}function a(x) {
    return b() + x
}一些语言[^11]的编译器能够对尾调用做优化,例如
function a() {
    // 做前面的事
    return b() 
}
function b() {
    // 做前面的事
    return c()
}
function c() {
    return 1000
}
a()没优化之前的伪码
function a() {
    return function b() {
        return function c() {
            return 1000
        }
    }
}优化后伪码如下
a()
b()
c()为何尾递归才能优化?
调用 a 时
调用 b 时
如果调用 a 时
尾递归
尾递归是尾调用的一种特例,也就是最后一步执行的是同一个函数
尾递归避免爆栈
安装 Scala

Scala 入门
object Main {
  def main(args: Array[String]): Unit = {
    println("Hello Scala")
  }
}还是先写一个会爆栈的函数
def sum(n: Long): Long = {
    if (n == 1) {
        return 1
    }
    return n + sum(n - 1)
}不出所料,在 n = 11000 n = 11000n=11000 时,还是出了异常
println(sum(11000))
Exception in thread "main" java.lang.StackOverflowError
	at Main$.sum(Main.scala:25)
	at Main$.sum(Main.scala:25)
	at Main$.sum(Main.scala:25)
	at Main$.sum(Main.scala:25)
	...这是因为以上代码,还不是尾调用,要想成为尾调用,那么:
def sum(n: Long): Long = {
    if (n == 1) {
        return 1
    }
    return n + sum(n - 1)  // 依赖于外层函数的 n 变量
}如何让它执行后就摆脱对 n 的依赖呢?
sum(n - 1, n + 累加器)改写后代码如下
@tailrec
def sum(n: Long, accumulator: Long): Long = {
    if (n == 1) {
        return 1 + accumulator
    } 
    return sum(n - 1, n + accumulator)
}执行流程如下,以伪码表示 s u m ( 4 , 0 )
// 首次调用
def sum(n = 4, accumulator = 0): Long = {
    return sum(4 - 1, 4 + accumulator)
}
// 接下来调用内层 sum, 传参时就完成了累加, 不必等回来时累加,当内层 sum 调用后,外层 sum 空间没必要保留
def sum(n = 3, accumulator = 4): Long = {
    return sum(3 - 1, 3 + accumulator)
}
// 继续调用内层 sum
def sum(n = 2, accumulator = 7): Long = {
    return sum(2 - 1, 2 + accumulator)
}
// 继续调用内层 sum, 这是最后的 sum 调用完就返回最后结果 10, 前面所有其它 sum 的空间早已释放
def sum(n = 1, accumulator = 9): Long = {
    if (1 == 1) {
        return 1 + accumulator
    }
}本质上,尾递归优化是将函数的递归调用,变成了函数的循环调用
改循环避免爆栈
public static void main(String[] args) {
    long n = 100000000;
    long sum = 0;
    for (long i = n; i >= 1; i--) {
        sum += i;
    }
    System.out.println(sum);
}




例7. 二分查找递归
int f(int[] a, int target, int i, int j) {
    if (i > j) {
        return -1;
    }
    int m = (i + j) >>> 1;
    if (target < a[m]) {
        return f(a, target, i, m - 1);
    } else if (a[m] < target) {
        return f(a, target, m + 1, j);
    } else {
        return m;
    }
}
例8. 归并排序递归
void split(B[], i, j, A[])
{
    if (j - i <= 1)                    
        return;                                
    m = (i + j) / 2;             
    
    // 递归
    split(A, i, m, B);  
    split(A, m, j, B); 
    
    // 合并
    merge(B, i, m, j, A);
}
例9. 快速排序递归
algorithm quicksort(A, lo, hi) is 
  if lo >= hi || lo < 0 then 
    return
  
  // 分区
  p := partition(A, lo, hi) 
  
  // 递归
  quicksort(A, lo, p - 1) 
  quicksort(A, p + 1, hi) 
像下面的递归式,都不能用主定理求解
例1 - 递归求和
long sum(long n) {
    if (n == 1) {
        return 1;
    }
    return n + sum(n - 1);
}
例2 - 递归冒泡排序
void bubble(int[] a, int high) {
    if(0 == high) {
        return;
    }
    for (int i = 0; i < high; i++) {
        if (a[i] > a[i + 1]) {
            swap(a, i, i + 1);
        }
    }
    bubble(a, high - 1);
}


阅读量:556
点赞量:0
收藏量:1