leetcode学习——链表

本文主要记录了自己跟着代码随想录通过leetcode上与链表有关的题学习链表知识时整理的笔记。首先学习链表的基础知识,比如链表的定义、类型、存储方式、定义的代码等。然后对链表的基本操作进行简单的介绍,比如链表删除节点、添加节点。同时,分析对比了链表和数组的性能。此外,通过leetcode上链表的经典题目的分析和实现来掌握链表的具体使用,例如删除、增加、查找、两两交换链表的节点、反转链表、判断两个链表是否相交,相交则求出相交的节点、判断链表是否有环,有环则求出环的入口等等。最后,对本文的知识点进行了总结,并附上总结的流程图和本文涉及的所有题目链接地址。

目录

链表基础知识

链表的类型

链表的存储方式

链表的定义代码

链表的基本操作

删除节点

添加节点

与数组性能对比分析

链表经典题目(附详细的解题分析和c++代码)

一、删除链表

二、反转链表

三、两两交换链表中的节点

四、设计链表(增、删、查)

五、删除链表倒数第n个节点

六、链表相交

七、环形链表

链表总结

虚拟头结点

链表的基本操作

反转链表

删除倒数第N个节点

链表相交

环形链表

总结的思维导图

题目链接总结


链表基础知识

定义:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

如图所示:

链表1​​

链表的类型

链表主要有单链表、双链表和循环链表三种。

  • 单链表

刚刚说的就是单链表。

注意:单链表中的指针域只能指向节点的下一个节点。

  • 双链表

定义:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

优点:既可以向前查询也可以向后查询。

如图所示:

链表2​​

  • 循环链表

定义:首尾相连的链表。

链表4​​

链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的

链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

如图所示:

链表3​​

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

链表的定义代码

C/C++的定义链表节点方式,如下所示:

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

C++默认生成一个构造函数(不会初始化任何成员变量),所以定义时可以不写构造函数

通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!

链表的基本操作

删除节点

删除D节点,如图所示:

链表-删除节点​​

分析:只要将C节点的next指针 指向E节点就可以了。

注意:D节点不是依然存留在内存里,只不过是没有在这个链表里而已。所以在C++里最好是再手动释放这个D节点,释放这块内存。

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

添加节点

如图所示:

链表-添加节点​​

链表的增添和删除都是O(1)操作,且不会影响到其他节点。

要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

与数组性能对比分析

链表的特性和数组的特性进行一个对比,如图所示:

链表-链表与数据性能对比​​

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

链表经典题目(附详细的解题分析和c++代码)

主要对链表的基本操作进行方法的介绍和详细地说明,同时会附带相关的题目来加强理解和学习。学习主要参考代码随想录,感兴趣的可以了解一下,解释写得很详细。

一、删除链表

题目链接:203. 移除链表元素

题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]

​​

示例 2: 输入:head = [], val = 1 输出:[]

示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]


解题思路:链表删除节点示意图如下所示:

​​

删除链表节点主要有两种方式:区别在于要不要创建虚拟头结点。

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点在进行删除操作。

(1)直接使用原来的链表来进行移除。

203_链表删除元素3​​

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。需要单独写一段逻辑来处理移除头结点的情况。

203_链表删除元素4​​

203_链表删除元素5​​

(2)设置一个虚拟头结点在进行移除

可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。

203_链表删除元素6​​

return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点

代码

(1)不设虚拟节点且删除时没有手动在内存中删除被删除的节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        while(head && head -> val == val) {
            head = head -> next;
        }
        if (!head) return head;
        ListNode *cur = head;
        while(cur -> next) {
            if(cur -> next -> val == val) {
                cur -> next = cur -> next -> next;
            }else {
                cur = cur -> next;
            }
        }
        return head;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

(2)不设虚拟头结点且手动删除在内存中删除被删除的节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 删除头结点
        while (head != NULL && head->val == val) { // 注意这里不是if
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }
        // 删除非头结点
        ListNode* cur = head;
        while (cur != NULL && cur->next!= NULL) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

(3)设置虚拟头结点并手动在内存中删除被删除的节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

二、反转链表

题目链接:206. 反转链表

题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

​​

示例 1:输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]

