如何理解递归算法?
首先说说递归思想,我认为可以从以下三点进行把握:
- 将大问题分解为有限个子问题;
- 每个子问题的求解方式相同;
- 存在已知的最小子问题,作为“归”的条件。
一句话解释:递归思想是将大问题分解为数个求解方式相同的子问题,且该问题具有已知的最小子问题。
另外,递归是分为两个部分:“递”和“归”,这不是废话。在递归算法中,我们可以在“递”的时候处理问题,也可以在“归”的时候处理问题,前者更高效一点,但问题不大。
要想把握住“递”,就是理解函数下一步调用自己干了啥?
要想把握住“归”,就是理解函数的返回条件是什么?是那个已知的最小子问题。
然后说说如何理解某个递归算法:
- 一定要从宏观理解,即理解此函数总体而言是干什么的,究竟是处理什么“大问题”的,切忌死扣一大堆单步理解。
- 在递归算法中,大问题被分解为数个求解方式相同的子问题,这个求解方式体现在该递归函数单步做了什么事,因为递归单步只处理一个子问题。换句话说,所谓单步操作,实际上就是每次对子问题的求解。
- 找到递归函数的递推方程,或基本思想;
- 最后把一大堆单步处理按照递推思想,直接像多米诺骨牌一样递推就好了!
- 注意“归”的条件
下举例说明:
宏观来看,这是一个反转单链表的函数:(注:结构体在文章末)
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 }
但可能你还是不太好把握,下面我来详细说说:
- 首先我们来看看该递归函数的单步操作,结合函数变量名我们可以发现:它的单步操作核心是反转curr与pre,即把curr->next指向pre。
- 然后函数就接着调用自身,但注意传递的参数变了,原来是curr的地方传next, 而原本是pre的地方传了curr。
- 这个next是什么呢?在当前函数中,next是curr->next, 哦原来next是单链表的第二个结点。所以接下来的调用就是反转next与curr。
- 这样的单步操作一步步的像多米诺骨牌一样传递下去,直到curr变成nullptr,此时已经递到单链表末尾了,该返回了,即触发了“归”的条件。
结合这个例子我们再来看看如何理解递归算法:
- 找到单步操作:在此例中是反转curr与pre;
- “递”了啥?即下一步干了啥:在此例中是反转next与pre
- 啥时候“归”的?哦,是当curr为nullptr的时候,实际上就是当前链表为空的时候。
- 怎么像多米诺骨牌一样?此例中,每次反转curr与pre,下一次反转curr->next与curr,一直一直递推下去到单链表末尾,也就把全部结点反转了!
注释:
① 此例是尾递归,还有另一种“当前结果依赖于之后结果”的递归没讲,下次再写;
② 单链表结构体如下
1 #define ElemType int 2 typedef struct LNode { 3 ElemType data; 4 LNode *next; 5 } LNode, *LinkedList;
③ 如果你想用此例反转带头结点的单链表,也很简单,外边再套个娃就行。简单来说就是:一个反转带头结点的单链表的函数,里边调用了反转头结点之后单链表的函数,最后还是返回头结点地址即可。
④ 请期待下一篇《如何写递归算法》