链表相关特点:无法高效获取长度,无法根据偏移快速访问元素。
根据链表的特点衍生出一系列问题:判断环的长度等和长度与位置有关的问题,获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环。这些问题都可以通过灵活运用双指针来解决。
一、判断链表是否有环
141. 环形链表
思路:现在考虑一个环形链表,把慢指针和快指针想象成两个在环形赛道上跑步的运动员(分别称之为慢跑者与快跑者)。而快跑者最终一定会追上慢跑者。这是为什么呢?考虑下面这种情况(记作情况 A)- 假如快跑者只落后慢跑者一步,在下一次迭代中,它们就会分别跑了一步或两步并相遇。
/**
* 已经定义的 ListNode 结点
* typedef struct Node {
* int val;
* struct Node *next;
* } ListNode;
* #define bool int
* #define true 1
* #define false 0
*/
bool isLinkedListCycle(ListNode *head) {
ListNode * p = head,* q = head; //q是快指针,p是慢指针
while(q != NULL){
q = q->next;
if(q != NULL){
q = q->next;
}else{
return false;
}
p = p->next;
if(p == q){
return true;
}
}
return false;
}
二、求链表环的链接点
142. 环形链表 II
解法:Floyd 算法
算法:
Floyd 的算法被划分成两个不同的 阶段 。在第一阶段,找出列表中是否有环,如果没有环,可以直接返回 null 并退出。否则,用 相遇节点 来找到环的入口。
阶段 1
这里我们初始化两个指针 - 快指针和慢指针。我们每次移动慢指针一步、快指针两步,直到快指针无法继续往前移动。如果在某次移动后,快慢指针指向了同一个节点,我们就返回它。否则,我们继续,直到 while 循环终止且没有返回任何节点,这种情况说明没有成环,我们返回 null 。
下图说明了这个算法的工作方式:
环中的节点从 0 到 C-1 编号,其中 C 是环的长度。非环节点从 -F到 -1编号,其中 F是环以外节点的数目。 F 次迭代以后,慢指针指向了 0 且快指针指向某个节点 h ,其中 F≡h(modC) 。这是因为快指针在 F次迭代中遍历了 2F个节点,且恰好有 F 个在环中。继续迭代 C−h 次,慢指针显然指向第 C-h 号节点,而快指针也会指向相同的节点。原因在于,快指针从 h号节点出发遍历了 2(C-h)个节点。
因此,如果列表是有环的,快指针和慢指针最后会同时指向同一个节点,因此被称为 相遇 。
阶段 2
给定阶段 1 找到的相遇点,阶段 2 将找到环的入口。首先我们初始化额外的两个指针: ptr1 ,指向链表的头, ptr2 指向相遇点。然后,我们每次将它们往前移动一步,直到它们相遇,它们相遇的点就是环的入口,返回这个节点。
我们利用已知的条件:慢指针移动 1 步,快指针移动 2 步,来说明它们相遇在环的入口处。(下面证明中的 tortoise 表示慢指针,hare 表示快指针)
因为 F=b ,指针从 h 点出发和从链表的头出发,最后会遍历相同数目的节点后在环的入口处相遇。
/**
* 已经定义的 ListNode 结点
* typedef struct Node {
* int val;
* struct Node *next;
* } ListNode;
*/
ListNode* linkedListCycleLinkedNode(ListNode *head) {
ListNode * p = head ,*q = head;
ListNode *ans = NULL;
while(q){
q = q->next;
if(q != NULL){
q = q->next;
}else{
return ans;
}
p = p->next;
if(p == q){
break;
}
}
if(q == NULL) return ans;
p = head;
while(p != q){
p = p->next;
q = q->next;
}
return p;
}
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fastPtr=head, *slowPtr=head;
while (fastPtr!=NULL && fastPtr->next!=NULL)
{
fastPtr = fastPtr->next->next;
slowPtr = slowPtr->next;
if (fastPtr==slowPtr)
{
fastPtr = head;
while (fastPtr != slowPtr)
{
fastPtr = fastPtr->next;
slowPtr = slowPtr->next;
}
return fastPtr;
break;
}
}
return nullptr;
}
三、求环的长度
思路: 在环上相遇后,记录第一次相遇点为Pos,之后指针slow继续每次走1步,fast每次走2步。在下次相遇的时候fast比slow正好又多走了一圈,也就是多走的距离等于环长。
设从第一次相遇到第二次相遇,设slow走了len步,则fast走了2len步,相遇时多走了一圈:环长=2len-len。
/**
* 已经定义的 ListNode 结点
* typedef struct Node {
* int val;
* struct Node *next;
* } ListNode;
*/
int linkedListCycleLength(ListNode *head) {
ListNode * p = head, *q = head;
int ans = 1;
while(q && q->next){
q = q->next->next;
p = p->next;
if(p == q){
q = q->next->next;
p = p->next;
while(q != p){
q = q->next->next;
p = p->next;
ans++;
}
return ans;
}
}
return NULL;
}
四、倒数第K个节点
思路:“倒数第k个元素的问题”。设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 p 即指向倒数第 k 个结点。
五、获取中间位置的元素
思路:获取中间元素的问题。设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast向后走两次,slow向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠前一个
六、求两个链表相交位置
160. 相交链表
思想:创建两个指针 pA 和 pB,分别初始化为链表 A 和 B 的头结点。然后让它们向后逐结点遍历。
当 pA 到达链表的尾部时,将它重定位到链表 B 的头结点 (你没看错,就是链表 B); 类似的,当 pB 到达链表的尾部时,将它重定位到链表 A 的头结点。
若在某一时刻 pA和 pB相遇,则 pA/pB 为相交结点。
想弄清楚为什么这样可行, 可以考虑以下两个链表: A={1,3,5,7,9,11} 和 B={2,4,9,11},相交于结点 9。 由于 B.length (=4) < A.length (=6),pB 比 pA少经过 2 个结点,会先到达尾部。将 pB重定向到 A 的头结点,pA重定向到 B 的头结点后,pB 要比 pA多走 2 个结点。因此,它们会同时到达交点。
如果两个链表存在相交,它们末尾的结点必然相同。因此当 pA/pB 到达链表结尾时,记录下链表 A/B 对应的元素。若最后元素不相同,则两个链表不相交。
/**
* 已经定义的 ListNode 结点
* typedef struct Node {
* int val;
* struct Node *next;
* } ListNode;
*/
ListNode* findIntersectionListNode(ListNode *head1, ListNode *head2) {
if (head1 == NULL || head2 == NULL) {
return NULL;
} else {
ListNode *lA = head1;
ListNode *lB = head2;
while (1) {
if(lA == lB) {
return lA;
}
if (lA == NULL) {
lA = head2;
}
if (lB == NULL) {
lB = head1;
}
lA = lA->next;
lB = lB->next;
}
return NULL;
}
}
参考:https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode/