如何理解递归算法?

 

首先说说递归思想,我认为可以从以下三点进行把握:

  1. 将大问题分解为有限个子问题;
  2. 每个子问题的求解方式相同;
  3. 存在已知的最小子问题,作为“归”的条件。

 

  一句话解释:递归思想是将大问题分解为数个求解方式相同的子问题,且该问题具有已知的最小子问题。

 

另外,递归是分为两个部分:“递”和“归”,这不是废话。在递归算法中,我们可以在“递”的时候处理问题,也可以在“归”的时候处理问题,前者更高效一点,但问题不大。

  要想把握住“递”,就是理解函数下一步调用自己干了啥?

  要想把握住“归”,就是理解函数的返回条件是什么?是那个已知的最小子问题。

 

然后说说如何理解某个递归算法:

  1. 一定要从宏观理解,即理解此函数总体而言是干什么的,究竟是处理什么“大问题”的,切忌死扣一大堆单步理解。
  2. 在递归算法中,大问题被分解为数个求解方式相同的子问题,这个求解方式体现在该递归函数单步做了什么事,因为递归单步只处理一个子问题。换句话说,所谓单步操作,实际上就是每次对子问题的求解
  3. 找到递归函数的递推方程,或基本思想;
  4. 最后把一大堆单步处理按照递推思想,直接像多米诺骨牌一样递推就好了!
  5. 注意“归”的条件

 

下举例说明:

 

宏观来看,这是一个反转单链表的函数:(注:结构体在文章末)

 1 /**
 2  * @param: *curr是当前单链表表头结点,*pre是*curr的前一个结点
 3 */
 4 LinkedList reverseList(LNode *curr, LNode *pre = nullptr) {
 5     // 最小子问题:若当前链表头结点指针为空,则返回pre指针即可
 6     if (curr == nullptr) {
 7         return pre;
 8     }
 9 
10     // 当前操作:反转curr与pre
11     LNode *next = curr->next;   // 先保存一下curr->next, 它是待会下一次反转的头结点
12     curr->next = pre;
13 
14     return reverseList(next, curr);    // 向后递推
15 }

 

但可能你还是不太好把握,下面我来详细说说:

  1. 首先我们来看看该递归函数的单步操作,结合函数变量名我们可以发现:它的单步操作核心是反转curr与pre,即把curr->next指向pre。
  2. 然后函数就接着调用自身,注意传递的参数变了,原来是curr的地方传next, 而原本是pre的地方传了curr。
  3. 这个next是什么呢?在当前函数中,next是curr->next, 哦原来next是单链表的第二个结点。所以接下来的调用就是反转next与curr。
  4. 这样的单步操作一步步的像多米诺骨牌一样传递下去,直到curr变成nullptr,此时已经递到单链表末尾了,该返回了,即触发了“归”的条件。

 

结合这个例子我们再来看看如何理解递归算法:

  1. 找到单步操作:在此例中是反转curr与pre;
  2. “递”了啥?即下一步干了啥:在此例中是反转next与pre
  3. 啥时候“归”的?哦,是当curr为nullptr的时候,实际上就是当前链表为空的时候。
  4. 怎么像多米诺骨牌一样?此例中,每次反转curr与pre,下一次反转curr->next与curr,一直一直递推下去到单链表末尾,也就把全部结点反转了!

 

 

 

 

注释:

①    此例是尾递归,还有另一种“当前结果依赖于之后结果”的递归没讲,下次再写;

②    单链表结构体如下

1 #define ElemType int
2 typedef struct LNode {
3     ElemType data;
4     LNode *next;
5 } LNode, *LinkedList;

③    如果你想用此例反转带头结点的单链表,也很简单,外边再套个娃就行。简单来说就是:一个反转带头结点的单链表的函数,里边调用了反转头结点之后单链表的函数,最后还是返回头结点地址即可。

④    请期待下一篇《如何写递归算法》

posted @ 2024-03-21 08:39  hk416hasu  阅读(46)  评论(0编辑  收藏  举报