本篇博客整理了一些常见的链表oj题(以下所有oj题均出自leetcode平台),用于交流和学习。下面将通过C实现这些oj题。
给定一个链表的首节点 head ,判断链表中是否有环。链表存在环返回true,否则返回false。示例如下:
示例1:
该链表存在环,返回true
示例2:
该链表不存在环,返回false
基本思路:使用快慢指针,慢指针一次走一步,快指针一次走两步。若链表无环:链表节点数为偶数个,fast最终会走到NULL;链表节点数为奇数个,fast->next最终会为NULL。若链表有环:slow与fast最终会相遇。
为什么链表有环时,slow与fast一定会相遇呢?下面我们对这一结论进行证明。
代码实现:
bool hasCycle(struct ListNode *head) {
struct ListNode* slow, *fast;
slow = fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true;
}
return false;
}
拓展问题1:如果fast一次走3步、4步、n步,slow与fast还会相遇吗?
fast一次走3步时:
设slow进环时,fast与slow的距离为N。fast一次走3步,slow一次走1步,则每走一次fast与slow的距离缩减2。
当N为偶数时,最终距离N会缩减为0。
当N为奇数时,最终距离N会缩减为-1,即fast与slow错过了没能相遇。此时我们设环的长度为C,此时fast与slow的距离为C-1,若环的长度为奇数,则C-1为偶数,每走一次fast与slow的距离缩减2,最终C-1会缩减为0,fast与slow相遇;若环的长度为偶数,则C-1为奇数,最终距离C-1会缩减为-1,即fast与slow的距离又变为C-1,说明fast与slow永远不会相遇。
综上,如果fast一次走3步,当slow进环时fast与slow的距离为奇数且环的长度为偶数时,fast与slow永远不会相遇。所以fast一次走3步,slow与fast不一定会相遇。
同理可证,当fast走4步、n步时,slow与fast也不一定会相遇。
拓展问题2:找到并返回环的入口处的节点。如果链表无环返回NULL。
通过先前的证明,我们已经得出了结论:如果链表带环,slow一次走一步,fast一次走两步,slow与fast最终一定会相遇。
slow与fast相遇后,将相遇节点的下一个节点作为另一个链表的首节点,则相遇节点为新链表与原链表共同的尾节点。此时新链表与原链表相交的起始节点即为原链表环的入口处的节点。具体的代码实现这里就不做展示了。
给定链表的首节点head,链表的每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。示例如下:
基本思路:第一步,拷贝原链表每个节点的val,并将拷贝的节点依次插入到原链表每个节点的后面。第二步,拷贝原链表每个节点的random状态,如果原链表当前节点的random为NULL,则拷贝节点的random也为NULL;如果原链表当前节点的random指向某个节点,则拷贝节点的random应指向原链表当前节点的random指向的节点的next(即copy->random = cur->random->next)。第三步,独立出复制的链表,并恢复原链表的链接状态。
按照该思路,大致作图如下:
这里只作出了第一步的草图,根据该图构思第二、三步就比较轻松了,因此这里不再继续作第二、三步的草图。
代码实现:
struct Node* copyRandomList(struct Node* head) {
struct Node* cur = head;
//将复制的节点依次插入到原链表每个节点的后面
while (cur)
{
struct Node* next = cur->next;
struct Node* copy = malloc(sizeof (struct Node));
copy->val = cur->val;
cur->next = copy;
copy->next = next;
cur = next;
}
cur = head;
//拷贝原链表的random顺序
while (cur)
{
struct Node* copy = cur->next;
if (cur->random == NULL)
//原链表当前节点的random为N时
copy->random = NULL;
else
//原链表当前节点的random指向原链表某个节点时
copy->random = cur->random->next;
cur = copy->next;
}
cur = head;
//独立出复制的链表并恢复原链表的链接
struct Node* copyHead = malloc(sizeof (struct Node));
struct Node* copyTail = copyHead;
while (cur)
{
struct Node* copy = cur->next;
copyTail->next = copy;
copyTail = copyTail->next;
cur->next = copy->next;
cur = cur->next;
}
copyTail->next = NULL;//处理链表为空的情况
struct Node* guard = copyHead;
copyHead = copyHead->next;
free(guard);
return copyHead;
}
给定一个已排序链表的首节点head ,删除原始链表中所有重复数字的节点(重复节点不保留),只留下不同的数字。返回已排序的链表。示例如下:
基本思路:创建一个哨兵位头节点用于建立不含重复节点的新链表,同时遍历链表,判断当前节点(cur)与下一个节点(next)是否重复。如果当前节点与下一个节点重复,则让当前节点链向下一个节点的下一个节点(cur->next = next->next),并free下一个节点(next),再迭代next(next = cur->next),接着继续判断当前节点是否与下一个节点重复,直到当前节点与下一个节点不再重复或下一个节点为NULL后(内循环结束),free当前节点后迭代cur(cur = next)。如果当前节点与下一个节点不重复,直接将当前节点尾插到新链表,再迭代cur(cur = next)。直到原链表遍历完毕后(外循环结束),新链表也就建立完成了。
按照该思路,大致作图如下:
注意:当原链表的尾节点与前一个节点重复时,原链表的尾节点不会尾插到新链表,这样就会导致新链表的尾节点没有链向NULL,所以在原链表遍历结束后需要将新链表的尾节点链向空。
代码实现:
struct ListNode* deleteDuplicates(struct ListNode* head){
struct ListNode* newHead = malloc(sizeof (struct ListNode));
struct ListNode* tail = newHead;
struct ListNode* cur = head;
while (cur)
{
struct ListNode* next = cur->next;
int repeat = 0;//默认当前节点与下一个节点不重复
while (next && cur->val == next->val)
{
repeat = 1;//当前节点与下一个节点重复
cur->next = next->next;
free(next);
next = cur->next;
}
if (repeat)//发生过至少一次重复
{
free(cur);
cur = next;
}
else//当前节点与下一个节点不重复或下一个节点为NULL
{
tail->next = cur;
tail = tail->next;
cur = next;
}
}
tail->next = NULL;//注意这里要置空
struct ListNode* guard = newHead;
newHead = newHead->next;
free(guard);
return newHead;
}