本文主要记录了自己跟着代码随想录通过leetcode上与链表有关的题学习链表知识时整理的笔记。首先学习链表的基础知识,比如链表的定义、类型、存储方式、定义的代码等。然后对链表的基本操作进行简单的介绍,比如链表删除节点、添加节点。同时,分析对比了链表和数组的性能。此外,通过leetcode上链表的经典题目的分析和实现来掌握链表的具体使用,例如删除、增加、查找、两两交换链表的节点、反转链表、判断两个链表是否相交,相交则求出相交的节点、判断链表是否有环,有环则求出环的入口等等。最后,对本文的知识点进行了总结,并附上总结的流程图和本文涉及的所有题目链接地址。
目录
链表基础知识
定义:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
如图所示:
链表的类型
链表主要有单链表、双链表和循环链表三种。
- 单链表
刚刚说的就是单链表。
注意:单链表中的指针域只能指向节点的下一个节点。
- 双链表
定义:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
优点:既可以向前查询也可以向后查询。
如图所示:
- 循环链表
定义:首尾相连的链表。
链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如图所示:
这个链表起始节点为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)直接使用原来的链表来进行移除。
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。需要单独写一段逻辑来处理移除头结点的情况。
(2)设置一个虚拟头结点在进行移除
可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
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指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
之前链表的头节点是元素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. 设计链表
题目描述:你可以选择使用单链表或者双链表,设计并实现自己的链表。单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,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. 链表相交
题目描述:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 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指针。
会发现最终都是这种情况, 如下图:
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
动画如下:
如果有环,如何找到这个环的入口
假设从头结点到环形入口节点 的节点数为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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
那么 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 呢?
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么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个节点。
链表相交
在【六、链表相交】中使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)
环形链表
在【七、环形链表】中,讲解了在链表如何找环,以及如何找环的入口位置。
这道题目主要在于一些数学证明。
总结的思维导图
这个图是海螺人所画。