导语:给定一个用户ID, 如何查找这个用户的“最终推荐人”?

如何理解“递归”

问题

问: 看电影时想知道自己在第几排?
答: 问前一排,前一排再问前一排,直到问到第一排,再返回每一排位置,最终得到结果
解析: 去的过程叫“递”, 回来的过程叫“归”

递推公式

  1. 基本上,所有的递归问题都可以用递推公式来表示
f(n) = f(n - 1) + 1,  其中 f(1) = 1

代码实现

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

递归需要满足的三个条件

  1. 一个问题的解可以分解为几个子问题(数据规模更小)的解

  2. 分解后的子问题,除了数据规模不同,求解思路完全相同

  3. 存在递归终止条件

如何编写递归代码

问题

n个台阶,每次你可以跨1个台阶或2个台阶,n个台阶有多少种走法?

方法

写出递归公式,找到终止条件

f(1) = 1;
f(2) = 2;
f(n) = f(n-2) + f(n-1);

代码实现

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

总结

  1. 将大问题分解为小问题的规律
  2. 写出递推公式
  3. 确定终止条件
  4. 将递推公式和终止条件翻译成代码

递归代码要警惕堆栈溢出

  1. 函数调用使用栈保存临时变量
  2. 函数执行完成返回时出栈
  3. 一直压入栈,会有堆栈溢出风险

如何避免出现堆栈溢出

  1. 限制递归深度
    int f(int n){
    	++depth;
    	if (depth > 1000) {
    		throw exception;
    	}
    
    	if (n == 1) return 1;
    	return f(n-1) + 1;
    }
  2. 缺点:站空间无法确定,递归深度不可靠

递归代码要警惕重复计算

recursion1

通过一个数据结构(比如散列表)来保存已经求解的结果f(k)

public int f(int n){
	if (n == 1) return 1;
	if (n == 2) return 2;

	if (hasSolvedList.containsKey(n)) {
		return hasSolvedList.get(n);
	}

	int ret = f(n-1) + f(n-2);
	hasSolvedList.put(n, ret);
	return ret;
}

时间复杂度:

  1. O(n)
  2. 无法确定,因为函数调用会有额外开销

空间复杂度:

  1. O(n),在递归结束之前,所有函数的变量都会被保存,所以空间复杂度为O(n)

递归代码改写非递归代码

Exapmle1

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

等价于

int f(int n)
{
	int ret = 1;
	for (int i = 2; i <= n; i++) {
		ret = ret + 1;
	}
	return ret;
}

Exapmle2

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

等价于

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

	int ret = 0
	int pre = 2;
	int prepre = 1;

	for (int i = 3; i <= n; i++) {
		ret = pre + prepre;
		prepre = pre;
		pre = ret;
	}

	return ret;
}

递归调试方式

  1. 打印日志发现递归值
  2. 结合条件断点调试

解答开篇

如何找到最终推荐人

long findRootReferererId(long actorId) {
	Long referrerId = select refrerrer_id from [table] where actor_id = actorId;
	return findRootReferererId(referrerId);
}

存在问题

  1. 递归深度未知
  2. 无限递归 (A-B-C-A)

总结

  1. 满足三个条件即可递归
    1. 可拆分
    2. 可重复
    3. 有终止
  2. 递归三步骤
    1. 写出递归公式
    2. 找出终止条件
    3. 翻译递归代码
  3. 递归弊端
    1. 堆栈溢出
    2. 重复计算
    3. 函数调用耗时多
    4. 空间复杂度高

扩展

斐波那契数列(Fibonacci sequence)

  1. 定义

    这个数列从第3项开始,每一项都等于前两项之和

  2. 实现

    1. 写出递归公式
      f(n) = f(n-2) + f(n-1);
    2. 终止条件
      f(0) = 0;
      f(1) = 1;
      
    3. 翻译递归代码
      int Fibonacci(n){
      	if (n == 0) return 0;
      	if (n == 1) return 1;
      
      	//在return时打印即可打印所有数列
      	return f(n-2) + f(n-1);
      }

详见: 黄铮的 数据结构与算法之美 课程系列