1.输入两个链表,找出它们的第一个公共结点
这里的示例输入是伪代码形式
链表的相交是y字型,而不是x字型。这是一个单链表,每个节点只有一个next,所以不可能有x字型。
思路:最基本的思路,看链表A中的节点是否为链表B中的节点。但这样的时间复杂度效率太低,为O(m*n);这样只有一个好处可以在确定相交后就能求出相交的节点。注意:不能用节点中的值进行比较,要用地址进行比较。
思路2:找到链表A和链表B的尾结点,比较地址是否相同,相同则链表相交。时间复杂度为O(m+n)。如何找第一个交点?逆置可以吗?不可以,逆置只能逆置一个链表,另一个链表会找不到。以示例1为例,只要链表A从第一个节点开始,链表B从第二个节点开始,遍历,第一个地址相同的节点就是交点。确定链表AB的长度(la,lb),就能保证链表在正确的位置(长的链表先走差距步( |la-lb| ))开始遍历。
代码如下:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
assert(headA && headB);
int la = 0;
int lb = 0;
int i = 0;
struct ListNode* pa = headA;
struct ListNode* pb = headB;
while(pa->next)
{
pa = pa->next;
la++;
}
while(pb->next)
{
pb = pb->next;
lb++;
}
if(pb != pa)//如果不等,说明不相交
{
return NULL;
}
if(la > lb)//A长还是B长两种情况判断
{
for(i = 0; i < la-lb; i++)
{
headA = headA->next;
}
}
else
{
for(i = 0; i < lb-la; i++)
{
headB = headB->next;
}
}
while(headA != headB)
{
headA = headA->next;
headB = headB->next;
}
return headA;
}
2.给定一个链表,判断链表中是否有环(大的来了)
带环链表一遍历就会死循环。带环链表是无法计算节点数的链表。
先将带环链表抽象化:假设这就是一个带环链表
设置快慢指针,fast,slow,在slow进环后,fast开始追赶slow,直到fast追上slow。
仅仅判断是否为环是很简单的。但这个本质上是一个逻辑题。
先写出代码:
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
return true;
}
}
return false;
}
扩展追问:
1.fast走两步,slow走一步,一定能追上吗?如何证明?
2.fast走3,4,5,n步呢?
3.求出链表的入口点。
1.假设slow进环后slow和fast的距离是N,每次追击,距离缩小1,则slow和fast之间的距离必然会减至0。
2.走3步,则不一定追的上,比如环长为8,每次追击距离缩小2,slow进环后与fast的距离为7,那么距离变化为:7,5,3,1,7,5,3,1……
就会永远追不上,走4步,也可能一直追不上,比如进环后距离为7,环长为9,距离变化为7,4,1,7,4,1……再考虑走n步的情况,假设环
长为N,slow进环后距离为C,那么如果(C+kN)/(n-1)存在
整数k使得公式(C+kN)/(n-1)的值可为整数就必定能追上,反之则不能。
另一个理解方式:走3步,如果C为偶数则必定会追上,若C为奇数,则最后的距离为-1,也就是N-1,N-1为偶数,也能追上,N-1为奇
数就永远追不上。走4步,最后的距离为2或1或0,为2,则距离为kN+2,存在k使得(kN+2)%3=0就能追上,不存在就永远追不上,同
理,存在k使得(kN+1)%3=0,就能追上。不同情况的C会导致不同情况的最后距离。
3.求slow入口点
法一:公式法
存在公式可以证明l1和l2的长度相等,也就是两个指针从头结点和快慢指针相遇点开始走,会在环的入口点相遇。
fast一次走2步,slow一次走1步,设slow走的距离为x,fast的距离为2x,设环的长度为N,因为相对速度是1,所以一定是在fast第二
圈时追上,也就是说,fast比slow多走的x就是环的长度。slow走过的距离是环的长度,所以l1,l2相等。
这样想不完全对,因为环的大小不一定,所以可能存在,fast已经走了很多圈的情况,但slow不可能走满一圈。
所以应该表示成kN = x,k为整数。小环也满足l1,l2长度相等,就是表示方式不一样。环很大则满足上面的情况。fast要追上slow至少
要走一圈。所以设环长为N,slow进环后fast要追的距离为C,头结点到slow入环点距离为L。x = L+N-C,2x = L+kN+N-C,因为2倍
关系,所以2L+2N-2C = L+kN+N-C =》 L = kN-(N-C)
所以在代码中,一个指针从头结点开始,一个指针从相遇点开始,每次前进1,地址相同的节点就是slow入环点。
代码如下:
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
struct ListNode* meet = slow;
while(meet != head)
{
meet = meet->next;
head = head->next;
}
return meet;
}
}
return NULL;
}
法二
把环断开,转换成链表的相交问题。但这种方法的代码会复杂一点。
3. 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点,要求返回这个链表的深度拷贝。
深拷贝就是拷贝一样格式的链表,输入中第1个节点7的random指向null,第2个节点13的random指向第1个节点,深拷贝要保证,
random指向的节点也是一样的。难点也是在random上,保证random要指向新拷贝链表的对应节点,直接复制则指向原链表的节点。
思路:第一步拷贝原链表。第二步让random指向对应的节点。
如何指向对应的节点?
通过遍历找节点存储的值可以吗?不可以,节点可能存储相同的值,那怎么找?用相对距离。然后用快慢指针找到对应的节点。这个过
程相对复杂,而且快慢指针找负距离的相当麻烦。
所以介绍一个新方法:将新拷贝链表的每个节点分别连在原节点的后面。
拷贝节点的random在原节点random的后面。完成random后再把拷贝链表解开。然后将各拷贝节点链接到一起。
这只是思路,写代码时还要另外画图。
对照图写代码不容易出错。(标记行)
代码如下:
struct Node* copyRandomList(struct Node* head)
{
if(head == NULL)
return NULL;
struct Node* cur = head;
while(cur)//在原节点后拷贝链表
{
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = cur->next->next;
}
cur = head;
while(cur)//改对应的random,看标记行下第一幅图理解
{
struct Node* copy = cur->next;
if(cur->random == NULL)
{
cur->next->random = NULL;
cur = cur->next->next;
}
else
{
copy->random = cur->random->next;
cur = cur->next->next;
}
}
cur = head;
struct Node* ret = cur->next;
while(cur)//链接复制链表,看标记行下第二幅图理解
{
struct Node* tmp = cur->next;
cur->next = tmp->next;
cur = cur->next;
if (cur != NULL)//在最后cur指向NULL了,if语句结束后就是while判断cur是否为空了,if中cur->next会非法访问
tmp->next = cur->next;
else
tmp->next = NULL;
}
return ret;
}