【算法自由之路】快慢指针在链表中的妙用(下篇)
继上篇之后,链表这块还有两个相对较难的问题我们继续举例。
问题 1 给定具有 random 指针的 next 方向无环单链表,复制该链表
单听这个问题可能有点懵,这个链表结构我先给出
private static class ListNode {
int val;
ListNode next;
ListNode random;
public ListNode(int val) {
this.val = val;
}
}
注意这里有个要求 random 可以指向 null 或者链表内任意一个节点即 next 这个无环链上的任意一个节点
在不考虑空间复杂度的情况下,一个简单的思路,借助哈希表,在 next 方向上遍历链表并克隆节点,将原节点和克隆节点的映射存入 Map,即 Map<ListNode OldNode, ListNode CopyNode>
再次遍历链表,构建新链表的指针指向即可。时间复杂度 O(N) 空间复杂度 O(N)
舍弃哈希表的方法可以将空间复杂度优化为 O(1), 整体思路为:
- 将原链表复制,其复制节点挂到原节点的 next 指针上,人为构建一个规律性结构
- 遍历新链表,将复制节点的 random 指针设置正确
- 拆分新旧链表,恢复 next 指针的指向
package algorithmic.base;
// 随机链表复制
public class RandomListCopy {
private static class ListNode {
int val;
ListNode next;
ListNode random;
public ListNode(int val) {
this.val = val;
}
}
public static ListNode copy(ListNode head) {
if (head == null) {
return null;
}
ListNode temp = head;
// 首先沿着 next 将每个节点复制,并挂载在原节点的下一个
while (temp != null) {
ListNode next = temp.next;
ListNode copyNode = new ListNode(temp.val);
copyNode.next = next;
temp.next = copyNode;
temp = next;
}
// 调整复制节点的 random 指向
temp = head;
while (temp != null) {
ListNode next = temp.next.next;
temp.next.random = temp.random == null ? null : temp.random.next;
temp = next;
}
// 拆分链表
temp = head;
ListNode copyHeadTemp = head.next;
ListNode copyHead = head.next;
while (copyHeadTemp != null) {
ListNode next = copyHeadTemp.next;
if (next == null) {
temp.next = null;
break;
}
ListNode copyNext = next.next;
temp.next = next;
copyHeadTemp.next = copyNext;
temp = next;
copyHeadTemp = copyNext;
}
return copyHead;
}
public static void outPrintLink(ListNode listNode) {
while (listNode != null) {
System.out.print(" c:" + listNode.val);
if (listNode.random != null)
System.out.print(" r:" + listNode.random.val);
listNode = listNode.next;
}
System.out.println();
}
public static void main(String[] args) {
ListNode a = new ListNode(1);
ListNode b = new ListNode(2);
ListNode c = new ListNode(3);
a.next = b;
a.random = b;
b.random = c;
b.next = c;
c.random = a;
ListNode copy = copy(a);
outPrintLink(a);
outPrintLink(copy);
}
}
问题 2 给定两个有环或者无环的单链表,判如果两个链表相交返回第一个相交点,如果不相交返回 null
要求如果两个链表长度相加为 N 则时间复杂度为 O(N) 空间复杂度 O(1)
这是一道非常好的分类讨论问题,我们在思考的时候容易进入误区,会想象出一些本不存在的链表结构,比如链表有环,如果有环则一定不会出现中间成环后又伸出一个尾巴的情况
-
如果是两个无环链表那么它有两种情况: 相交或者不相交。
-
如果是一个有环链表和一个无环链表那他们一定不相交。
-
如果是两个有环链表则需要讨论:相交或者不相交。 如果相交需要讨论 相交点是否是一个点。
如何判断有环? 快慢指针,追击问题,如果快指针最终能追上慢指针则有环
**进一步,如果有环返回第一个入环节点,否则返回 null?**在快慢指针相遇时,快指针返回头部一次走一步,慢指针在相遇位置继续一次走一步,下一次相遇则为入环节点
**判断两个无环单链表是否相交?**链表尾节点是否相同
package algorithmic.base;
// 两个链表相交问题
public class TwoListCross {
public static class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
@Override
public String toString() {
return "ListNode{" +
"val=" + val +
'}';
}
}
// 给定两个链表头节点,判断是否相交,如果相交返回第一个相交节点,否则返回 null
public static ListNode isCross(ListNode head1, ListNode head2) {
if (head1 == null || head2 == null) {
return null;
}
ListNode ring1 = haveRing(head1);
ListNode ring2 = haveRing(head2);
// 两个无环链表相交问题
if (ring1 == null && ring2 == null) {
ListNode temp1 = head1;
ListNode temp2 = head2;
int count1 = 1;
int count2 = 1;
// 取两个链表最后一个节点,并计数
while (temp1.next != null) {
count1++;
temp1 = temp1.next;
}
while (temp2.next != null) {
count2++;
temp2 = temp2.next;
}
// 尾节点不同说明不相交
if (temp1 != temp2) {
return null;
}
ListNode longList = count1 > count2 ? head1 : head2;
ListNode shortList = count1 > count2 ? head2 : head1;
int needFirstGo = Math.abs(count1 - count2);
// 因为相交最后一定共用,长链表先走差值步,然后一起走,首次相遇点返回即可
for (int i = 0; i < needFirstGo; i++) {
longList = longList.next;
}
while (longList != shortList) {
longList = longList.next;
shortList = shortList.next;
}
return longList;
} else if (ring1 != null && ring2 != null) {
if (ring1 == ring2) {
return ring1;
} else {
ListNode temp = ring1.next;
while (temp != ring1) {
if (temp == ring2) {
// 如果是有环相交,两个链表入环点不同则返回任意一个都可以
return ring2;
}
temp = temp.next;
}
// ring1 走了一圈都没有遇到 ring2 说明两个有环链表不相交
return null;
}
} else {
return null;
}
}
private static ListNode haveRing(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
while (slow != fast) {
// 若有环,相交必追上
if (fast.next == null) {
return null;
}
slow = slow.next;
fast = fast.next.next;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
public static void main(String[] args) {
ListNode a = new ListNode(1);
ListNode b = new ListNode(2);
ListNode c = new ListNode(3);
ListNode d = new ListNode(4);
ListNode e = new ListNode(5);
a.next = b;
b.next = c;
c.next = d;
d.next = e;
e.next = null;
ListNode a2 = new ListNode(10);
ListNode b2 = new ListNode(20);
ListNode c2 = new ListNode(30);
ListNode d2 = new ListNode(40);
ListNode e2 = new ListNode(50);
a2.next = b2;
b2.next = c2;
c2.next = d2;
d2.next = e2;
e2.next = null;
// 无环无相交
System.out.println(isCross(a, a2));
// 无环有相交
c.next = d2;
System.out.println(isCross(a, a2));
// 有环有无相交
c.next = b;
System.out.println(isCross(a, a2));
// 有环有无相交2
c2.next = b2;
System.out.println(isCross(a, a2));
// 有环同节点相交
c2.next = b;
System.out.println(isCross(a, a2));
// 有环不同节点相交
c.next = d;
e.next = b;
c2.next = c;
System.out.println(isCross(a, a2));
}
}
很有意思的一个问题,这里我重新总结一下考点
- 分类讨论:难点在于考虑清楚可能存在的情况,不要被进入思维误区考虑本不存在的可能性。
- 如何判断链表有环并返回入环第一个节点:有环可以使用快慢指针的追击,如果能追上说明一定有环,第一个入环节点是经典追击问题,快指针追击到后回到起点于慢指针保持同一速度,再次相遇即为首次入环点。
- 单链表相交如何确定第一个入环节点:首先借助 Set 结构是简单就能做到的,首先将一个链表的节点全部加入 Set ,遍历第二个链表,首次命中 Set 的即为所求。这个空间不省,省空间的做法是,因为相交最后一定共用,长链表先走差值步,然后一起走,首次相遇点返回即可