项目地址:https://github.com/SpecialYy/Sword-Means-Offer
题目
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
解析
预备知识
此处指的是单链表存在的环的情况,以下图为似:
我们目标就是要找环的入口点,也就是说从链表首部出发,首次切入环的点,比如上图中3就是就是环入口。
所以我们需要解决的问题有2个:
- 如何判断链表有环?
- 在有环的情况,如何找到环的入口点?
首要问题是判断链表有环,受这道题的启发:https://blog.csdn.net/dawn_after_dark/article/details/80744040。
我们用2个指针,一个快指针和一个慢指针。可以发现若存在环,必然在某个时刻快指针可以追上慢指针。
若果没有环,快指针必然走到链表末尾,为null。
思路一
这里其实我不太想用数学来描述这个解法,所以我先举个例子。比如2个人在操场上跑圈,a的速度为1m,b的速度为2m,同时从起点出发,快的必然能追上a。又因为同时从起点出发,所以最后相遇点还是起点。i % n = (2i) % n
,显然i = kn(k = 0, 1, 2)
。
那如果b先走k步,a还是在起点呢,然后再同时出发,因为此时b要先与k步,本来要在起点相遇的,现在由于起始位置都占据了有力优势,所以b追上a比之前要少k步,也就说这时相遇点为n - k
。第一种情况其实就是第二种情况特例,即b先走0步,可以规划为一种来讨论。
这个场景可以很好的对应到链表上,假设让b就走到环的入口点(起点离环入口k步),然后a还是在起点处,同时出发,相遇点就是上面讨论的n - k
了。又因为起点离环入口也是k步,所以可以让a重新回到起点,b的速度变为1,再同时起步,这样相遇时即为环的入口点,因为两者都是走了k步。
**
* 经典解法
* @param pHead
* @return
*/
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead == null || pHead.next == null) {
return null;
}
boolean isIntersect = true;
ListNode p = pHead;
ListNode q = pHead;
while(q != null && q.next != null) {
p = p.next;
q = q.next.next;
if(p == q) {
break;
}
}
isIntersect = p == q ? true : false;
if(isIntersect) {
p = pHead;
while(p != q) {
p = p.next;
q = q.next;
}
return p;
}else {
return null;
}
}
可能操场那个例子不太准确,下面我写一下数学的推导过程:
假设链表首部到环入口点距离为x,环长为c, 两者在环内相交的点距离环的入口为a,slow表示慢指针走的距离,fast表示快指针走的距离,m,n分别表示快慢指针在相遇时已经走得多少环。
2slow = fast (因为快指针的速度是慢指针速度的2倍)
通过以上推导可以发现链表首部到入口距离的x实则为
c - a
,而相遇点也正好离终点(起点)的距离为
c - a
。所以可以采用同时按1步长前进,相交求得结果。
思路二
对于上图,我们发现只要使两个指针相遇在3节点,即可找到环切点。这时我们发现第一个指针比第二指针多走一个环长,即可在环切点相遇。
我们还是利用2个指针,只要让一个指针先走一个环长的步数,然后2个指针再同时走,即可在环切点相遇。
问题是如何求环长呢?
很简单,我们可以根据预备知识找到碰撞点,然后从碰撞点出发,统计直到下一次到达碰撞点所走的步数即可。
/**
* 计算环长
* @param node
* @return
*/
public int caculateCircleLength(ListNode node) {
ListNode start = node;
int length = 0;
do {
length++;
node = node.next;
}while(node != start);
return length;
}
利用环长求其切点。
/**
* 利用环长法
* @param pHead
* @return
*/
public ListNode EntryNodeOfLoop2(ListNode pHead) {
if(pHead == null || pHead.next == null) {
return null;
}
boolean isIntersect = true;
ListNode p = pHead;
ListNode q = pHead;
while(q != null && q.next != null) {
p = p.next;
q = q.next.next;
if(p == q) {
break;
}
}
isIntersect = p == q ? true : false;
if(isIntersect) {
int circleLength = caculateCircleLength(q);
System.out.println(circleLength);
q = pHead;
while(circleLength-- > 0) {
q = q.next;
}
p = pHead;
while(p != q) {
p = p.next;
q = q.next;
}
return p;
}else {
return null;
}
}
思路三
这种思路会破坏链表的连通性,改进的办法是通过为每个节点添加标志位表明是否被断开即可。但还是不太推荐,仅供学习思想。
我们还是采用2个指针的做法,一个指向当前节点,一个指向当前节点的前驱节点。我们对遍历到每一个当前节点,都进行断链操作,即断开前驱节点到当前节点的连接。这样当当前节点为null,说明我们重新访问到了之前的节点,因为该节点的联通性在之前已经被断开了。
Note: 当前节点为null,也有可能走到了链表末尾,所以我们还是需要先判断是否有环,再采用此方式找切点。
/**
* 断链法
* @param pHead
* @return
*/
public ListNode EntryNodeOfLoop3(ListNode pHead) {
if(pHead == null || pHead.next == null) {
return null;
}
boolean isIntersect = true;
ListNode p = pHead;
ListNode q = pHead;
while(q != null && q.next != null) {
p = p.next;
q = q.next.next;
if(p == q) {
break;
}
}
isIntersect = p == q ? true : false;
if(isIntersect) {
p = pHead;
q = pHead.next;
while(q != null) {
p.next = null;
p = q;
q = q.next;
}
return p;
}else {
return null;
}
}
总结
链表相交的题重在采用多个速度不一的指针来做,至于相交点,则要靠联想能力或数学推导能力。