24.两两交换链表中的节点
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]示例 2:
输入:head = [] 输出:[]示例 3:
输入:head = [1] 输出:[1]提示:
- 链表中节点的数目在范围
[0, 100]
内0 <= Node.val <= 100
思路:
通过改变结点指针的指向来实现链表的结点两两交换的过程
使用虚拟头结点的来进行实现,便于改变结点指向时查找到前驱点
实现:
1.定义一个虚拟头结点,让其指向真正链表的头结点
2.定义一个指针cur,指向要交换的两个结点的前一个结点,初始化为虚拟头结点
3.遍历链表进行交换,循环条件是cur的下两个结点都不为空,即cur->next和cur->next->next都不为空,此时两个顺序也不能交换,如果先写cur->next->next不为空,当cur->next为空,那么此时就会对空指针进行操作,编译器就会出错
3.进行结点交换的操作,先保存cur的后第一个结点的信息,让cur->next指向其后面的第二个结点,再保存该结点的后继结点,再让该结点指向cur后的第一个结点,最后让该结点指向保存的第二个结点后继结点,此时两个结点交换完成
4.移动cur指针,依次交换后序的结点
代码实现:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* swapPairs(struct ListNode* head) {
//创建一个虚拟头结点
struct ListNode *nhead = (struct ListNode*)malloc(sizeof(struct ListNode));
nhead->next = head;
//创造循环变量指针
struct ListNode *cur = nhead;
//循环交换结点
while(cur->next && cur->next->next)
{
//定义两个临时变量,记录交换之后可能会丢失的结点
//记录要交换的第一个结点的位置
struct ListNode *loca1 = cur->next;
//记录要交换的第二个结点的后一个结点
struct ListNode *loca2 = cur->next->next->next;
//进行结点交换
cur->next = cur->next->next; // 虚拟的头结点指向第二个结点(第一次交换)
cur->next->next = loca1; //第二个结点指向第一个结点
loca1->next = loca2; // 第一个结点指向第二个结点的后继结点
cur = cur->next->next; //一次会操作两个结点,因此需要移动两位
}
return nhead->next;
}
易错点:
1.要注意cur指针的指向,如果要交换第一个结点和第二个结点的位置,那么cur的位置必须在虚拟头结点,否则就导致第一个结点的前驱点找不到,无法将虚拟头结点指向第二个结点,即cur指针的位置必须的要操作的两个结点的前面,因此也要注意操作结点时的边界条件。
2.在交换两个结点的指向时,如果没有保存有第一个要交换结点的位置,以及第二个要交换结点后继结点的位置,会导致结点信息的丢失,导致结点连接错误。
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]提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
思路:
使用虚拟头结点和使用快慢指针的方法,这样便于能够快速查找到要删除结点的前驱点
实现:
1.定义一个虚拟头结点,让该结点指向头结点
2.定义两个指针,分别为快指针和慢指针,让快指针先走n+1步,然后快慢指针一起走,当快指针到达空的位置时,慢指针所在在位置就是我们要删除结点的前驱点
3.将该结点从链表中删除,记得手动去释放该结点所占用的内存空间
问题:为什么要让快指针走先走n+1步而不是n步呢
答:因为快指针如果先走n步,后序快慢指针一起移动,那么当快指针恰好指向为空时,那么慢指针的位置恰好就是我们要删除的倒数第n个结点,这样我们就难以查找到该结点的前驱点,因此让快指针先走n步,后序一起走,当快指针停下时,慢指针刚好就为前驱点,便于我们去删除结点。
代码实现:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
//定义一个虚拟头结点,让其指向头结点
struct ListNode *nhead = (struct ListNode*)malloc(sizeof(struct ListNode));
nhead->next = head;
//定义快慢指针
struct ListNode *fast = nhead;
struct ListNode *slow = nhead;
//快指针先走n+1步
for(register int i = 0 ; i <= n ; i++)
fast = fast->next;
//快慢指针一起走,指定快指针为空
while(fast)
{
fast = fast->next;
slow = slow->next;
}
//删除倒数第n个结点
struct ListNode *del = slow->next;//记录要删除的结点,需要释放内存
slow->next = slow->next->next;
free(del);
//删除虚拟头结点
head = nhead->next;
free(nhead);
return head;
}
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]
思路:
求两个链表相交的结点,注意,两个结点相等的是指针而不是值
实现:
1.先分别求出两个链表的长度
2.求出两个链表的长度差
3.让链表尾部对其,移动指针,查找是否有相同的元素
代码实现:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//定义指针来遍历链表,并计算出链表长度
struct ListNode *lg = NULL; //记录较长的链表
struct ListNode *shr = NULL;//记录较短的链表
int lenA = 0 ,lenB = 0; //记录两个链表的长度
lg = headA;
while(lg) //计算headA的长度
{
lg = lg->next;
lenA++;
}
lg = headB;
while(lg)//计算headB的长度
{
lg = lg->next;
lenB++;
}
//求两个链表的长度差
int add = 0;
if(lenA > lenB)
{
lg = headA;
shr = headB;
add = lenA - lenB;
}
else
{
lg = headB;
shr = headA;
add = lenB-lenA;
}
//进行尾部对其
while(add--)
lg = lg->next;
//查找是否有相同的元素
while(lg)
{ //查找到交叉的结点
if(lg == shr)
return lg;
//没有查找到
lg = lg->next;
shr = shr->next;
}
//只要程序没有退出,则证明没有交叉的结点,返回为空
return NULL;
}
142.环形链表
题目描述:
给定一个链表的头节点
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
或者链表中的一个有效索引
思路:
判断链表是否有环:
使用快慢指针的方法来进行判断,如果链表有环,那么链表一定会相遇,因为快指针进入链表的环后,会在环里一直循环,当慢指针进入链表的环时,快慢指针一定会在某一个时刻在环里相遇,如果链表是一条直线,那么快慢指针不可能相遇,因为快慢指针移动的速度不同。
如何找到链表环的入口:
假如链表有环,则可以设链表头到环的入口的距离为x个结点,链表环入口到快慢指针相遇的距离设为y个结点,快慢指针相遇到另一个方向环出口的距离为y个结点。
如果快慢指针相遇,则慢指针走了x+y个结点,而快指针走了x+y+n(y+z),n为快慢指针相遇时,快指针在环里移动的圈数
实现:
判断链表是否有环:
定义快慢指针,都让他们指向链表的头,让快指针一次移动两个结点,慢指针移动一个结点,如果快慢指针都进入到链表的环中,那么相当于快指针一次以移动一个结点的速度去追赶慢指针,因此两个指针一定会相遇
找到环的出入口:
由于快指针的速度是慢指针的两倍,因此可以写出
2(x+y) = x+y+n(y+z),可求得x = n(y+z)-y; n一定大于等于1,即快指针至少要走一圈
如果去除一圈的距离可得 x = (n -1 )(y+z)+z; 假如n为1,则 x=z,此时代表着慢指针刚好到环入口时,快指针恰好到环的出口处,因此根据x = (n -1 )(y+z)+z可以得出,当慢指针到达环入口时,快指针在环里面转了n圈恰好位于环出口位置,因此
可以在相遇的位置定义一个指针index1,在起始位置定义一个Index2,两个指针以相同的速度去移动,此时两者相遇的位置就是环的入口处
代码实现:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
//定义快慢指针,都指向链表头
struct ListNode *fast = head;
struct ListNode *slow = head;
//让快指针一次移动两格,慢指针一次移动一格
//快指针一次移动两个结点
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
//如果快慢指针相遇,说明找到了环
if(fast == slow)
{
//定义一个指针记录相遇的位置
struct ListNode *index1 = fast;
//再定义一个指针指向链表头,让他们同时移动
struct ListNode *index2 = head;
//当这两个指针相遇,代表找到了环的入口
while(index1 != index2)
{
index1 = index1->next;
index2 = index2->next;
}
//如果退出循环,则返回环的入口位置
return index1;
}
}
//证明没有找到环
return NULL;
}
总结:
在此次对链表的算法练习题中,能够更加熟练的去查找到某一个节点的前驱点,能够注意到了记录链表结点的信息,防止在对链表结点进行操作时,造成链表结点的丢失,更深刻的理解了虚拟头结点和双指针的用法,但对于环形链表的数学推导不熟悉,需要多加练习