最近重新开始做力扣的习题,加深了自己对于递归的理解,因此以两道题为例分别解释递归的作用。
两道题分别是LeetCode 83和24。虽然它们都有相应的迭代解法,但是递归的解法相对更为巧妙。
以LeetCode83为例:
迭代方式
ListNode deleteDuplicates(ListNode head) {
ListNode dummy = head;
while (head != null && head.next != null) {
if (head.val == head.next.val) {
head.next = head.next.next;
} else {
head = head.next;
}
}
return dummy;
}
我们先看迭代版本的思路:
(1)当值相等时,游标不动,对当前结点进行常规删除
不移动的原因是有可能被删除结点的下一个结点的值依旧可能与当前结点的值相等
(2)当值不等时,不需要删除当前结点,因此进行向下一个结点进行移动
(3)重点:此时方向是从前向后进行的
递归方式
Cpp版本
ListNode* deleteDuplicates(ListNode* head) {
if (!head || !head->next) {
return head;
}
head->next = deleteDuplicates(head->next);
return head->val != head->next->val ? head : head->next;
}
Java版本
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
return head.val != head.next.val ? head : head.next;
}
这里最重要的一点,就是:
我们首先进行了head.next = deleteDuplicates(head.next);
操作,然后再对后续的head结点进行取舍。
先明确一点,对于链表而言,需要改变链表的结构,通常是对其next指针进行变换,而不能对head结点本身进行操作例如
head= another_head
这样,这只是对head本身指代的指针进行了改变,而不会影响原始链表的结构。
这里蕴含了递归的一个精髓:先处理后面的,再计算前面的,即后入先出的思想
用一张图进行解释:
head.next = deleteDuplicates(head.next);
表示:当前结点的后续结点均以完成了删除重复结点的操作。也就是说,图中的node3、node4、node5都不是重复的结点。
那么我们应该怎么删除重复结点呢?记住,递归时最后才处理前面的,因此我们需要比较的仍然是head结点与head.next结点,也就是最后一行代码:
return head.val != head.next.val ? head : head.next;
这句话的含义,是:是否保留head结点
如果head的值与head.next不等,那么选择保留;如果相等,那么返回的就是head.next,因为我们已经通过head.next = deleteDuplicates(head.next);
保证了head.next中没有重复的值。
由于递归时先计算后面的,因此到了链表尾部时,先比较node3和node4,发现两者相等,因此对于deleteDuplicates(node2.next)而言,返回的就是node4;然后再弹栈向前继续计算,同时最终返回的一定就是头部结点。
因此,递归的一个小技巧可以总结如下:
1.先定义递归的终止条件(没的说)
2.通过递归改变链表(或其他)的结构
3.定义具体的变换操作
虽然是一道很简单的题,但是可以从里面提取很多有用的思想,接下来继续看24,这一道题就只讲解递归版本(C++语言)。
ListNode* swapPairs(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode *next = head->next;
head->next = swapPairs(next->next);
next->next = head;
return next;
}
我们按照刚刚总结的技巧来看。
首先需要交换两个结点,那么当前结点和他的下一个结点必须非空,因此我们定义他的终止条件为
if (!head || !head->next) {
return head;
}
我们先看交换的过程:
可以看到,next结点后面的链表是已经完成交换的结果,在此基础上再对next和head两个结点进行交换。
为了保证next后面的链表转换完成,我们先进行:
swapPairs(next->next);
之后再向前进行计算。而交换后的head结点,它的下一个结点就指向之前的next->next,因此有
head->next = swapPairs(next->next);
交换的结果还需要保证next的下一个结点是head:
next->next = head;
最终,由于递归是从后向前进行,最终要返回的是头结点,而此时的头结点是next,因此:
return next;
以上就是这两道题的递归讲解。以前一直都是用迭代的思维去看递归,就非常的痛苦;现在逐渐理解了,一定要用从后向前的方式来看待递归。
最后,希望大家刷题愉快~