今天(2020-08-26)写完一个简单的链表题:删除链表的倒数第 n 个结点
    思路也非常简单(暴力找长度,然后与 n 相减得到对应的答案,就不说了):

    • 设置两个指针:quick 和 slow,共同指向链头。
    • quick 先走 n 步。
    • quick 和 slow 一起走,直到 quick 走到链尾。
    • slow 指向的结点就是要删除的结点。

    根据上述思路,我就写出了自己的第一个版本:

    1. void removeNthFromEnd1(node** head_ref, int n)
    2. {
    3. if (n == 0) return ;
    4. node* slow = *head_ref;
    5. node* quick = *head_ref;
    6. for(int i=0; i<n; ++i) { // 这里循环 n 次
    7. quick = quick->next;
    8. }
    9. if (quick == NULL) {
    10. *head_ref = slow->next;
    11. return ;
    12. }
    13. while(quick->next != NULL) {
    14. quick = quick->next;
    15. slow = slow->next;
    16. }
    17. slow->next = slow->next->next;
    18. }

    可以看到上面会有很多判断边界条件的地方,并且不同的情况需要不同处理,这里是指,当要删除第一个元素的时候。
    故可以引入哑结点(dummy node)也可以称之为头结点,来处理上述边界问题:

    1. void removeNthFromEnd2(node** head_ref, int n) // 使用哑结点
    2. {
    3. if (n == 0) return;
    4. node* dummy = (node*)malloc(sizeof(node));
    5. dummy->next = *head_ref;
    6. node* slow = dummy;
    7. node* quick = dummy;
    8. for(int i=0; i<=n; ++i) { // 加入 dummy ,故要循环 n + 1 次
    9. quick = quick->next;
    10. }
    11. while(quick != NULL) {
    12. quick = quick->next;
    13. slow = slow->next;
    14. }
    15. slow->next = slow->next->next;
    16. *head_ref = dummy->next;
    17. }

    加入哑结点了之后,就不用写 if 来判断删除的是否为第一个结点了。不过代码也并没有减少多少。
    随后看了题解之后,看到一个很巧妙的写法:

    1. void emoveNthFromEnd3(node** head_ref, int n)
    2. {
    3. if (n == 0) return ;
    4. node* quick = *head_ref;
    5. node* slow = *head_ref;
    6. while(quick) {
    7. if (n < 0) slow = slow -> next; // 当 quick 前进 n + 1 步,slow 才能走。
    8. quick = quick->next;
    9. --n;
    10. }
    11. if (n == 0) *head_ref = slow->next;
    12. slow->next = slow->next->next;
    13. }

    这里是直接遍历循环整个 quick ,同时记录下前进的步数。当 quick 前进 n+1 步时,slow 就能跟着一起走了。
    关于为什么是 n+1 步。如果你要删除倒数第 n(n 小于或等于链表长度)个结点,quick 就要先走 n 步。然后,quickslow 一起走,这样一个问题就是,slow 走到了你要删除结点的位置。如果你写过链表,就知道,要删除一个结点,必须获得该结点的前一个结点才能删除。所以让 quickn+1 步,以达到 slow 少走一部的效果。
    这里走 n+1 步也非常巧妙:先判断,再减减。
    当然,你可以将判断和减减写在一行,不过这样代码可读性太差了:

    1. if (n-- < 0) slow = slow ->next;

    我个人觉得,循环遍历整个链表的方法已经很不错了。当然还可以将上述方法加上哑结点:

    1. void removeNthFromEnd4(node** head_ref, int n)
    2. {
    3. if (n == 0) return;
    4. node* dummy = (node*)malloc(sizeof(node));
    5. dummy->next = *head_ref;
    6. node* slow = dummy;
    7. node* quick = dummy;
    8. while(quick) {
    9. if (n < 0) slow = slow->next; // 同样是 n + 1 步
    10. n--;
    11. quick = quick->next;
    12. }
    13. slow->next = slow->next->next;
    14. *head_ref = dummy->next;
    15. }

    这里还是 n+1 步,因为加入哑结点并不会影响遍历的结果,quickslow 会共同遍历掉哑结点。其实这里无论加入多少个哑结点,同样是 quick 先走 n+1 步,slow 再跟着走。直到 quick 遍历完整个链表。也就是 quickslow 的相对位置为 n+1 ,因为 quick 总是回到链尾的 NULL 结点,而 n 正好是要删除的那个数,所以加上一使得 slow 正好位于要删除结点的前一位,使得删除结点变得简单。
    可以注意到,leedcode 上题目得函数声明是使用得指针函数:

    1. node* removeNthFromEnd(node* head, int n)

    而我写得函数都是使用的双指针:

    1. void removeNthFromEnd(node** head, int n)

    我相信聪明的你,会将两者互相转换。
    上述代码,再以下代码的基础上运行:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. struct Node {
    4. int data;
    5. struct Node* next;
    6. };
    7. typedef struct Node node;