LeetCode之路:206. Reverse Linked List

来源:互联网 发布:淘宝双十一购物节方案 编辑:程序博客网 时间:2024/06/06 09:48

一、引言

这道题折磨了我很久呢 T_T

不得不说,这道题的递归解法着实让人有些不能理解(即使亲手写出来了,仔细钻研仍然难以理解自己的代码)。

先来看看题目吧:

Reverse a singly linked list.
Hint:
A linked list can be reversed either iteratively or recursively. Could you implement both?

这道题的题意非常简单:

逆置单向链表。
提示:
一个单向链表可以循环、递归逆置。那么你能使用这两种方法实现它吗?

这道题其实不难,难就难在了需要用两种方法实现它。

另外,或许也是 LeetCode 上面的一道题的思路启发了我,逆置单链表本身就又有两种思路:

1.逆置结构:改变指针指向构成单链表逆置
2.逆置值:改变值构成单链表逆置

不能理解的话,我这里画了一个图:

reverse

二、逆置值:循环和递归的实现

逆置值的思路,主要是需要两次遍历列表,第一次将所有的数据全部获取,第二次将获取到的数据逆向放置。

以下是逆置循环版本代码:

// my solution 1 iteratively change value , runtime = 9 msclass Solution1 {public:    ListNode* reverseList(ListNode* head) {        vector<int> value;        for (auto p = head; p != nullptr; p = p->next)            value.push_back(p->val);        int i = 0;        for (auto p = head; p != nullptr; p = p->next, ++i)            p->val = value[value.size() - i - 1];        return head;    }};

这里使用了 vector<int> 来存储链表中的值集合,最后一次遍历中,使用了 value[value.size() - i - 1] 来实现了值的逆置设入。

这个思路的递归版本会比较复杂,需要对递归有深刻的理解,想想如何实现两次遍历呢?

// my solution 2 recursively change value , runtime = 9 msclass Solution2 {public:    ListNode* reverseList(ListNode* head) {        if (head == nullptr) return head;        value.push(head->val);        reverseList(head->next);        head->val = value.front();        value.pop();        return head;    }private:    queue<int> value;};

这份代码会比较生涩,详细解释下:

  1. 首先,我使用了 std::queue 队列来存储链表中的值。使用队列的好处不言而喻,正好实现了逆置需求。

  2. 其次,让我们看看递归过程:
    1. 首先,参数节点判空处理
    2. 然后,在 reverseList() 递归调用前,实现队列值的压入;这样的话,直到找到单向链表的末尾,都在进行值的压入操作
    3. 再然后,我们结束了递归调用的那一刻,也就是我们递归遍历到了单向链表结尾的时候,此时我们将队列的值逆向设置在每层递归的 head->val 中去,也就刚好实现了值的逆置

这个方法非常巧妙,即使是我已经写出来了,再来看看这个方法依然觉得非常简洁优雅。

三、逆置结构:循环和递归的实现

接下来,让我们看看逆置结构的思路:这条思路就是要处理指针指向了。比之逆置值的思路而言,这一思路能够少遍历一次。

以下是逆置结构循环版本代码:

// my solution 3 iteratively change structure , runtime = 9 msclass Solution3 {public:    ListNode* reverseList(ListNode* head) {        ListNode *pre = nullptr;        while (head != nullptr) {            auto temp = head->next;            head->next = pre;            pre = head;            head = temp;        }        return pre;    }};

这份代码并不像表面上看到的那么易懂:

  1. 首先,我们定义了一个 pre 变量用来记录 head 节点之前的节点值(初始化为空指针)

  2. 然后,我们进入循环。要知道我们要干什么,我们要将 head(也就是当前节点)的 next 指针指向 pre,仅此而已。但是为了实现 head 向 head->next 的递进不与 head->next 的赋值相冲突,我们需要定义一个 temp 临时变量记录其值。

  3. 最后,我们需要知道返回的是什么。我们需要返回新的链表的头结点,而这个节点又刚好是 head 递进到空指针后的 pre 节点

可见,处理指针远比处理值的思路的代码逻辑复杂。

不过更难以理解的还是下面的逆置结构递归版本代码:

// my solution 4 recursively change structure , runtime = 6 msclass Solution {public:    ListNode* reverseList(ListNode* head) {        if (!head || !head->next) return head;        auto newHead = reverseList(head->next);        head->next->next = head;        head->next = nullptr;        return newHead;    }};

这个方法可不像它表面上那么简单,让我们好好研究下:

  1. 首先,我进行了判空处理。一般的判空只判空 head 即可,而这里为什么还要判空 head->next 呢?这是因为我们需要通过 head->next 为空的情况下,返回此时的 head 来给出我们最后需要返回的新的链表的头节点(这是这个思路的最难的地方也是精髓所在)

  2. 然后,我们开始递归调用本函数,并且接受递归的返回(指向新链表的头节点用于返回)。递归调用结束之后,我们需要处理指针。刚递归结束时,我们正好处于链表的结尾处,此时我们只需要将当前的 head->next 的 next 指针指向 head 即可,并且将 head->next 指针置空。

  3. 最后,经过层层递归返回,我们一次又一次从尾巴到开头设置了新的指针指向,并且还保存了新的链表的头节点,直接返回即可。

那么,这里有个难以理解的地方:

head->next->next = head;head->next = nullptr;

我们每层递归中,都将 head->next->next 指向了 head,这我们可以理解,这是反向指向;但是 head->next 修改为 nullptr,这就有点说不通了,每次都修改为空指针,不会影响下一层递归的 head->next->next 的赋值吗?

哈哈 ^_^

这个问题我也想了好久,最后发现,其实是我多虑了。

我们在每次递归中,处理的对象都是 head->next 这个对象,那么这个对象在这层递归过程中,就是一个局部变量而已,我们设置 head->next->next 的指向,并没有修改 head->next 的值哦,而是通过我们存储的局部变量 head->next 来寻找到我们需要改变的对象而已。

所以说,因为每层递归之中的 head->next 的值都是相互不会影响的,所以 head->next = nullptr 的运行结果,就是我们期望看到的,整个链表的第一个节点的 next 指针被设置为空,此时返回正好达成题意。

可见,逆置结构的方法远远要比逆置值的方法复杂 T_T,不过确实挺能锻炼人的(比如说我就纠结了小半个下午)。

四、总结

这是一道比较锻炼人的题目,如果真的可以思路清晰的不费吹灰之力就写出以上四个方法的话,那真的是大神了,请收下我的膝盖 :)

所以说,不要小看任何自以为会很简单的题目哦~~~

To be Stronger!