如何判断两个可能有环的链表是否相交?

1,040 阅读5分钟

前言

本文介绍链表相关内容,判断两个可能存在环形结构的链表是否相交。

正文

给出两个可能有环也可能无环的单链表,头结点分别为head1head2,写出一个函数,如果两个链表相交,请返回相交的第一个节点,如果不相交则返回null

函数实现要求:如果两个链表长度之和为N,时间复杂度要达到O(N),额外空间复杂度要达到O(1)。

思路分析

先看一下有环链表和无环链表的图示:

因为给出的链表并不确定是否包含环形结构,所以,在实现的时候,要先判断链表是否有环。

先实现一个判断是否有环的函数,实现点如下:

  • 如果链表中不含有环形结构,函数返回null
  • 如果链表中含有环形结构,函数返回环形的第一个节点(如图中的d节点)。

判断是否有环

判断是否有环有多种方式,其中一个方式就是使用HashSet结构,循环一个单链表,将一个个节点放入HashSet中,如果循环最后某一个节点的下一个为null,说明这个链表为无环结构。

在放入HashSet前,先判断HashSet中是否含有即将要放入的节点,如果存在的话,则说明这个链表为有环链表。

快慢指针判断

还可以使用快慢指针来判断链表是否有环,判断方式如下:

  • 定义两个指针都指向head头结点
  • 让快指针一次走两个节点,让慢指针一次走一个节点
  • 如果快指针遇到null,说明是无环链表
  • 如果快指针和慢指针能够相遇,说明是有环链表
  • 相遇后,快指针回到头结点慢指针不动,快慢指针再次一个节点一个节点的走
  • 如果快慢指针再次相遇,则,相遇点就是入环的第一个节点。

判断是否相交

判断两个链表是否相交,需要判断几种情况如下:

  • 两个链表都无环
  • 两个链表其中有一个有环,另一个无环

两个链表都无环

两个无环链表相交示意图:

简单的方式实现就是,准备一个HashSet,把其中一个链表节点都放进去,然后遍历两一个链表判断节点是否在HashSet中即可。

不使用HashSet这样的集合怎么做呢,具体步骤如下:

  • 先遍历第一个链表,记录链表长度以及最后一个节点。
  • 再遍历另一个链表,记录链表长度以及最后一个节点。
  • 如果两个链表的最后一个节点是同一个(内存地址相同),则说明相交了。

那怎样找到第一个相交的节点呢?方式也简单,步骤如下:

  • 假设第一个链表长度为50,第二个链表长度为30
  • 第一个链表先走20步,然后两个链表依次一步一步走。
  • 如果遇到的节点是同一个,则这个节点就是相交的第一个节点。

两个链表其中一个有环

这种情况是不存在的,两个链表不可能相交,直接返回空即可。

两个链表都有环

如果两个链表都有环,也需要分情况来讨论:

  • 两个有环链表不相交
  • 两个链表相交在环形之前
  • 两个链表相交在环形上

针对第二种和第三种情况,图示如下:

可以先判断两个链表的入环节点,如果入环节点是同一个,那么就属于第二种情况,这种情况找第一个相交点跟两个无环链表一致。

针对第一种和第三种情况,判断如下:

  • 先找到两个链表的入环节点
  • 遍历其中一个链表的环部,看是否能够找到与另一个入环相等的节点
  • 如果有,则说明相交
  • 如果没有,则不相交

代码实现

先用快慢指针判断下一个链表是否含有环,有环的话返回第一个入环节点,无环的话返回null,代码如下:

public Node getLoopNode(Node head) {
   if (head == null || head.next == null || head.next.next == null) {
      return null;
   }
   // 慢指针
   Node slow = head.next;
   // 快指针
   Node fast = head.next.next;
   while (slow != fast) {
      if (fast.next == null || fast.next.next == null) {
         return null;
      }
      fast = fast.next.next;
      slow = slow.next;
   }
   // 走到这里说明快慢指针能相遇,相遇后,快指针回到头结点
   fast = head;
   while (slow != fast) {
      slow = slow.next;
      fast = fast.next;
   }
   return slow;
}

如果两个链表都没有环,判断是否相交,代码如下:

public Node noLoop(Node head1, Node head2) {
   if (head1 == null || head2 == null) {
      return null;
   }
   Node cur1 = head1;
   Node cur2 = head2;
   int n = 0;
   while (cur1.next != null) {
      n++;
      cur1 = cur1.next;
   }
   while (cur2.next != null) {
      n--;
      cur2 = cur2.next;
   }
   if (cur1 != cur2) {
      return null;
   }
   // n为链表1长度减去链表2长度的值
   cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1
   cur2 = cur1 == head1 ? head2 : head1; // 谁短,谁的头变成cur2
   n = Math.abs(n);
   while (n != 0) {
      n--;
      cur1 = cur1.next;
   }
   while (cur1 != cur2) {
      cur1 = cur1.next;
      cur2 = cur2.next;
   }
   return cur1;
}

两个链表都有环,判断代码如下:

public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
   Node cur1 = null;
   Node cur2 = null;
   if (loop1 == loop2) {
      cur1 = head1;
      cur2 = head2;
      int n = 0;
      while (cur1 != loop1) {
         n++;
         cur1 = cur1.next;
      }
      while (cur2 != loop2) {
         n--;
         cur2 = cur2.next;
      }
      cur1 = n > 0 ? head1 : head2;
      cur2 = cur1 == head1 ? head2 : head1;
      n = Math.abs(n);
      while (n != 0) {
         n--;
         cur1 = cur1.next;
      }
      while (cur1 != cur2) {
         cur1 = cur1.next;
         cur2 = cur2.next;
      }
      return cur1;
   } else {
      cur1 = loop1.next;
      while (cur1 != loop1) {
         if (cur1 == loop2) {
            return loop1;
         }
         cur1 = cur1.next;
      }
      return null;
   }
}

判断逻辑都有了,看下主函数的调用:

public Node getIntersectNode(Node head1, Node head2) {
   if (head1 == null || head2 == null) {
      return null;
   }
   Node loop1 = getLoopNode(head1);
   Node loop2 = getLoopNode(head2);
   if (loop1 == null && loop2 == null) {
      return noLoop(head1, head2);
   }
   if (loop1 != null && loop2 != null) {
      return bothLoop(head1, loop1, head2, loop2);
   }
   return null;
}

总结

本文介绍链表相关内容,判断两个可能存在环形结构的链表是否相交,判断情况有些复杂,需要在考虑的时候仔细分析下,要不然很容易漏掉某一种情况。