本篇博客并非提供完整解题思路代码,而是重点阐述在OJ的链表题目中容易被忽视的点,从而让部分读者在出错百思不得解的情况下能快速发现自己的漏洞,高效查缺补漏,本博客支持读者按题搜索,同时也支持读者根据博客内容自行完成题目,这都是比较经典且有坑的题目,还是比较支持读者通读全篇的。
目录
一、203.移除链表元素
1.1忽视头结点
1.2忽视cur->next仍指向原链表
1.3忽视tail为空链表
1.4通过代码
二、876.链表的中间结点
2.1忽略fast->next为NULL
2.2通过代码
三、链表中倒数第k个结点
3.1忽视k和链表长度的关系
3.2通过代码
四、CM11.链表分割
4.1什么是哨兵位
4.2忽略最后一个结点
4.3通过代码
五、206.反转链表
5.1三指针的初始位置错误
5.2通过代码
六、21.合并两个有序链表
6.1通过代码
七、141.环形链表
7.1忽视判断条件
7.2通过代码
八、142.环形链表 II
8.1通过代码
九、160.相交链表
9.1尾结点的条件判断错误
9.2通过代码
十、链表的回文结构
10.1通过代码
十一、138.随机链表的复制
11.1通过代码
一、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 输出:[]提示:
- 列表中的节点数目在范围
[0, 104]
内1 <= Node.val <= 50
0 <= val <= 50
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这个题目我们只提供一种思路,那就是遍历先前的链表,用新链表接收原链表不为 val 的结点。
struct ListNode* cur = head;
struct ListNode* newnode = NULL;
struct ListNode* tail = NULL;
以上就是我们的大致思路,我们来写一下代码:
1.1忽视头结点
其实这里是比较容易能想到的,当我们的Newnode为空时,我们要直接给Newnode赋值,而不是给Newnode的next赋值:
1.2忽视cur->next仍指向原链表
当我们完成上面的代码并返回Newnode时,我们可以发现除了第一个测试用例不对,剩下两个都对了,这又是为什么呢?
因为当我们的最后一个值与val相同时,我们看似不会把原链表带到新链表,但是我们忽略了cur是一个结构体,它的next一直没有改变,所以我们看似没有做什么操作但是已经把原链表最后的“6”带到了新链表中,这时应该怎么做呢?
我们重新审视我们的代码,可以发现我们当cur->val != val时进行了操作,但当cur->val == val时没有进行任何操作哦,现在我们可以free一下每一个值为val的结点,再把新链表中最后一个结点置空
1.3忽视tail为空链表
当我们测试上面的代码时,我们又会发现第二个测试用例出错:
这又是因为什么呢?其实当我们没有修改的时候,我们不存在tail->next = NULL ,但修改之后如果tail是NULL时,这就会出现野指针问题,所以在此之前我们只需要判断一下就可以啦。
1.4通过代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* newnode = NULL;
struct ListNode* tail = NULL;
while(cur != NULL)
{
if(cur->val != val)
{
if(newnode == NULL)
{
newnode = cur;
tail = newnode;
}
else
{
tail->next = cur;
tail = tail->next;
}
cur = cur->next;
}
else
{
struct ListNode* tmp = cur;
cur = cur->next;
free(tmp);
tmp = NULL;
}
}
if(tail != NULL)
{
tail->next = NULL;
}
return newnode;
}
二、876.链表的中间结点
给你单链表的头结点
head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。示例 2:
输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。提示:
- 链表的结点数范围是
[1, 100]
1 <= Node.val <= 100
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
本题我们提供双指针解法的忽视点,用fast和slow两个指针遍历链表,fast每次走两步,slow每次走一步,当fast走到头时,slow所在位置为链表中间结点。
所以我们要用fast = fast->next->next、slow = slow->next
2.1忽略fast->next为NULL
当我们的原链表结点为奇数个时,最后会存在fast->next为NULL的情况,这时fast->next->next就涉及了野指针的问题,我们可以在判断fast的同时判断fast->next是否为空。
2.2通过代码
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
三、链表中倒数第k个结点
描述
输入一个链表,输出该链表中倒数第k个结点。
示例1
输入:
1,{1,2,3,4,5}
返回值:
{5}
OJ题目链接:链表中倒数第k个结点_牛客题霸_牛客网
其是这题和我们上题用的方法如出一辙,都是通过两个指针的差值巧妙得出答案,上次我们用了两个指针的倍数关系,这次我们可以使用两个指针的固定距离。
同样定义一个fast指针,一个slow,那么如何让他们相差k个结点呢?
好啦,思路已经写到这里,下面我们一起来看看代码吧!
while(k>0)
{
fast = fast->next;
k--;
}
while(fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
3.1忽视k和链表长度的关系
本来以为天衣无缝的代码可还是报错了,明明自测运行的时候是正确的呀?下面我们来看看错误的测试用例:
我们其实不难看出,我们在写代码时并未想全面k的取值,如果k比链表的长度还要大怎么办?
我们应该如何判断呢?
我们如果像这样进行判断,那么有一种情况会被我们忽略,那就是 k == 链表长度时,我们无法返回整个链表,而是返回NULL。
那这样怎么避免呢?我们可以把判断的代码提到循环体的最上面:
简单的一个操作,就可以让我们的题目通过。
3.2通过代码
四、CM11.链表分割
描述
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
OJ题目链接:链表分割_牛客题霸_牛客网
我们的思路是创建两个新链表,让他们分别存储小于x和另外的值,最后再链接两个链表。
4.1什么是哨兵位
我们在做这题之前要了解什么是哨兵位,因为这题如果不设置哨兵位会多很多麻烦。
在我们之前做的题中我们不难发现,当要传入第一个结点时,我们都要判断头结点是否为空,以此来确定是给头结点赋值还是给头结点的next赋值,这就是不带哨兵位的链表,哨兵位其实就是我们动态开辟了一块空间给头结点,这样头结点永远都不会为空。
既然头结点永远不会为空,那我们在传值的时候完全可以不用考虑,直接传给phead->next,需要注意的是,在需要返回值时,我们不可以单一的返回phead,而是返回phead->next,另外因为还需要free掉malloc的空间,所以我们得出答案后最好还要用原链表的头结点指向我们的答案,以此来free掉malloc开辟的空间。
4.2忽略最后一个结点
其实这和我们1.2出现的错误一样,都是忘记了我们倒数第二个结点的next其实还是指向原链表的,当最后一个结点不满足我们的条件时,倒数第二个结点还是会指向它,所以我们最好的办法就是像1.2那样,将最后一个可能的结点直接置空。
记得要free掉malloc开辟的空间哦~
4.3通过代码
class Partition
{
public:
ListNode* partition(ListNode* pHead, int x)
{
ListNode* newHead1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* newHead2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* tail1 = newHead1;
ListNode* tail2 = newHead2;
ListNode* cur = pHead;
while(cur != NULL)
{
if(cur->val < x)
{
tail1->next = cur;
tail1 = tail1->next;
}
else
{
tail2->next = cur;
tail2 = tail2->next;
}
cur = cur->next;
}
tail1->next = newHead2->next;
tail2->next = NULL;
pHead = newHead1->next;
free(newHead1);
free(newHead2);
return pHead;
}
};
五、206.反转链表
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]示例 2:
输入:head = [1,2] 输出:[2,1]示例 3:
输入:head = [] 输出:[]提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这题的思路我们可以采用三指针来做,一个指针记录原链表的位置,一个指针记录前一个结点的位置,另一个指针负责遍历整个链表并改变每个结点的指向。
5.1三指针的初始位置错误
我们根据我们的思路写出以上代码,可我们还是通不过, 因为我们知道链表的最后一个结点肯定是NULL,如果按照我们的思路,显然我们没有指向NULL的结点,所以我们要把我们三个指针的位置前移一个结点:
5.2通过代码
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL)
{
return NULL;
}
else
{
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next = cur->next;
while(cur != NULL)
{
cur->next = prev;
prev = cur;
head = cur;
cur = next;
if(next != NULL)
{
next = next->next;
}
}
return head;
}
}
六、21.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]示例 2:
输入:l1 = [], l2 = [] 输出:[]示例 3:
输入:l1 = [], l2 = [0] 输出:[0]提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这题其实没什么坑,只要注意多加判断list1和list2为空时的情况就可以啦,我们一起来看通过代码
6.1通过代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
{
return list2;
}
else if(list2 == NULL)
{
return list1;
}
else
{
struct ListNode* NewNode = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail1 = list1;
struct ListNode* tail2 = list2;
struct ListNode* cur = NewNode;
while(tail1 != NULL && tail2 != NULL)
{
if(tail1->val < tail2->val)
{
cur->next = tail1;
tail1 = tail1->next;
}
else
{
cur->next = tail2;
tail2 = tail2->next;
}
cur = cur->next;
}
if(tail1 == NULL)
{
cur->next = tail2;
}
else
{
cur->next = tail1;
}
list1 = NewNode->next;
free(NewNode);
return list1;
}
}
七、141.环形链表
给你一个链表的头节点
head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回
true
。 否则,返回false
。示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。示例 2:
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。示例 3:
输入:head = [1], pos = -1 输出:false 解释:链表中没有环。提示:
- 链表中节点的数目范围是
[0, 104]
-105 <= Node.val <= 105
pos
为-1
或者链表中的一个 有效索引 。
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这题我们的思路是快慢指针,让快指针的速度是慢指针的二倍,如果这个链表有环,那么他们一定会在环内相遇。
7.1忽视判断条件
根据上面的思路,我们很快就能写出代码,可是我们会得到一个执行出错:
原因是什么呢?我们是要让fast一次走两步,可是当fast->next为NULL时,就会产生野指针的问题,我们只需要将fast->next也列入循环的判断条件就可以啦。
7.2通过代码
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow = head;
struct ListNode*fast = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
八、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 解释:链表中没有环。提示:
- 链表中节点的数目范围在范围
[0, 104]
内-105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
因为快指针走的距离是慢指针走的距离的二倍,所以我们能得到:L + nC - X = 2( L + C - X )
化简得: X = L + ( n - 1 ) * C
我们再对照一下图就可以得到,如果一个指针从相遇点开始走,一个指针从头开始走,那他们相遇的位置正好会是入环点!
8.1通过代码
感觉这题并没有什么坑,只是推导会有一些难受,下面我们直接来看通过代码:
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* slow = head;
struct ListNode*fast = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
struct ListNode* tail1 = fast;
struct ListNode* tail2 = head;
while(tail1 != tail2)
{
tail1 = tail1->next;
tail2 = tail2->next;
}
return tail1;
}
}
return NULL;
}
九、160.相交链表
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。图示两个链表在节点
c1
开始相交:题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数评测系统将根据这些输入创建链式数据结构,并将两个头节点
headA
和headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [1,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 。
提示:
listA
中节点数目为m
listB
中节点数目为n
1 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA] == listB[skipB]
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
我们的思路还是使用双指针,可是如何控制两个指针的差结点呢?
通过对比我们可以知道,我们要得出的是相交结点之前的差值,我们又可以观察出其实相交结点之前的差值就是两链表的差值,这样我们就可以先分别统计两链表的长度,并且可以进一步判断两链表尾结点是否相同来先一步确认是否相交。
再让一个指针先走差值后,启动另一个指针,当他们相同时,就是两链表的交点。
9.1尾结点的条件判断错误
我们在上面已经说了我们可以顺便判断尾结点是否相同,那就说明了我们不能再将 tailA 和 tailB 作为循环进行的条件了,因为最后都是NULL,我们无法判断两链表尾结点是否相同,所以在这里我们要将 tail->next 作为循环进行的条件。
9.2通过代码
下面我们一起来看通过代码:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* tailA = headA;
int LenA = 1;
struct ListNode* tailB = headB;
int LenB = 1;
while(tailA->next != NULL)
{
tailA = tailA->next;
LenA++;
}
while(tailB->next != NULL)
{
tailB = tailB->next;
LenB++;
}
if(tailA != tailB)
{
return NULL;
}
int count = abs(LenA - LenB);//求绝对值的函数
struct ListNode* longlist = headA;
struct ListNode* shortlist = headB;
if(LenB > LenA)
{
longlist = headB;
shortlist = headA;
}
while (count--)
{
longlist = longlist->next;
}
while (longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
十、链表的回文结构
描述
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
测试样例:
OJ题目链接:链表的回文结构_牛客题霸_牛客网
我们可以将链表的后半段逆置,所以这题既用到了寻找中间结点,也用到了逆置链表的方法,我们只需要祭出我们的CV大法就可以轻松解决。
下面我们直接来看代码:
10.1通过代码
class PalindromeList {
public:
//ctrl+v逆置链表的代码
ListNode* reverseList(struct ListNode* head)
{
if(head == NULL)
{
return NULL;
}
else
{
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next = cur->next;
while(cur != NULL)
{
cur->next = prev;
prev = cur;
head = cur;
cur = next;
if(next != NULL)
{
next = next->next;
}
}
return head;
}
}
//ctrl+v寻找中间结点的代码
ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
bool chkPalindrome(ListNode* A)
{
// write code here
ListNode* mid = middleNode(A);
ListNode* rA = reverseList(A);
while(A !=NULL && rA != NULL)
{
if(A->val != rA->val)
{
return false;
}
A = A->next;
rA = rA->next;
}
return true;
}
};
十一、138.随机链表的复制
给你一个长度为
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:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]示例 2:
输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]]示例 3:
输入:head = [[3,null],[3,0],[3,null]] 输出:[[3,null],[3,0],[3,null]]提示:
0 <= n <= 1000
-104 <= Node.val <= 104
Node.random
为null
或指向链表中的节点。
OJ题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
就目前来说,这题最好的方法就是先复制结点,把每个结点都对应放在原链表结点的后面,当我们要复制random时,其对应的就是原链表random指向结点的下一个结点。
如图,这样插入后我们再进行random的寻找就不用再遍历整个链表了,时间复杂度从O(N^2)简化到了O(N),在写代码过程中,要进行random指向的判断,有可能为NULL,也有可能是它本身。
11.1通过代码
struct Node* copyRandomList(struct Node* head)
{
struct Node* cur = head;
while(cur != NULL)
{
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
cur = head;
while(cur != NULL)
{
struct Node* copy = cur->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = copy->next;
}
struct Node* newhead = NULL;
struct Node* tail = NULL;
cur = head;
while(cur != NULL)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
if(tail == NULL)
{
newhead = tail = copy;
}
else
{
tail->next = copy;
tail = tail->next;
}
cur->next = next;
cur = next;
}
return newhead;
}