算法-约瑟夫环问题
题目: 给定两个可能有环也可能无环的单链表,头结点 head1 和 head2。 请实现一个函数,如果两个链表相交,请返回相交的 第一个节点 。如果不相交,返回 null 【要求】 如果两个链表的长度之和为 N ,时间复杂度请达到 O(N) , 额外空间复杂度请达到 O(1) 。
使用容器定位
-
思路一 (空间复杂度无法达到要求)使用容器完成约瑟夫环:
比如无环链表: head->A->B->C->D->E->null 与 有环链表: head->A->B->C->D->B->E->null 可以使用 hash 表进行存储,当因为 hash 表中存储的元素都是唯一的,也就是说当我们在遍历链表的过程当中, 将元素置入 hash 表当中,如果链表遍历但最后一个 null 都没有出现重复的元素,则表示该链表不存在环。 如果在遍历的过程当中,一但出现重复的元素,则表示该链表已经形成了一个环。
/**
* 利用容器实现环的查找
*
* @param elements
* @param <E>
* @return
*/
public static <E> E vesselScreen(LinkedList<E> elements) {
HashSet<E> screen = new HashSet<E>();
for (int i = 0; i < elements.getSize(); i++) {
E data = elements.get(i);
if (screen.contains(data))
return data;
screen.add(data);
}
return null;
}
通过快慢指针定位
-
思路二 (空间复杂度 O(1))使用快慢指针:
通过快慢两个指针的移动,完成对该链表的遍历。如果慢指针最终指向空,则表示该链表是不存在环的,如果慢指针最终追上了快指针,则表示必定有环,因为当没有环的时候,本身快指针就比慢指针走的快,是不可能相遇的。
虽然是相遇,但是却无法定位到入环节点。当两个指针相遇的时候,快指针回到开头节点,快指针走一步,而慢指针走两步,而当快慢指针再次相遇的时候,便是入环的节点。
代码示例
/**
* 利用快慢指针,获取有环链表的入环节点
* @param node 有环单链表的头节点
* @return 入环节点
*/
public static <E> Node getLopNode(Node<E> node) {
if (node == null || node.next == null || node.next.next == null) {
return null;
}
Node n1 = node.next; //快
Node n2 = node.next.next; //慢
while (n1 != n2) { // 直到 n1 与 n2 首次相遇
if (n2.next == null || n2.next.next == null) {
return null;
}
n2 = n2.next.next;
n1 = n1.next;
}
n2 = node; // n2 从头结点重新开始
while (n1 != n2) {
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
单链表相交问题
-
思路一: 还是采用容器的方法,在之前将其中一个链表遍历完,再去遍历另一条链表,如果两个链表相交。 必定在容器当中存在该节点,而该节点就是入环节点,代码与之前的一样。
-
思路二:
需要分三种情况进行研究
-
第一种,两个链表的节点都是无环;
两个链表都是无环的分别遍历两个链表,直到最后一个节点。如果两个链表是相交的,那么直到最后一个节点,两个链表的最后的节点必定指向的是同一个节点。 确定相交的节点,则只需要确定两个链表的长度,让两个链表中的长链表先走两个链表的差值步,让后在此基础上同时遍历两个链表,直到两个链表的指针指向同一个节点,该节点就是相交节点。
-
第二种,一个链表有环,一个链表无环;
这种情况下,两个链表永远不可能相交。一个已经形成环,另一个是单链表。
-
第三种,两个链表都是有环存在的;
两个链表都存在环,有两种相交情况,相交只有一个入环节点,和相交有两个入环节点。
那么对于第三种情况,我们该怎么判断呢?
判断有环链表的相交情况
对于第三种情况,仔细观察可以看到。当
链表 A与链表 B的入环节点 loop1==loop2 的时候,一定是属于 图1 当中的情况的。然而进一步对于相交节点的确定只需要将,链表 A 与链表 B 从头结点到入环节点(loop1 or loop2)终止,就会变成入下图的结构,将有环相交变为无环相交的求解问题,而求解过程与两个无环单链表相交的求解过程一致。利用快,慢指针即可。
而当 loop1!=loop2 的时候,将会出现
有环不相交与有环入环节点不同两种情况。而对于这两种情况,如果要进行区分其实也很简单,仔细观察只要找到其中任意一个链表 A 或 B ,从 loop 的入环节点开始循环转完一圈,如果转完一圈发现,在链表 A 环或链表 B 环当中包含对方的 loop 节点,则表示该链表 A 与链表 B 是入环节点不同并相交的,如果不包含则表示两个链表则是单独分离不相交。
代码示例
- 判断两个链表是否相交
/** * 判断两个链表是否相交,如果相交返回相交的第一个节点,如果不想交返回 null * * @param nodeA * @param nodeB * @param <E> * @return */ public static <E> Node noLoop(Node<E> nodeA, Node<E> nodeB) { if (nodeA == null || nodeB == null) return null; Node curA = nodeA; Node curB = nodeB; int n = 0; while (curA != null) { n++; curA = curA.next; } while (curB != null) { n--; curB = curB.next; } if (curA != curB) return null; curA = n > 0 ? nodeA : nodeB; //两链表谁长,谁就作为头节点 curB = curA == nodeA ? nodeB : nodeA; //如果 curA 是指向 nodeA ,那就 curB 就指向 nodeB ,否则相反; n = Math.abs(n); //两个链表相差的步数 while (n != 0) { n++; curA = curA.next; } while (curA != curB) { //直到两链表首次相遇 curA = curA.next; curB = curB.next; } return curA; }- 判断两个有环链表是否相交
/** * 两个有环链表,返回第一个相交节点,如果不相交返回 null * * @param headA 链表A 头节点 * @param loopA 入环节点 A * @param headA 链表B 头节点 * @param loopB 入环节点 B * @return 相交返回相交节点 不相交返回 null */ public static <E> Node bothLoop(Node<E> headA, Node<E> loopA, Node<E> headB, Node<E> loopB) { Node cur1 = null; Node cur2 = null; if (loopA == loopB) { //入环节点相同,采用链表相交的判断方式【长链表先走,短链表再走】 cur1 = headA; cur2 = headB; int n = 0; while (cur1 != loopA) { //直到尾节点相遇 n++; cur1 = cur1.next; } while (cur2 != loopB) { n--; cur2 = cur2.next; } cur1 = n > 0 ? headA : headB; cur2 = cur1 == headA ? headB : headA; Math.abs(n); while (n != 0) { //长链表先走差值步骤 cur1 = cur1.next; n--; } while (cur2 == cur1) { //一起开始,直到相遇 cur1 = cur1.next; cur2 = cur2.next; } return cur1; } else { //如果 链表A 与 链表 B 的入环节点不一样.就会出现两种情况 cur1 = loopA; while (cur1 != loopA) { if (cur1 == loopB) { return loopA; // //[情况2 两个有环链表相交,并且入环节点不同] } cur1 = cur1.next; } // [情况1 两个有环链表不相交] return null; } } -
附录-代码1
-
节点元素
package com.peppa.common; /** * 自定义对的节点元素 * @param <T> */ public class Node<T> { T data; Node<T> next; public Node(T data){ this.data=data; this.next=null; } } -
链表结构
package com.peppa.common; /** * 自定义链表结构 */ public class LinkedList<T> { private Node<T> head; // 头结点 private int length; // 链表长度 // 添加元素到尾巴节点 public void append(Node<T> data) { if (head == null) { head = data; length = 1; return; } Node<T> current = head; while (current.next != null) { // 定位到最后的一个元素 current = current.next; } current.next = data;// 追加到尾部 length++; } //打印链表元素 public void printList() { Node<T> current = head; int coutn = 0; while (current != null && coutn < length) { System.out.print(current.data + " "); current = current.next; coutn++; } System.out.println(); } //根据元素下标获取元素数据 public T get(int index) { Node<T> current = head; if (index > length << 1) { return current.data; } for (int i = 0; i < index; i++) { current = current.next; } return current.data; } public int getSize() { return length; } } -
main
/** * 运行程序 * * @param args */ public static void main(String[] args) { LinkedList<Integer> linkedList = new LinkedList(); Node<Integer> integerNode_1 = new Node<Integer>(1); Node<Integer> integerNode_2 = new Node<Integer>(2); Node<Integer> integerNode_3 = new Node<Integer>(3); Node<Integer> integerNode_4 = new Node<Integer>(4); Node<Integer> integerNode_5 = new Node<Integer>(5); Node<Integer> integerNode_6 = new Node<Integer>(6); Node<Integer> integerNode_7 = new Node<Integer>(7); linkedList.append(integerNode_1); linkedList.append(integerNode_2); linkedList.append(integerNode_3); linkedList.append(integerNode_4); linkedList.append(integerNode_5); linkedList.append(integerNode_6); linkedList.append(integerNode_7); linkedList.append(integerNode_3); linkedList.printList(); Integer data = vesselScreen(linkedList); if (data != null) System.out.println("有环-相交元素为: " + data); else System.out.println("无环-无相交元素"); }算法文件地址