算法题003 -- [判断单链表中是否有环,找到环的入口节点] by java

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/cjh_android/article/details/82899586

程序运行截图:
在这里插入图片描述

public class Node {

	// 这仅仅是一个标识,用来标记这是哪一个节点
	public int tag;

	public Node next;

	public Node(Node next, int tag) {
		this.next = next;
		this.tag = tag;
	}

	// 逻辑断开 思路需要的tag
	public boolean hasNext = true;
}
public class Algorithm3 {

	/**
	 * [题目] 一个链表中包含环,请找出该链表的环的入口结点。
	 */

	public static void main(String[] args) {
		
		Node node = createTestNode();
		Node nodeBySlowFast = isCircleBySlowFast(node);
		System.out.println("isCircleBySlowFast : "
				+ ((nodeBySlowFast == null) ? "这不是一个环链表" : ("当前环链表的入口是: " + nodeBySlowFast.tag)));
		
		node = createTestNode();
		Node nodeByHashCode = isCircleByHashCode(node);
		System.out.println("isCircleByHashCode : "
				+ ((nodeByHashCode == null) ? "这不是一个环链表" : ("当前环链表的入口是: " + nodeByHashCode.tag)));
		
		node = createTestNode();
		Node nodeByHasNext = isCircleByHasNext(node);
		System.out.println("isCircleByHasNext : "
				+ ((nodeByHasNext == null) ? "这不是一个环链表" : ("当前环链表的入口是: " + nodeByHasNext.tag)));
	}

	public static Node createTestNode() {
		Node node12 = new Node(null, 12);
		Node node11 = new Node(node12, 11);
		Node node10 = new Node(node11, 10);
		Node node9 = new Node(node10, 9);
		Node node8 = new Node(node9, 8);
		Node node7 = new Node(node8, 7);
		Node node6 = new Node(node7, 6);
		Node node5 = new Node(node6, 5);
		Node node4 = new Node(node5, 4);
		Node node3 = new Node(node4, 3);
		Node node2 = new Node(node3, 2);
		// 设置环入口
		node12.next = node5;
		Node head = new Node(node2, 1);
		return head;
	}

	/**
	 * 网上做法,其实也是最难的思路:
	 * 难度不在于编码,而在于不借助于除了 Node 之外的思路,寻找环的入口 !!
	 * 判断是否是有环链表:一个快指针和一个慢指针,在环中,快指针肯定会反向追上慢指针。
	 * 寻找环入口:
	 * 两个指针,一个从链表头开始走,一个从环内快慢指针相交的节点走; 
	 * 那么当这两个指针相交的时,此时相交的节点就是环入口。
	 * PS:这个寻找环入口的思路,并不容易,甚至于要理解也是有点困难的, 我自己也没有想到方法,只能引用别人的博客中的思路。
	 * 
	 * @param node
	 * @return
	 */
	public static Node isCircleBySlowFast(Node node) {
		Node slow = node;
		Node fast = node;
		Node p = node;
		while (slow != null && fast.next != null) {
			slow = slow.next;
			fast = fast.next.next;
			if (slow == fast) {
				Node q = fast;
				while(p != q) {
					p = p.next;
					q = q.next;
				}
				return p;
			}
		}
		return null;
	}

	/**
	 * 另外的思路: 
	 * 如果说 快慢指针的做法是较好的空间复杂度的算法, 
	 * 其实我一直觉得可以使用一个字符串记录 hashcode 来判断是否重复 
	 * PS:不过不知道其他语言有没有类似 Java 这种 hashcode 的特性
	 * 
	 * @param node
	 * @return
	 */
	public static Node isCircleByHashCode(Node node) {
		String hashcodes = "";
		String split = " || ";
		Node temp = node;
		while (temp != null) {
			if (hashcodes.contains(split + temp.hashCode())) {
				return temp;
			}
			hashcodes = hashcodes + split + temp.hashCode();
			temp = temp.next;
		}
		return null;
	}

