链表基础练习¶
约 6931 个字 1697 行代码 18 张图片 预计阅读时间 44 分钟
本篇介绍¶
前面了解了链表的基础结构,下面通过基础的练习巩固前面学习到的基础知识。链表处的题目没有特别显著的统一解法,大部分都是对链表结构的增删改,所以通过题目具体分析链表的题目类型
力扣203.移除链表元素¶
问题描述:
Quote
给你一个链表的头节点head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
-
解法1:原地处理 从题目就可以看出,本题涉及链表结构的数据删除,需要注意,力扣中
head
不是头节点,而是第一个有效数据节点,所以此处需要考虑两种情况:- 删除的是第一个有效数据节点
- 删除的是非第一个有效数据节点
但是为了可以实现统一处理,这里引入一个虚拟头结点
dummy
,使其指向第一个有效数据节点,这个做法在后面涉及到与链表的数据删除或者更改时非常常见。这样做可以保证第一个有效数据节点的删除逻辑和非第一个有效数据节点一致。剩余的逻辑就是链表的元素删除了,力扣中的链表大部分默认都是单链表,具体可以参考单链表数据结构头删部分和单链表数据结构尾删部分 -
解法2:异地处理
异地处理就不是删除的逻辑,而是尾插的逻辑,具体可见单链表数据结构尾插,为了方便插入,这里同样需要引入一个虚拟头结点
dummy
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
力扣707.设计链表¶
问题描述:
Quote
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性: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
的节点。
示例:
输入
C++ | |
---|---|
1 2 |
|
输出
C++ | |
---|---|
1 |
|
解释
C++ | |
---|---|
1 2 3 4 5 6 7 |
|
思路分析:
本题就是根据对链表结构的理解设计一个自己的链表结构,既可以是单链表也可以双链表,但是需要注意,本题get()
、addAtIndex()
和deleteAtIndex()
三个函数需要用到下标,但是链表本身没有下标,所以需要考虑用循环依次枚举下标直到其值等于所给下标为止,其余思路与链表数据结构相关操作基本一致
需要注意,力扣本身有一个单链表节点结构ListNode
结构,结构如下:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
但是对于双向链表节点结构则需要自行设计,根据题目要求设计出数据域和指针域即可
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
|
力扣206.反转链表¶
问题描述:
Quote
给你单链表的头节点head
,请你反转链表,并返回反转后的链表。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
-
解法1:异地处理
对于异地处理来说,反转链表本质就是第一个元素作为最后一个元素,第二个元素作为倒数第二个元素,第三个元素作为倒数第三个元素……,所以可以考虑使用头插的方式解决本题。遍历原链表,此处可以考虑为对每一个节点进行深拷贝,将对应的值通过新节点头插到新链表,保证每个节点的链接不会出现问题
-
解法2:原地处理
对于原地处理来说,必须处理好每一个节点的链接和断开。原地处理的具体操作是:因为是单链表,所以处理记录当前节点
cur
,还需要记录前一个节点prev
,使用一个临时变量temp
记录当前节点的下一个节点地址,防止接着断开链接时找不到下一个节点,再将当前节点的指针指向前一个节点,此时就完成一次一个节点的指针反向,让prev
走到cur
的位置,接着再让cur
走到temp
的位置重复上面的步骤即可需要注意的是,
prev
指针的起始位置不同,处理方式也不相同
写法进阶:
上面的两个方式最常见的写法就是迭代,但是对于第二个方法来说,还可以使用递归来解决,建议先用迭代实现,再根据迭代写出递归的代码,也可以参考算法:递归中的递归思路
下面是根据循环转递归写法的思路:
在循环中需要使用到两个指针,分别是cur
表示当前节点,prev
表示上一个节点,为了保证翻转后的链表的最后一个节点指向空,需要将prev
初始化为nullptr
,因为要返回翻转后的链表的头节点,所以函数头设计为ListNode* _reverseList(ListNode* cur, ListNode* prev)
,接着考虑函数体,根据循环的思路,先记录当前节点的下一个节点,防止反转后找不到下一个节点,再将当前节点的next
指向当前节点的前一个节点,这样就完成了两个节点的翻转,接着就是翻转后面的节点,翻转后面的节点的逻辑和翻转当前两个节点的逻辑相同,所以再次调用函数完成,最后考虑递归函数的结束条件,因为要返回头结点,所以当前cur
走到空的时候,prev
指向的就是翻转后的链表的头结点,返回prev
即可
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
力扣876.链表的中间结点¶
问题分析:
Quote
给你单链表的头结点head
,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 |
|
思路分析:
本题考虑使用快慢指针法,快指针一次走两步,慢指针一次走一步,最后返回慢指针即可,注意循环条件是fast && fast->next
,而不是fast->next && fast
,因为对于偶数个节点的链表来说,在走到了fast
需要走到空才停止,而奇数个节点的链表走到next
节点为空停止即可,如果是第二种,那么当链表为偶数个节点的链表,当slow
走到第二个中间节点时,fast
已经在空的位置,此时对于循环条件fast->next && fast
来说,会先判断fast->next
,由于此时fast == NULL
,故此时会出现空指针解引用错误
参考代码:
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
力扣19.删除链表的倒数第N个结点¶
问题描述:
Quote
给你一个链表,删除链表的倒数第n
个结点,并且返回链表的头结点。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
-
计算链表长度反向找到倒数第
n
个节点假设链表长度为
size
,倒数第n
个节点代表前面有size-n
个节点,假设现在第一个节点的下标为0,则size-n
刚好就是倒数第n
个节点的下标,利用这个结合前面设计链表题目中的下标思路即可解决本题 -
快慢指针
定义
fast
指针和slow
指针分别表示快指针和慢指针,倒数第n
个节点,说明该节点距离最后一个节点相差n-1
节点,所以先让fast
指针走n步,保证在slow
和fast
同时走时,fast
到链表结尾,slow
和fast
相差n-1
个节点。为了便于删除,依旧是引入虚拟头节点指向链表的第一个节点,并且因为在单链表中删除节点时,获取到待删除节点的前一个节点会更加方便,所以让slow
和fast
在一开始就相差1
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
牛客网.倒数第k个节点¶
Note
本题牛客网已下线,所以没有具体的题目链接,但是思路与力扣19题一致,只提供C语言版本
问题描述:
Quote
输入一个链表,输出该链表中倒数第k
个结点
参考代码:
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
力扣21.合并两个有序链表¶
问题描述:
Quote
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
本题可以考虑使用双指针的思路,但是这两个指针是分别在两个链表上而不是在同一个链表上,假设这两个指针分别为curA
和curB
,代表的就是当前节点。接下来就是比较逻辑:
curA > curB
:因为要保证链表升序,所以curB
节点链接到新链表上,向后移动curB
,但是不要移动curA
,因为curA
较大,不能保证下一次的curB
一定比curA
大,所以当前的curA
还需要继续和curB
比较直到其满足curA < curB
curA <= curB
:与上面的过程刚好相反
本题可以直接将原链表的节点链接到新链表上,也可以考虑通过当前值创建新节点,再将新节点链接到新链表上
写法优化:
本题可以结合上面的写法写出迭代版本,也可以使用递归写法,具体见算法:常规题目中的递归
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
力扣面试题02.04.分割链表¶
问题描述:
Quote
给你一个链表的头节点head
和一个特定值x
,请你对链表进行分隔,使得所有小于x
的节点都出现在大于或等于x
的节点之前。
你不需要保留每个分区中各节点的初始相对位置。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
思路分析:
根据题目的描述「所有小于x
的节点都出现在大于或等于x
的节点之前」,可以考虑小于x
的插入到大于等于x
的前面,大于等于x
的直接尾插,所以需要两个指针指向新链表中的节点,一个指针指向最后一个小于x
的节点,便于接下来插入新的小于x
的节点,另一个指针指向最后一个大于等于x
的节点,便于尾插下一个大于等于x
的节点
因为涉及到链表的尾插,所以可以考虑使用一个虚拟头结点dummy
便于分析
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
力扣234.回文链表¶
问题描述:
Quote
给你一个单链表的头节点head
,请你判断该链表是否为回文(回文序列是向前和向后读都相同的序列)链表。如果是,返回true
;否则,返回false
。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
思路分析:
本题的思路很简单,找到链表的中间结点,再根据中间节点将原始链表分割成两部分,同时比较这两部分的值是否相同,如果完全相同就是回文链表,否则就不是。对于找链表的中间节点需要考虑到两种情况:
- 当链表的元素个数是偶数,此时慢指针指向的第二个中间节点,在回文链表中,该节点作为第二部分链表的最后一个节点
- 当链表的元素个数是奇数,此时慢指针指向的中间结点,在回文链表中,该节点的下一个位置才是第二部分链表的最后一个节点
所以在分割链表前要确定链表的长度,判断其元素是奇数还是偶数,判断出中间节点后进行分割链表,剩余的思路就是链表结构的基础操作思路
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
力扣24.两两交换链表中的节点¶
问题描述:
Quote
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
-
解法1:迭代
根据题目的描述以及第一个样例可以看出「两两交换」的意思是「没有进行过交换两个的节点进行相互转换」,此外还有一个言外之意「如果只有一个节点则不进行交换」,理解题意后就可以考虑思路:让当前节点的前一个节点指向当前节点的后一个节点,再让当前节点的指针指向前一个节点即可实现两个节点交换,但是需要注意,如果节点不够两个时不需要交换,而这个情况只会出现在最后一次交换,直接退出交换过程即可
-
解法2:递归
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
力扣160.相交链表¶
Note
本题同力扣面试题02.07.链表相交
问题描述:
Quote
给你两个单链表的头节点headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。
图示两个链表在节点c1
开始相交:
题目数据保证整个链式结构中不存在环。
注意,函数返回结果后,链表必须保持其原始结构。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为 0 listA
- 第一个链表 listB
- 第二个链表 skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数 skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数 评测系统将根据这些输入创建链式数据结构,并将两个头节点headA
和headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被视作正确答案。
示例 1:
C++ | |
---|---|
1 2 3 4 5 6 |
|
示例 2:
C++ | |
---|---|
1 2 3 4 5 |
|
示例 3:
C++ | |
---|---|
1 2 3 4 5 |
|
思路分析:
本题实际上有两个问题,第一个问题:两个链表是否相交,第二个问题:两个链表相交时相交的节点是哪一个
对于第一个问题,两个链表如果相交,说明两个链表的最后一个节点一定相等(即为同一个节点,注意不是值相等,而是节点相等),通过判断两个链表的最后一个节点是否相等即可判断两个链表是否相交
对于第二个问题,如何找相交节点,回忆数学中的相遇问题,相遇的本质是走到了同一个位置,那么对于链表来说,即为走到了同一个节点,而因为两个链表相交前的节点个数是不一定相同的,所以在前面判断是否相交时,可以同时计算两个链表的长度,而如果一个链表长,那么多出来的节点个数即为长链表中的多余部分,先让长链表遍历多出来的个数次,再同时遍历长链表和短链表,如果二者遇到了相同的节点,则说明这个节点时相交的节点
示意图如下:
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
力扣141.环形链表¶
问题描述:
Quote
给你一个链表的头节点head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next
指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从0开始)。注意:pos
不作为参数进行传递。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回true
。 否则,返回false
。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 |
|
示例 3:
C++ | |
---|---|
1 2 3 |
|
思路分析:
本题可以利用Floyd判圈算法,具体思路如下:
使用快慢指针,快指针每次走两步,慢指针每次走一步,当快指针和慢指针都没有进环,并且当快指针走到空时如果二者依旧没有相遇,那么说明链表没有环,而当快指针进环时,慢指针可能没有进环,此时也不可能相遇,所以当两个指针都在环内时才会相遇,而如果二者相遇,说明二者指向的节点相同,此时即有环
Floyd判圈算法
Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm),是一个可以在有限状态机、迭代函数或者链表上判断是否存在环,求出该环的起点与长度的算法。该算法据高德纳称由美国科学家罗伯特·弗洛伊德发明,但这一算法并没有出现在罗伯特·弗洛伊德公开发表的著作中
如果有限状态机、迭代函数或者链表上存在环,那么在某个环上以不同速度前进的2个指针必定会在某个时刻相遇。同时显然地,如果从同一个起点(即使这个起点不在某个环上)同时开始以不同速度前进的2个指针最终相遇,那么可以判定存在一个环,且可以求出2者相遇处所在的环的起点与长度
思考问题:
-
slow
和fast
为什么走两步会相遇slow
和fast
走两步会相遇是因为slow
和fast
之间的步数差距为1(每次跳过一个节点),也就是说,两个指针的距离(单位长度)每次都会减少1,例如,假设slow
指针当前在第五个节点,fast
指针在第一个节点,两个指针相差4个单位长度(3个节点),当slow
走一步时,slow
到达第六个节点,而fast
此时走两步,fast
到达第三个节点,两个指针相差3个单位长度,当slow
再走一步时,slow
到达第七个节点,而fast
此时走到第五个节点,两个指着相差2个单位长度,而当slow
走到第八个节点时,此时fast
走到第七个节点,两个节点相差1个单位长度,当slow
走到第九个节点时,fast
此时也走到第九个节点,此时二者相遇,而推广到有n
个节点,每次slow
走一步,fast
走两步,二者之间的单位长度个数减少1,即距离减少1,最后终将会相遇 -
如果
fast
走2步以上的步数是否会出现相遇,如果会什么时候相遇,什么时候不相遇对于第二个问题,如果
fast
走两步以上的步数,则fast
和slow
之间的距离差值每次减少二者之间的步数差值,如果fast
和slow
本身相差偶数个节点,并且二者之间的步数差值为2,那么会相遇,如果fast
和slow
相差奇数个单位长度时,此时到当slow
到最后一个节点时,fast
比slow
多走两步,走到了slow
的下一个节点的下一个节点,则二者错过相遇点,同理可得更多的步数差值,而因为二者步数相差1时,每次距离减少1,无论二者之间的距离差值为奇数还是偶数都会相遇
参考代码:
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
力扣142.环形链表Ⅱ¶
问题描述:
Quote
给定一个链表的头节点head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。
如果链表中有某个节点,可以通过连续跟踪next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从0开始)。如果pos
是-1,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改链表。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 |
|
示例 3:
C++ | |
---|---|
1 2 3 |
|
思路分析:
-
解法1:双指针解法 本题是前一个题目的变型版本,前面一题只需要判断是否有环,本题是在判断有环的基础上找到环的开始节点,所以本题一共两个问题,依旧可以使用Floyd判圈算法找到相遇位置,因为慢指针每次走一步,快指针每次走两步,所以快指针走过的路程即为慢指针的两倍,如果二者从相遇点开始起步,则有:
假设链表环前的距离为L,slow进环后的位置与环的起点的位置的距离为X,环的周长为C
那么对于慢指针来说,其走过的距离为L+X,因为快指针每次走两步,那么
slow
和fast
相遇之前,fast
至少已经走了一圈,因为fast
在slow
的前面,如果没有走一圈,不可能回到slow
的位置,那么快指针走过的距离为L+nC+X(先走没有环的部分,再走一定数量的圈数,再从起点走X距离追上slow
),故有等式L+X+nC = 2*(L+X),化简可得nC = L + X,故L = nC-X = (n - 1)C + C - X(先走n-1圈,再走去掉X的部分回到进环点),极限情况下,n = 1,则有L = C - X,即此时fast
从相遇点开始走,走了C-X的距离刚好走到进环点,此时即为入环的第一个节点 -
解法2:哈希表
结合set后可以考虑思路:所谓链表是否有环,即是否有重复的节点,而第一个重复的节点即为环的入口点,如果不成环则会一直走到空为止。那么利用set可以考虑将每一个节点插入到set中,因为set在插入时会保证数据不冗余,所以插入失败的第一个节点即为成环,也为环的入口点
参考代码:
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
力扣138.随机链表的复制¶
问题描述:
Quote
给你一个长度为n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有X
和Y
两个节点,其中X.random --> Y
。那么在复制链表中对应的两个节点x
和y
,同样有x.random --> y
。
返回复制链表的头节点。
用一个由n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]
表示:
val
:一个表示Node.val
的整数。 random_index
:随机指针指向的节点索引(范围从0到n-1
);如果不指向任何节点,则为null
。 你的代码只接受原链表的头节点head
作为传入参数。
示例 1:
C++ | |
---|---|
1 2 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
本题可以将原链表的每一个节点都进行一次拷贝,然后插入到对应的原节点的后面,接下来分三步走:
- 拷贝链表节点并将对应的节点链接到原链表对应节点之后
- 拷贝
random
节点 - 断开新链表和原链表的链接
定义两个指针copyHead
和copyTail
分别指向由新的节点组成的链表,根据尾插的思想一步一步先进行拷贝,再处理random
,对于此题来说,当原链表节点的random
为空时,拷贝链表的random
也为空,当原链表的random
不为空,此时观察规律可得copyTail->random = cur->random->next
例如,对于拷贝链表的第二个节点来说,其random
应该指向拷贝链表的第一个节点(注意不能只比较值,因为值可能重复,但是random
链接的顺序只有一个),而该第一个节点时原链表cur->random
指向的第一个节点的next
的节点),最后断开原链表和拷贝链表的相互链接,使二者变为两个链表,返回拷贝的链表即可
示意图如下:
优化思路:
在上面的思路中,使用到了一步「拷贝链表节点并将对应的节点链接到原链表对应节点之后」,这一步本质就是为了让拷贝后的新节点的random
更容易找到其指向的节点,但是这一步需要考虑好链接以及后面的断开链接,过程有点繁琐且细节颇多,可以考虑使用map建立原节点和拷贝节点映射,这样就可以做到根据原节点找到拷贝节点
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
牛客网.环形链表的约瑟夫问题¶
问题描述:
Quote
描述 编号为1到n
的 n 个人围成一圈。从编号为1的人开始报数,报到m
的人离开。
下一个人继续从 1 开始报数。
n-1
轮结束以后,只剩下一个人,问最后留下的这个人编号是多少?
数据范围:
\(1 \le n, m \le 10000\)
示例1:
C++ | |
---|---|
1 2 3 4 5 6 7 8 |
|
示例2:
C++ | |
---|---|
1 2 3 4 |
|
思路分析:
本题可以考虑使用单向循环链表来实现,也可以考虑使用双向链表,本题关键就是当计数器等于报数值m
时,就删除节点直到最后只剩一个节点时结束
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|