递归:如何用三行代码找到“最终推荐人”?

推荐注册返佣金这个功能中,用户A推荐B注册,B推荐C注册,我们可以说C的最终推荐人是A。一般来说,我们会通过数据库来记录这种关系,在数据表中,我们可以记录两行数据,actor_id表示用户id,referrer_id表示推荐人id。


递归:如何用三行代码找到“最终推荐人”?_第1张图片
推荐人.jpg

基于这个背景,给定一个用户id,如何查找这个用户的“最终推荐人”?

递归需要满足的三个条件

1、一个问题的解可以分解为几个子问题的解
2、这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
3、存在递归终止条件

如何编写递归代码

写递归代码的关键是写出递推公式,找到终止条件,然后将递推公式转化为代码。
举个栗子,假如这里有n个台阶,每次你可以跨1个台阶或者2个台阶,请问走这n个台阶有几种走法?
可以根据第一步的走法,将所有走法分为两类,第一类是第一步走了1个台阶,第二类是第一步走了2个台阶。所以n个台阶的走法就是,先走1个台阶后,第n-1个台阶的走法;加上先走2个台阶,第n-2个台阶的走法。用公式表示就是

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

当n=1时,就只有一种走法,f(1) = 1。当n=2时,有两种走法,f(2) = 2。所以递归的终止条件就是f(1) = 1,f(2) = 2。
把终止条件和递推公式放到一起就是

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

将公式转化成代码

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

写递归代码的关键就是,如何将大问题分解为小问题,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
当我们看到递归时,总想把递归平铺展开,脑子里就会循环,一层一层往下调,然后再一层一层返回,这样就很容易绕进去。对于递归代码,这种试图想清楚整个递归过程的做法,实际上是进入了一个误区。很多时候,我们理解起来比较吃力。
如果一个A问题可以分解为若干个子问题B、C、D,可以假设B、C、D已经解决,在此基础上思考如何解决问题A。而且只需要思考问题A与B、C、D的关系即可,不需要一层一层往下思考子问题与子子问题。屏蔽掉递归细节,这样子理解起来就简单多了。
因此,只要遇到递归,我们就把他抽象成一个递推公式,不用想一层一层的调用关系,不要试图用人脑去分解递归的每个步骤。

递归代码要警惕堆栈溢出

函数调用会使用栈来保存临时变量,每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成时,才出栈。如果递归求解的数据规模很大,调用层次很深,就会有堆栈溢出的风险。
如果最大深度比较小,如10、50。我们可以通过在代码中限制递归调用的最大深度来解决这个问题。递归调用超过一定深度之后,就不继续往下递归了,直接返回报错。

递归代码要警惕重复计算

如果我们把走台阶的递归过程分解一下的话,就是这样的。


递归:如何用三行代码找到“最终推荐人”?_第2张图片
递归台阶.jpg

从图中,可以看出,想要计算f(5),就必须要计算f(4)和f(3),而计算f(4),还需要计算f(3),因此f(3)就被计算了很多次。
为了避免重复计算,我们可以用Map来保存已经计算过的f(k),当递归调用f(k)时,我们先判断f(k)有没有被计算过,如果被计算过了,就直接从map中返回,不需要重复计算。代码如下

int f(int n){
  if(n==1){
    return 1;
  }
  if(n==2){
    return 2;
  }
  if(map.contains(n)){
    return map.get(n);
  }
  int result = f(n-1)+f(n-2);
  map.put(n,result);
  return result;
}

怎样将递归代码改为非递归代码

递归代码的表达力强,写起来非常简洁;弊端就是空间复杂度过高,有堆栈溢出风险和重复计算等问题。所以要根据实际情况来选择是否需要用递归的方式实现。

int f(int n){
  if(n==1){
    return 1;
  } 
  if(n==2){
    return 2;
  } 
  int result = 0;
  int pre = 2;
  int prePre = 1;
  for(int j=3;j<=n;j++){
    result = pre + prePre;
    prePre = pre;
    pre = result;
  }
  return result;
}

解答开篇

long findRootReferredId(long actorId){
  long referredId = select referredId from table where actorId = actorId;
  if(referredId == null){
    return actorId;
  }
  return findRootReferredId(referredId);
}

递归是一种非常高效、简洁的编码技巧。只要是满足“三个条件”的问题就可以通过递归代码来解决,不过递归代码也非常难写,难理解。正确姿势是写出递推公式,找出终止条件,然后翻译成代码,递归代码虽然简洁高效,但是递归也有很多弊端。比如堆栈溢出、重复计算、空间复杂度高等,所以在写递归代码时,一定要控制好这些副作用。

你可能感兴趣的:(递归:如何用三行代码找到“最终推荐人”?)