示例2输入:head = [1,2] 输出:[2,1]

示例 3:输入:head = [] 输出:[] 


解题思路:

(1)双指针法

只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

206_反转链表​​

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

如动画所示:(纠正:动画应该是先移动pre,在移动cur)

​​

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

代码

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = NULL;
        ListNode* cur = head;
        ListNode* temp;//cur的next,用来存储遍历到的节点的下一个节点
        while (cur) {
            temp = cur -> next;
            cur -> next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

 (2)递归法【还未研究

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方,可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};
  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间

我们可以发现,上面的递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。

具体代码如下(带详细注释):

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 边缘条件判断
        if(head == NULL) return NULL;
        if (head->next == NULL) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode *last = reverseList(head->next);
        // 翻转头节点与第二个节点的指向
        head->next->next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head->next = NULL;
        return last;
    }
}; 
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

三、两两交换链表中的节点

题目链接:24. 两两交换链表中的节点

题目描述:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:输入:head = [1,2,3,4]输出:[2,1,4,3]

​​

示例 2:输入:head = [] 输出:[]

示例 3:输入:head = [1] 输出:[1]


解题分析:画图理解

​​

​​

代码

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode *newNode = new ListNode(0);//创建虚拟头结点
        newNode -> next = head;
        ListNode* cur = newNode;
        while (cur -> next && cur -> next -> next) {
            ListNode* temp = cur -> next;
            ListNode* temp1 = cur -> next -> next ->next;
            cur -> next = cur -> next -> next;//步骤一
            cur -> next -> next = temp;//步骤二
            temp -> next =temp1;//步骤三
            cur = cur -> next -> next;//步骤四
        }
        return newNode -> next;//注意返回的是虚拟头节点的下一个节点
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

四、设计链表(增、删、查)

【还很容易乱】

题目链接:707. 设计链表

题目描述:你可以选择使用单链表或者双链表,设计并实现自己的链表。单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

解题思路:

删除链表节点:

链表-删除节点​​

添加链表节点:

链表-添加节点​​

代码

class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val):val(val), next(nullptr){}
    };

    // 初始化链表
    MyLinkedList() {
        _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
        _size = 0;
    }

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) {
        if (index > (_size - 1) || index < 0) {
            return -1;
        }
        LinkedNode* cur = _dummyHead->next;
        while(index--){ // 如果--index 就会陷入死循环
            cur = cur->next;
        }
        return cur->val;
    }

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }

    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    // 如果index小于0,则在头部插入节点
    void addAtIndex(int index, int val) {

        if(index > _size) return;
        if(index < 0) index = 0;        
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    void deleteAtIndex(int index) {
        if (index >= _size || index < 0) {
            return;
        }
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur ->next;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        //delete命令指示释放了tmp指针原本所指的那部分内存,
        //被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
        //如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
        //如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
        tmp=nullptr;
        _size--;
    }

    // 打印链表
    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    LinkedNode* _dummyHead;

};
  • 时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1)
  • 空间复杂度: O(n)

五、删除链表倒数第n个节点

题目链接:19. 删除链表的倒数第 N 个结点

题目描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]

​​

示例 2:输入:head = [1], n = 1 输出:[]

示例 3:输入:head = [1,2], n = 1 输出:[1]


解题思路:可以通过快慢指针的思想。具体操作如下:初始化快慢指针都指向虚拟头结点。让快指针fast提前走n步,之后快慢指针再一起走,直到快指针走到最后一个节点,此时在删除慢指针后面的那个节点。

解题代码

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* newNode = new ListNode(0);
        newNode->next = head;
        ListNode* low = newNode;
        ListNode* fast = newNode;
        while (n-- && fast->next) {
            fast = fast -> next;
        }
        while (fast->next) {
            fast = fast->next;
            low = low->next;
        }
        ListNode* temp = low->next;
        low->next = low->next->next;
        delete temp;
        return newNode->next;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

六、链表相交

题目链接:面试题 02.07. 链表相交

题目描述:给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交

​​

题目数据 保证 整个链式结构中不存在环。函数返回结果后,链表必须 保持其原始结构

示例 1:

​​

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

​​

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

​​

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

