欢迎各位大佬交流!!!
通过对经典链表OJ题目的练习,不仅能加深对链表的理解,更能体会链表的精妙之处!
目录
[11、环形链表 Ⅱ](#11、环形链表 Ⅱ)
9、相交链表
题意是如果两个链表相交,那就返回相交节点;
如果不相交,直接返回NULL即可;
首要任务就是判断两个链表是否相交;怎样判断呢?
如果两个链表相交,会有什么特征?
答案是它们的尾节点相同
一、判断两个链表是否相交
因此我们分别定义两个指针,用来遍历两个链表,均让它们走到尾节点;
此时判断两个指针是否相等,相等即为有环,否则无环;
cpp
struct ListNode* curA = headA;
struct ListNode* curB = headB;
//先走到尾节点
while(curA->next) curA = cur->next;
while(curB->next) curB = curB->next;
//判断是否相交
if(curA != curB) return NULL;
二、找到相交节点

对于图片中这个例子,怎样才能找到相交节点?
最简单的方法就是定义一个指针指向a1,一个指针指向b2;简单说就是两个指针同步!
然后进入循环中,一步一步走;循环结束条件就是两个指针相等;
那么我们是怎样找到这两个指针的指向的呢?
答案是通过两个链表中节点的数量;
假设链表A的节点数量是cntA,链表B的节点数量是cntB;
那么cntB - cntA 就是两个链表之间的差距;
怎样让两个指针同步?仅需让指向链表B的指针往前走差距步即可!
此时让两个指针同时走;当循环结束时,共同指向的就是相交链表
下面我们来尝试写代码:
既然要知道两个链表的节点数量
不妨直接在判断是否有环时统计节点数量;
三、小优化:
判断完存在环之后,需要使用 if-else 来处理不同的情况;
那么不妨使用假设法;
假设长链表是A,短链表是B;然后根据节点数量增加 if 判断,修正结果;
此时直接使用长链表走差距步即可,无需顾虑A还是B;
cpp
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int cntA = 0,cntB = 0;
//先走到尾节点
while(curA->next)
{
curA = curA->next;
cntA++;
}
while(curB->next)
{
curB = curB->next;
cntB++;
}
//判断是否相交
if(curA != curB) return NULL;
//假设长链表是A
struct ListNode* longList = headA;
struct ListNode* shortList = headB;
if(cntA < cntB)
{
//说明B是长链表
longList = headB;
shortList = headA;
}
int gap = abs(cntA - cntB);
//长链表先走差距步
while(gap--)
{
longList = longList->next;
}
//再同时走
while(shortList != longList)
{
shortList = shortList->next;
longList = longList->next;
}
return shortList;
}

10、环形链表
我觉得如果第一次做这道题,不太好想;
快慢指针:
这题我们可以通过快慢指针的方法来解,依旧是快指针走一步,慢指针走两步;
先给出结论:如果有环,两个指针最终会相遇;
我们先看这个思路能否AC
cpp
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow) return true;
}
return false;
}

证明:
下面我们来证明一下:
如果链表没环的话,但 fast 走到为空时循环结束,返回 false;
下图即为有环情况下,两个指针一定会相遇的证明;

延伸:
如果快指针一次走三步、四步呢?n步呢?是否还会相遇?


总结:无论快指针一次走多少步,最终均会相遇!
11、环形链表 Ⅱ
首先我们要判断链表中是否有环;
根据上一题经验,直接定义快慢指针;快指针一次走两步,慢指针一次走一步;
如果链表中有环,那么快慢指针一定会相遇;
相遇后,我们需要思考怎样能够找到进环时的节点?
思路一:
既然两个指针相遇了,不妨定义 meet 指针表示相遇指针;
如果我们把这个链表以 meet 为中间节点一分为二,即将 meet 的下一个节点设为newhead,并将meet的next置为空;
即被分割成了一个以meet为尾节点的链表;一个以newhead为头节点的链表;
而题意要求我们返回入环时的节点,不就相当于两个链表的第一个相交节点吗?
而这正是我们第9题的内容;
下面我们来完善一下代码:
首先判断是否有环
cpp
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* meet = slow;
struct ListNode* newhead= slow->next;
meet->next = NULL;
}
}
return NULL;
}
接着实现相交链表,不妨将其封装为一个函数直接在相遇后执行;
要注意的是,由于先前我们已经判断过有环,因此两个链表一定会相交!
但由于要知道节点的数量,还是需要利用两个指针遍历;
不过最终的判断可以省略掉;
cpp
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
//先判断是否相交
struct ListNode* curA = headA;
struct ListNode* curB = headB;
//看尾节点是否相等
int lenA = 1,lenB = 1;
while(curA->next)
{
curA = curA->next;
lenA++;
}
while(curB->next)
{
curB = curB->next;
lenB++;
}
//if(curA != curB) return NULL;
//假设法
struct ListNode* longlist = headA;
struct ListNode* shortlist = headB;
if(lenA < lenB)
{
longlist = headB;
shortlist = headA;
}
//先走差距步
int gap = abs(lenA - lenB);
while(gap--)
{
longlist = longlist->next;
}
while(shortlist != longlist)
{
shortlist = shortlist->next;
longlist = longlist->next;
}
return shortlist;
}
最后来看一下完整代码
cpp
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
//先判断是否相交
struct ListNode* curA = headA;
struct ListNode* curB = headB;
//看尾节点是否相等
int lenA = 1,lenB = 1;
while(curA->next)
{
curA = curA->next;
lenA++;
}
while(curB->next)
{
curB = curB->next;
lenB++;
}
//if(curA != curB) return NULL;
//假设法
struct ListNode* longlist = headA;
struct ListNode* shortlist = headB;
if(lenA < lenB)
{
longlist = headB;
shortlist = headA;
}
//先走差距步
int gap = abs(lenA - lenB);
while(gap--)
{
longlist = longlist->next;
}
while(shortlist != longlist)
{
shortlist = shortlist->next;
longlist = longlist->next;
}
return shortlist;
}
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* meet = slow;
struct ListNode* newhead= slow->next;
meet->next = NULL;
return getIntersectionNode(head,newhead);
}
}
return NULL;
}