	/**
	 * 其实和 hashcode 是一样的思路: 
	 * 如果说 hashcode 局限性是并不能确定所有的语言都有类似 hashcode 一样的特性
	 * 那么这个方法的思路,则是对于要操作的链表结构允许修改 
	 * 我在节点类 Node 中添加了 hasNext 属性,这是一种 逻辑断开 的思路:
	 * 在我遍历链表的过程中,使用两个指针,因为这是单链不是双链,所以只能用两个指针来处理 
	 * 两个指针分别是 slow 和fast,slow紧跟着fast,相差一个节点的距离
	 * slow 走过节点的时候,会将 hasNext 置为 false,就是告诉你 链表已经断开了 
	 * 当fast 进入环后,遇到的第一个 逻辑断开 的链表节点就是环的入口
	 * 
	 * @param node
	 * @return
	 */
	public static Node isCircleByHasNext(Node node) {
		Node slow = node;
		Node fast = node;
		while (fast != null && fast.next != null) {
			if (!fast.next.hasNext) {
				return fast.next;
			}
			slow = fast;
			slow.hasNext = false;
			fast = fast.next;
		}
		return null;
	}
}

参考博客:判断单链表中是否有环,找到环的入口节点,文章写的很有条理,只是有些地方觉得描述的不够详细,其实我倒是希望能详细些,于是有了下面的补充,其实就是在原作者的基础上,写下自己的理解。


图解 isCircleBySlowFast 的思路

这个方法的思路在代码中已经说过了,判断是否存在环这不必赘述,难点在于怎么找到环入口。从参考的博客中的思路来看,这完全就是数学做题的思维,将所有的已知条件全部列举出来,将所有能找出的公式算出来,然后通过观察公式之间的关系来解题。
需要知道: 单链表中的方向是固定的,也就是说在环内,说道距离,定然是逆时针来计算的,这一点很重要。

设两个指针 slow / fast,链表起始点为 h, 环入口为t,环的长度为 n

在这里插入图片描述

设 h->t 之间的距离为 a, 当slow到t时,fast所处环中的位置记为m1,且t->m1的距离记为b

在这里插入图片描述
从上图中可以看出此时slow前进的距离为a。而fast的前进速度是slow的两倍。环的长度之前说过记为n。另外需要注意的是,当slow前进到t的时候,fast很可能已经在环内跑了好几圈了,设fast已经跑了r圈,于是就有了下面的公式:
slow 前进的距离 = a;
fast 前进的距离 = 2 x slow 前进的距离 = 2a;
同时!! fast 前进距离 = 非环部分 + 环内 = 非环部 + (所跑圈数+t到m1的距离) = a + (nr+b);
最后: 2a = a + (nr+b) --> a = nr +b ;

设slow和fast环内相遇的节点为m2

在这里插入图片描述
从上图可以看出,m2到t的距离,已经标记为b了,这是怎么来的?难道是因为t->m1的距离为b,所以m2->t的距离就是b?完全不是这样!!!
推导:
我们回到当slow到达t节点,fast处在m1位置时。此时fast与slow相距也就是 m1->t 的距离:环长 - t->m1,也就是 n - b。也就是说如果fast想追上slow,必须在前进速度的差值下填补这个距离,那么需要多久能追上 slow 呢?
前进时间 = 距离差值 / 速度差值 = ( n -b ) / ( 2 -1 ) = n-b.
从上面可知,fast想要追上slow必须经过的时长为: n-b。在这个时间里,slow以1的速度,前进了n-b到m2节点。那么m2->t的距离自然就是b了。
其实到了这里,如果不是看到了上文中的博客,我是真的想不到将m2与t关联在一起…臣妾真的做不到…
那么我们把m2与t联系起来,如果从m2到t,需要前进的距离为b,那么不管从m2为起始点,前进多少圈,只要在m2的位置前进b,都会到达t。也就是从m2前进:nx + b ,x为前进了几圈,都会抵达t。
还记得前面的公式吗?a = nr +b
如果从指针p、q分别从h和m2前进,当p到达t时,设前进了 nr+b,那么q也前进了 nr+b,上面已经推导出从m2出发,前进任意 nx+b,都会到达t。换句话说,当p从h出发到达t的同时,q也到达了t!!!
所以,最后以代码的形式表现上面推导的理论,就变成了分别从h和m2遍历,当p、q遍历到同一个节点时,该节点就是环入口。

感谢参考博客博主的文章。

猜你喜欢

转载自blog.csdn.net/cjh_android/article/details/82899586