算法学习:递归

206. 反转链表

背景:链表

目的:反转链表

方法:递归

如果使得任意两个结点反向连接,假设这两个节点为 firstnext ,应该有 next -> next = first ,但在这之前应该将 next 节点的下一个节点保存下来,否则将访问不到下一个节点。

处理完 firstnext 节点之后,就应该处理 next 和已经保存的下一个节点,处理的过程和上面一样,这就形成了一个递归的过程。

递归的又一个重点是出口条件,按上文所说的递归方法,递归应该在链表末尾结束。4 和 5 两个节点反转结束后,first 节点将指向节点 5,而 next 节点会指向 nullptr,所以当 next == nullptr 时,退出递归。

最终要返回的是反转链表后的头节点,在退出递归时 first 正好指向反转前最后一个节点,返回该节点即可。

额外需要注意的是反转前的头节点,它的 next 需要指向 nullptr,不然链表会有环。

最终实现的函数如下:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head == nullptr)
        {
            return head;
        }
        return reverseList(head,head->next);
    }
    ListNode* reverseList(ListNode* first,ListNode* next) {
        if(next == nullptr)
        {
            return first;
        }
        if(first->next == next)
        {
            first->next = nullptr; 
        }
        ListNode* save = next->next;
        next->next = first;
        return reverseList(next, save);
    }
};

可以更换一种递归方式。

上文当中的递归采用自底向上的思想,是从链表的首部开始遍历的,反转操作会使得某一时刻链表是中断的,无法通过 next 直接访问到未来的节点,于是需要在反转之前保存下一节点,那有没有办法让链表无法中断呢?

试想一下递归从尾部开始遍历,比如先反转 4 和 5 两个节点,再反转 3 和 4 两个节点,向下一个目的节点移动的过程可以交给递归自身来实现,并不需要我们主动去寻找下一个目的节点。

所以这个递归调用需要在反转之前完成,这是自顶向下的思想,也可以理解成深度优先的思想。

利用函数参数的性质使用递归,可以想象成在每一次递归调用时将我们要使用的量和要处理的过程 保存 下来,在栈中作为函数栈帧来展开,等到全部保存完毕(全部展开)之后再开始我们的处理,最顶部的栈帧先进行处理,处理结束之后弹出这块栈帧,栈顶永远是我们要处理的部分,直到栈空,即弹出所有栈帧,自顶向下的递归结束。

那么递归的出口如何确定,何时结束我们的递归?上文中说“全部展开”,当展开到最后一个节点时可以说明已经“全部展开”,对于最后一个节点有 next == nullptr 的特征,当然也需要考虑传入的节点为 nullptr 的情况,所以当位于以上情况时结束递归的调用,对于末尾的元素我们应该只返回它的地址,以符合题意中“返回反转之后的头部指针”。

if(!head || !head->next)
{
    return head;
}

反转的步骤,在“自底向上”的递归分析中我们可以发现 next->next = first 这里可以改变为 first->next->next = first ,于是我们的反转步骤如下

head->next->next = head;
head->next = nullptr; // 避免生成环

最终,最顶层的返回值要一直传递到最底层的调用,所以我们要把每次递归调用的结果返回,完整的代码如下:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(!head || !head->next)
        {
            return head;
        }
        ListNode* newNode = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return newNode;
    }
};

总结

这道题算是递归的入门题目,使用“自底向上”和“自顶向下”两种思想求解,解释的过程有些乱,在我熟练使用递归的思想后再来详细解释。