思路二:
思路一中是在相遇后,将链表一分为二改成相交链表问题;
有没有不需要一分为二的办法呢?
我们先给出结论:当快慢指针相遇后,重新定义两个指针,一个从链表头节点出发,另一个从相遇点出发,两个指针相遇的位置就是入环节点
下面我们来证明一下:

在这种思路下,代码实现将会变得异常简单;
cpp
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* meet = slow;
while(head != meet)
{
head = head->next;
meet = meet->next;
}
return meet;
}
}
return NULL;
}

12、随机链表的复制
最后一道题可以说是链表的试金石;
不仅思路不好想,而且对链表的操控要求也比较高;
那么我们就直接给出思路:
首先在原链表的每个节点后面都尾插一个复制好的节点;复制好的节点只修改val 和 next;
当尾插完所有复制好的节点后,更改 random 的指向;
最终将新节点连在一起,返回头指针即可;
下面我们来处理一些核心问题:
Q1:为什么要所有节点尾插完之后再更改 random 的指向呢?
因为 random 指向的可能是还未遍历到的节点!
Q2:怎样更改某个节点random的指向?
先找到原节点,接着将 random 指向原链表的random的next !
下面我们来尝试写代码:
一、复制并尾插
首先将将原链表中每个节点都复制一份并尾插在其后面;
cpp
struct Node* cur = head;
while(cur)
{
struct Node* next = cur->next;
//复制一个新的节点
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->val = cur->val;
newnode->next = cur->next;
//尾插在cur后面
newnode->next = cur->next;
cur->next = newnode;
cur = next;
}
二、修改random的指向
cpp
//修改random的指向
struct Node* pcur = head;
while(pcur)
{
//pcur是原节点,next是复制节点
struct Node* next = pcur->next;
if(pcur->random == NULL) next->random = NULL;
else next->random = pcur->random->next;
//pcur只需遍历原节点
pcur = pcur->next->next;
}
三、串联新链表
cpp
struct Node* newhead = NULL;
struct Node* newtail = NULL;
//串联复制链表
cur = head;
while(cur)
{
struct Node* ansnode = cur->next;
if(newtail == NULL)
{
//既是头指针也是尾指针
newhead = newtail = ansnode;
}
else
{
//尾插在newtail后面
newtail->next = ansnode;
newtail = ansnode;
}
cur = ansnode->next;
}
return newhead;
最终我们将代码整合在一起
cpp
struct Node* copyRandomList(struct Node* head)
{
struct Node* cur = head;
while(cur)
{
struct Node* next = cur->next;
//复制一个新的节点
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->val = cur->val;
newnode->next = cur->next;
//尾插在cur后面
newnode->next = cur->next;
cur->next = newnode;
cur = next;
}
//修改random的指向
struct Node* pcur = head;
while(pcur)
{
//pcur是原节点,next是复制节点
struct Node* next = pcur->next;
if(pcur->random == NULL) next->random = NULL;
else next->random = pcur->random->next;
//pcur只需遍历原节点
pcur = pcur->next->next;
}
struct Node* newhead = NULL;
struct Node* newtail = NULL;
//串联复制链表
cur = head;
while(cur)
{
struct Node* ansnode = cur->next;
if(newtail == NULL)
{
//既是头指针也是尾指针
newhead = newtail = ansnode;
}
else
{
//尾插在newtail后面
newtail->next = ansnode;
newtail = ansnode;
}
cur = ansnode->next;
}
return newhead;
}

如有不足之处欢迎指出!!!