解题思路:此题的关键在于清楚两条链表判断节点相交,可以让长链表先走几步,直到剩下的长度和短链表一样长时,则两个链表逐一节点判断是否相等。注意:相等指的是指针相等,而不是数值相等。

1)求出两个链表的长度size1、seize2,并求出两个链表长度的差值gap,然后让temp1移动到,和temp2 末尾对齐的位置,如图:

​​

 2)此时就可以比较temp1和temp2是否相同,如果不相同,同时向后移动temp1和temp2,如果遇到temp1== temp2,则找到交点。否则循环退出返回空指针。

​​

代码

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* temp1 = headA;
        ListNode* temp2 = headB;
        int size1 = 0, size2 = 0;
        // 计算链表A的节点总数
        while (temp1) {
            size1++;
            temp1 = temp1->next;
        }
        // 计算链表B的节点总数
        while (temp2) {
            size2++;
            temp2 = temp2->next;
        }
        // 重新初始化两个指针指向头结点
        temp1 = headA;
        temp2 = headB;
        // 始终让链表A为长度较长的那个链表,如果不是,则将两个链表进行交换
        if (size1 < size2) {
            swap(size1, size2);
            swap(temp1, temp2);
        }
        // 计算两个链表的节点个数差
        int gap = size1 - size2;
        // 让长的链表即链表A提前走gap步
        while (gap--) {
            temp1 = temp1->next;
        }
        // 开始逐一遍历比较两个链表是否相等
        while (temp1) {
            // 如果相等,则直接返回
            if (temp1 == temp2) {
                return temp1;
            }
            temp1 = temp1->next;
            temp2 = temp2->next;
        }
        // 注意链表不相交的情况返回NULL
        return NULL;
    }
};
  • 时间复杂度:O(n + m)
  • 空间复杂度:O(1)

七、环形链表

题目链接:142. 环形链表 II

题目描述:给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。

示例 1:

​​

输入:head = [3,2,0,-4], pos = 1  输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

​​

输入:head = [1,2], pos = 0  输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

​​

输入:head = [1], pos = -1  输出:返回 null  解释:链表中没有环。

解题思路:可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

142环形链表1​​

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

141.环形链表​​

如果有环,如何找到这个环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

​​

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

142.环形链表II(求入口)​​

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

代码

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            fast = fast->next->next;
            slow = slow->next;
            // 快慢指针相遇时,此时从head指针和快指针同时查找,直到相遇
            if (fast == slow) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                // 返回环的入口
                return index2;
            }
        }
        return NULL;
    }
};
  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度: O(1)

补充

在推理过程中,可能有一个疑问:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

142环形链表5​​

首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

142环形链表3​​

可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

142环形链表4​​

那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了

这说明什么呢?

在slow开始走的那一环已经和fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

链表总结

首先介绍了链表的基础知识:

  • 链表的种类主要为:单链表,双链表,循环链表
  • 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
  • 链表是如何进行增删改查的。
  • 数组和链表在不同场景下的性能分析。

虚拟头结点

链表操作中一个非常总要的技巧:虚拟头节点。

链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题

在【一、删除链表】此题中,给出了用虚拟头结点和没用虚拟头结点的代码,对比发现使用虚拟头结点的好处。

链表的基本操作

在【四、设计链表(增、删、查)】把链表常见的五个操作练习了一遍。

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点的数值

反转链表

在【二、反转链表】中,给出了两种反转的方式,迭代法和递归法。

建议先学透迭代法,然后再看递归法,因为递归法比较绕。可以先通过迭代法,彻底弄清楚链表反转的过程!

删除倒数第N个节点

在【五、删除链表倒数第n个节点】中结合虚拟头结点 和 双指针法来移除链表倒数第N个节点。

链表相交

在【六、链表相交】中使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)

环形链表

在【七、环形链表】中,讲解了在链表如何找环,以及如何找环的入口位置。

这道题目主要在于一些数学证明。

总结的思维导图

​​

这个图是海螺人所画。

题目链接总结

203. 移除链表元素

206. 反转链表

24. 两两交换链表中的节点

707. 设计链表

19. 删除链表的倒数第 N 个结点

面试题 02.07. 链表相交

142. 环形链表 II

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值