链表是否存在环及环入口点、两个链表是否相交、相交链表的第一个公共结点
1.求链表倒数第k个结点
题目描述:
输入一个单向链表,输出该链表中倒数第k个结点,链表的倒数第0个结点为链表的尾指针。
分析:设置两个指针p1,p2,首先p1和p2都指向链表头结点,然后p2向前走k步,这样p1和p2之间就间隔k个节点,最后p1和p2同时向前移动,直至p2走到链表末尾。需要注意的是,要考虑可能的非法输入参数,也就是说要做参数检查,防止程序出现异常。
struct ListNode
{
int value;
ListNode* next;
};
ListNode* leastKNode(ListNode *pHead, intk)
{
if(head == NULL|| k < 0)
{
throw std::new exeception("Invalid parameters!");
}
ListNode* pFirst =pHead;
ListNode* pSec = pHead;
for(; k>0 &&pSec != NULL; --k)
pSec = pSec->next();
//链表长度小于k
if (k > 0)
returnNULL;
//pSec走到链表尾时,pFirst指向倒数第k个结点
while(pSec != NULL)
{
pFirst =pFirst->next;
pSec =pSec->next;
}
return pFirst;
}
2.判断两个链表是否相交
题目描述:给出两个单链表的头指针(如下图所示)h1和h2,判断这两个链表是否相交。(为了简化问题,假设两个链表均不带环)
分析:这是来自编程之美上的微软亚研院的一道面试题目,其思路如下:
1>. 最直接的方法:
循环判断第一个链表的每个节点是否在第二个链表中。这种方法的时间复杂度为O(Length(h1) * Length(h2))。
2>. hash遍历:
针对第一个链表构造hash表,然后判断第二个链表的每个结点是否在出现在该hash表中,如果第二个链表的所有结点都能在hash表中找到,即说明第二个链表与第一个链表有相同的结点。时间复杂度为:O(Length(h1) + Length(h2));同时为了存储第一个链表所有节点,空间复杂度为O(Length(h1))。
3>. 如果两个没有环的链表相交于某一节点,那么在这个节点之后的所有节点都将重合。如果两链表相交,则最后一个节点一定重合。很容易能得到链表的最后一个节点,所以一种简洁的方法是只要判断两个链表的尾指针是否相等。相等,则链表相交;否则,链表不相交。这种方法的时间复杂度为O((Length(h1) + Length(h2)),空间复杂度为O(1)。
上面的问题前提假设链表无环,如果链表是有环呢?
在没有前提假设的情况下,判断两个链表是否相交的问题主要步骤为:
判断带不带环
如果都不带环,就判断尾节点是否相等
如果都带环,判断一链表上俩指针相遇的那个节点,在不在另一条链表上。在,则相交;不在,则不相交。
如果一个带环一个不带环,则肯定不相交。
3.判断链表是否带环
同样设置两个指针p1和p2,初始时都指向链表头。p1每次前进一步,p2每次前进二步,如果链表存在环,则p2先进入环,p1后进入环,两个指针在环中走动,必定相遇。
//判断链表是否有环,如果有环,返回环里的节点
bool hasCircle(ListNode * pHead,ListNode *& pCircleNode, ListNode *& pLastNode)
{
ListNode* pFast =pHead->next;
ListNode* pSlow =pHead;
while(pFast !=pSlow && pFast != NULL && pSlow != NULL)
{
if(pFast->next != NULL)
pFast = pFast->next;
else
pLastNode = pFast;
if(pSlow->next == NULL)
pLastNode = pSlow;
pFast =pFast->next;
pSlow =pSlow->next;
}
if(pFast ==pSlow && pFast != NULL && pSlow != NULL)
{
pCircleNode = pFast;
return true;
}
else
return false;
}
单链表有环,则怎么找到环入口点呢?
当快慢指针相遇时,慢指针肯定没有遍历完链表,而快指针可能已经在环内循环了n圈(1<=n)。假设慢指针走了s步,则快指针走了2s步,同时快指针步数等于s 加上在环上多转的n圈,设环长为r,则:
2s = s + nr s = nr
设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a。
a + x = nra + x = (n – 1)r +r = (n-1)r + L – a a = (n-1)r + (L – a – x)
(L – a – x)为相遇点到环入口点的距离,由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是从链表头、与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。
ListNode* findLoopEntrance(ListNode*pHead)
{
if (pHead == NULL)
throw std::new exception("Invalid Parameter!");
ListNode* pSlow = pHead;
ListNode*pFast = pHead;
while (pFast != NULL && pFast->next !=NULL)
{
pSlow =pSlow->next;
pFast =pFast->next->next;
if (pSlow== pFast )
break ;
}
if (pFast== NULL || pFast->next == NULL)
return NULL;
pSlow = pHead;
while (pSlow!= pFast)
{
pSlow = pSlow->next;
pFast = pFast->next;
}
return pSlow;
}
从网是找到的一种易于理解的解释:
一种O(n)的办法就是(两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然):
关于这个解法最形象的比喻就是在操场当中跑步,速度快的会把速度慢的扣圈
可以证明,p2追赶上p1的时候,p1一定还没有走完一遍环路,p2也不会跨越p1多圈才追上。我们可以从p2和p1的位置差距来证明,p2一定会赶上p1但是不会跳过p1的。因为p2每次走2步,而p1走一步,所以他们之间的差距是一步一步的缩小,4,3,2,1,0 到0的时候就重合了。根据这个方式,可以证明,p2每次走三步以上,并不总能加快检测的速度,反而有可能判别不出有环。既然能够判断出是否是有环路,那改如何找到这个环路的入口?
解法如下:当p2按照每次2步,p1每次一步的方式走,发现p2和p1重合,确定了单向链表有环路了。接下来,让p2回到链表的头部,重新走,每次步长不是走2了,而是走1,那么当p1和p2再次相遇的时候,就是环路的入口了。
证明:
在p2和p1第一次相遇的时候,假定p1走了n步骤,环路的入口是在p步的时候经过的,那么有
p1走的路径: p+c = n; c为p1和p2相交点距离环路入口的距离
p2走的路径: p+c+k*L = 2*n; L为环路的周长,k是整数
n+K*L = 2*n ==> n=K*L
显然,如果从p+c点开始,p1再走n步骤的话,还可以回到p+c这个点。
同时p2从头开始走的话,经过n步,也会达到p+c这点。
显然在这个步骤当中p1和p2只有前p步骤走的路径不同,所以当p1和p2再次重合的时候,必然是在链表的环路入口点上。
综合上面的2、3两部分,判断两个链表是否相交:
//如果都不带环,就判断尾节点是否相等
//如果都带环,判断一链表上两指针相遇的节点是否出现在另一条链表上
bool isListIntersect(ListNode* pHead1, ListNode* pHead2)
{
ListNode* pCircleNode1;
ListNode* pCircleNode2;
ListNode* pLastNode1;
ListNode* pLastNode2;
bool isCircle1 = hasCircle(pHead1,pCircleNode1, pLastNode1);
bool isCircle2 = hasCircle(pHead2,pCircleNode2, pLastNode2);
//一个有环,一个无环
if(isCircle1 != isCircle2)
return false;
//两个都无环,判断最后一个节点是否相等
else if(!isCircle1 && !isCircle2)
return pLastNode1 == pLastNode2;
//两个都有环,判断环里的节点是否能到达另一个链表环里的节点
else
{
ListNode * temp = pCircleNode1->next;
while(temp != pCircleNode1){
if(temp == pCircleNode2)
return true;
temp = temp->next;
}
return false;
}
return false;
}
4. 两个链表相交的第一个节点
分析:
如果两个尾结点是一样的,说明它们有重合;否则两个链表没有公共的结点。
当两个链表长度不一样时,假设一个链表比另一个长L个结点,先在长的链表上遍历L个结点,之后再同步遍历两个链表。这样就能保证同时到达最后一个结点了。由于两个链表从第一个公共结点开始到链表的尾结点之间的所有结点都是重合的。因此,它们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。分别遍历两个链表得到它们的长度,并求出两个长度之差。在长的链表上先遍历若干次之后,再同步遍历两个链表,直到找到相同的结点或其中一个链表结束。(这里没有考虑循环链表的情况)
ListNode* findFirstCommonNode(ListNode*pHead1, ListNode* pHead2)
{
//获取两个链表的长度
unsigned int nLength1 = ListLength(pHead1);
unsigned int nLength2 = ListLength(pHead2);
int nLengthDif = nLength1 - nLength2;
ListNode *pListHeadLong = pHead1;
ListNode *pListHeadShort = pHead2;
if(nLength2 > nLength1)
{
pListHeadLong = pHead2;
pListHeadShort = pHead1;
nLengthDif = nLength2 - nLength1;
}
//在长的链表上先遍历nLengthDif步
for(int i = 0; i < nLengthDif; ++ i)
pListHeadLong = pListHeadLong->m_pNext;
//同步遍历两个链表
while((pListHeadLong != NULL)
&& (pListHeadShort != NULL)
&& (pListHeadLong != pListHeadShort))
{
pListHeadLong = pListHeadLong->m_pNext;
pListHeadShort = pListHeadShort->m_pNext;
}
//找到第一个公共结点
ListNode *pFisrtCommonNode = NULL;
if(pListHeadLong == pListHeadShort)
pFisrtCommonNode = pListHeadLong;
return pFisrtCommonNode;
}
unsigned int ListLength(ListNode* pHead)
{
unsigned int nLength = 0;
ListNode* pNode = pHead;
while(pNode != NULL)
{
++ nLength;
pNode = pNode->m_pNext;
}
return nLength;
}
转载于:https://blog.51cto.com/redpaopaw/1294826