【恋上数据结构】递归(函数调用过程、斐波那契数列、上楼梯、汉诺塔、递归转非递归、尾调用)

什么是递归?

递归:函数(方法)直接或间接调用自身,是一种常用的编程技巧。

方法直接调用自身:

int sum(int n) {
	if (n <= 1) return n;
	return n + sum(n - 1);
}

方法间接调用自身:

/**
 * 没有递归出口, 最终会 StackOverflow
 */
static void a(int v) {
	b(--v);
}
static void b(int v) {
	a(--v);;
}

在这里插入图片描述

函数的调用过程(栈空间)

栈空间会将调用的函数依次入栈,一般来说最先入栈的是 main,下图中的 test1 虽然被调用了,但是没有执行操作,编译器会忽略它test2 调用了 test3,所以 test2test3 依次入栈。
在这里插入图片描述

函数的递归调用过程

下图中 main 先入栈,sum(4)sum(3)sum(2)sum(1) 由于递归调用依次入栈,可见空间复杂度为 O(n)
在这里插入图片描述
在这里插入图片描述

递归实例分析(1 + 2 + 3 + … + 100 的和)

求 1 + 2 + 3 + … + (n - 1) + n 的和(n>0)


递归做法:

int sum(int n) {
	if(n <= 1) return n;
	return sum(n - 1) + n; 
}

总消耗时间 T(n) = T(n − 1) + O(1),因此,时间复杂度:O(n)、空间复杂度:O(n)


循环做法:

int sum(int n) {
int result = 0;
	for (int i = 0; i <= n; i++) {
		result += i;
	}
	return result;
}

时间复杂度:O(n),空间复杂度:O(1)


求和公式:

int sum(int n) {
	if (n <= 1) return n;
	return (1 + n) * n >> 1;
}

时间复杂度:O(1),空间复杂度:O(1)


  • 注意:使用递归不是为了求得最优解,是为了简化解决问题的思路,代码会更加简洁
  • 递归求出来的很有可能不是最优解,也有可能是最优解

递归的基本思想、使用套路

基本思想:拆解问题,大化小
在这里插入图片描述
使用套路:明确功能、关系、边界条件
在这里插入图片描述

斐波那契数列

斐波那契数列:1、1、2、3、5、8、13、21、34、……

  • F ( 1 ) = 1 F ( 2 ) = 1 F ( n ) = F ( n 1 ) + F ( n 2 ) n 3 F(1) = 1,F(2) = 1,F(n) = F(n - 1) + F(n - 2)(n ≥ 3)

编写一个函数求第 n 项斐波那契数:

int fib(int n) {
	if (n <= 2) return 1;
	return fib(n - 1) + fib(n - 2);
}
  • 根据递推式 T(n) = T(n − 1) + T(n − 2) + O(1),可知:
  • 时间复杂度:O(2n)
  • 空间复杂度:O(n)

递归调用的空间复杂度 = 递归深度 * 每次调用所需的辅助空间

fib函数的调用过程

在这里插入图片描述

fib优化1 — 记忆化

用数组存放计算过的结果,避免重复计算

时间复杂度:O(n),空间复杂度:O(n)

/*
 * 用数组存放计算过的结果,避免重复计算
 */
int fib(int n) {
	if(n <= 2) return 1;
	int[] array = new int[n + 1];
	array[2] = array[1] = 1;
	return fib(array, n);
}
int fib(int[] array, int n) {
	if (array[n] == 0) {
		array[n] = fib(array, n - 1) + fib(array, n - 2);
	}
	return array[n];
}

在这里插入图片描述

fib优化2 — 去除递归调用

这是一种 “自底向上” 的计算过程

时间复杂度:O(n),空间复杂度:O(n)

int fib(int n) {
	if(n <= 2) return 1;
	int[] array = new int[n + 1];
	array[2] = array[1] = 1;
	for (int i = 3; i <= n; i++) {
		array[i] = array[i - 1] + array[i - 2];
	}
	return array[n];
}

fib优化3 — 滚动数组

由于每次运算只需要用到数组中的 2 个元素,所以可以使用滚动数组来优化

时间复杂度:O(n),空间复杂度:O(1)

int fib(int n) {
	if (n <= 2) return 1; 
	int[] array = new int[2];
	array[0] = array[1] = 1;
	for (int i = 3; i <= n; i++) {
		array[i % 2] = array[(i - 1) % 2] + array[(i - 2) % 2];
	}
	return array[n % 2];
}

乘、除、模运算效率较低,建议用其他方式(位运算)取代

int i = 100;
// (i % 2) == (i & 1);
System.out.println(
	(i % 2) == (i & 1) // true
);
 // (i * 2) == (i << 1);
System.out.println(
	(i * 2) == (i << 1) // true
);
// (i / 2) == (i >> 1);
System.out.println(
	(i / 2) == (i >> 1) // true
);

位运算优化后的滚动数组:

int fib(int n) {
	if ( n <= 2) return 1;
	int[] array = new int[2];
	array[0] = array[1] = 1;
	for (int i = 3; i <= n; i++) {
		array[i & 1] = array[(i - 1) & 1] + array[(i - 2) & 1];
	}
	return array[n & 1];
}

fib优化4 — 去除数组

只有两个元素,直接通过2个变量即可,不需要创建数组。

时间复杂度:O(n),空间复杂度:O(1)

int fib(int n) {
	if (n <= 2) return 1;
	int first = 1;
	int second = 1;
	for (int i = 3; i <= n; i++) {
		second = first + second;
		first = second - first;
	}
	return second;
}

fib优化5 — 数学公式

在这里插入图片描述
时间复杂度、空间复杂度取决于 pow 函数(至少可以低至 O(logn) )

int fib(int n) {
	double c = Math.sqrt(5);
	return (int)((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c);
}

上楼梯(跳台阶)

在这里插入图片描述
1 阶台阶只有1种走法,所以: f ( 1 ) = 1 f(1) = 1
2 阶台阶有2种走法(11、2),所以: f ( 2 ) = 2 f(2) = 2
3 阶台阶有3种走法(111、21、12),所以: f ( 3 ) = 3 f(3) = 3

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

跟斐波那契数列几乎一样,因此优化思路也是一致的:

int climbStairs(int n) {
	if (n <= 2) return n;
	int first = 1;
	int second = 2;
	for(int i = 3; i <= n; i++) {
		second = first + second;
		first = second - first;
	}
	return second;
}

汉诺塔(Hanoi)

在这里插入图片描述

1个盘子、2个盘子、3个盘子图示

1个盘子的情况:
在这里插入图片描述
2个盘子的情况:
在这里插入图片描述
3个盘子的情况:
在这里插入图片描述
在这里插入图片描述

汉诺塔 — 思路

分 2 种情况讨论即可:

  • 当 n == 1时,直接将盘子从 A 移动到 C

  • 当 n > 1时,可以拆分成3大步骤

    • ① 将 n – 1 个盘子从 A 移动到 B :hanoi(n - 1, p1, p3, p2);
      在这里插入图片描述
    • ② 将编号为 n 的盘子从 A 移动到 C:move(n, p1, p3);
      在这里插入图片描述
    • ③ 将 n – 1 个盘子从 B 移动到 C:hanoi(n - 1, p2, p1, p3);
      在这里插入图片描述

    步骤 ① ③ 明显是个递归调用

汉诺塔 — 实现

T(n) = 2 ∗ T(n - 1) + O(1),时间复杂度是:O(2n),空间复杂度:O(n)

public class Hanoi {
	public static void main(String[] args) {
		new Hanoi().hanoi(4, "A", "B", "C");
	}
	/**
	 * 将第 i 号盘子从 from 移到 to
	 */
	void move(int i, String from, String to) {
		System.out.println(i + "号盘子: " + from + "->" + to);
	}
	
	/**
	 * 将 n 个盘子从 p1 移动到 p3
	 */
	void hanoi(int n, String p1, String p2, String p3) {
		if (n <= 1) {
			move(n, p1, p3);
			return;
		}
		hanoi(n - 1, p1, p3, p2); 	//  将 n – 1 个盘子从 p1 移动到 p2 
		move(n, p1, p3); 			// 将编号为 n 的盘子从 p1 移动到 p3
		hanoi(n - 1, p2, p1, p3); 	// 将 n – 1 个盘子从 p2 移动到 p3 
	}
	
}
1号盘子: A->C
2号盘子: A->B
1号盘子: C->B
3号盘子: A->C
1号盘子: B->A
2号盘子: B->C
1号盘子: A->C

汉诺塔的代码是没有规律的,不像斐波那契数列,因此没有优化的空间

递归转非递归(用栈模拟100%可以转)

记住一句话:递归100%可以转成非递归
在这里插入图片描述

递归转非递归的万能方法

  • 自己维护一个栈,来保存参数、局部变量
  • 但是空间复杂度依然没有得到优化

例如针对下面这段递归代码:

public static void main(String[] args) {
	log(5);
}

static void log(int n) {
	if(n < 1) return;
	log(n - 1);
	int v = n + 10;
	System.out.println(v);
}

我们尝试将递归转为非递归

首先创建一个栈帧类:

public class Frame {
	int n;
	int v;
	public Frame(int n, int v) {
		super();
		this.n = n;
		this.v = v;
	}
}

然后我们手动模拟函数调用后入栈的过程,从而将递归转为非递归

static void log(int n) {
	Stack<Frame> frames = new Stack<>();;
	while (n > 0) {
		frames.push(new Frame(n, n + 10));
		n--;
	}
	while (!frames.isEmpty()) {
		Frame frame = frames.pop();
		System.out.println(frame.v);
	}
}

某些时候其实有更精妙的做法,可以重复使用一组相同的变量来保存每个栈帧的内容

static void log(int n) {
	for(int i = 0; i < n; i++) {
		System.out.println(i + 10);
	}
}

这里重复使用变量 i 保存原来栈帧中的参数,使得空间复杂度从 O(n) 降到了 O(1)。

尾调用(Tail Call)

在这里插入图片描述

下面这段代码不是尾调用:因为它最后一个动作是乘法,没有调用自身。

int factorial(int n) {
	if (n <= 1) return n;
	return n * factorial(n - 1);
}

尾调用优化(Tail Call Optimization)

在这里插入图片描述

尾调用优化前后的汇编代码(C++)

针对这么一段尾调用代码:

void test(int n) {
	if (n < 0) return;
	printf("test - %d\n", n);
	test(n - 1);
}

尾调用优化的汇编代码:
在这里插入图片描述
尾调用优化的汇编代码:
在这里插入图片描述

尾递归示例

阶乘

求 n 的阶乘 1 * 2 * 3 * … * (n - 1) * n (n>0)

普通递归:

int factorial(int n ) {
	if (n <= 1) return n;
	return n * factorial(n - 1);
}

转化为尾递归

int factorial(int n ) {
	return factorial(n, 1);
}
/**
 * @param result 从大到小累乘的结果
 */
int factorial(int n, int result) {
	if(n <= 1) return result;
	return factorial(n - 1, n * result);
}

斐波那契数列

普通递归:

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

转化为尾递归

int fib(int n) {
	return fib(n, 1, 1);
}

int fib(int n, int first, int second) {
	if (n <= 1) return 1;
	return fib(n - 1, second, first + second);
}
发布了191 篇原创文章 · 获赞 71 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/105532299
今日推荐