java数据结构与算法之链表相交问题

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java数据结构与算法之链表相交问题相关的知识,希望对你有一定的参考价值。

两个单向链表相交问题

问题描述:

有两个单向链表他们可能相交,也可能不相交。若相交则返回相交的节点,若不相交则返回null。

两个单向链表相交,有且仅有三种情况。

链表相交的问题算是链表问题中最难的问题之一了!!!!

一、逐一分析三种情况


【情况一】、两个无环链表


在这里插入图片描述

方法一、通过hash表来做

实现流程:

  • 先从头到尾遍历一条链表,在遍历的同时将节点放入到hash表中
  • 然后将第二条链表从头到尾开始遍历,遍历的同时首先查看hash表中有没有该节点
  • 如果有则证明在第一条链表中已经将该节点加入过了,说明该节点是两条链表公用的节点(相交的节点),立即返回该节点即可
  • 直到第二条链表都遍历完了在hash表中都没有找到重复的节点,则说明这两条链表没有相交

代码实现如下:

public static SingleNode findFistIntersectNode01(SingleNode head1, SingleNode head2) {

        if (head1 == null || head2 == null) {
            return null;
        }
        final HashSet<SingleNode> set = new HashSet<>();

        // 先选一条链表,将这条链表的所有节点加入到hash表中
        while (head1 != null) {
            set.add(head1);
            head1 = head1.next;
        }

        // 遍历第二条链表,每遍历一个节点都去set集合中检查一下该节点是否加入过,如果加入过则该节点就是两个链表的相交节点
        while (head2 != null) {
            if (set.contains(head2)) {
                return head2;
            }
            head2 = head2.next;
        }

        // 如果第二条链表都遍历完了都没有在set集合中找到相同的节点,则两条链表不相交
        return null;
    }

node定义

@Data
public class SingleNode<T> {
    public T value;
    public SingleNode next;

    public SingleNode() {}

    public SingleNode(final T data) {
        this.value = data;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || this.getClass() != o.getClass()) return false;
        final SingleNode that = (SingleNode) o;
        return Objects.equals(this.value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.value);
    }

    @Override
    public String toString() {
        return (String) this.value;
    }
}

方法二、利用遍历两个链表的方式来做

实现流程:

  • 通过先遍历两个链表的长度,然后取各自链表的最后一个节点,比较这两个节点是否相等。若相等则相交,反之则不相交。
  • 如果相交,则将长链表的长度 减去 短链表的长度,然后长链表先走差值步。
  • 然后短链表再从链表头开始走,长链表也继续走,直到他们走到的节点内存地址一样,该节点就是他们相交的节点。
/**
 * @param head1 第一个链表的头节点
 * @param head2 第二个链表的头节点
 * @description: 通过遍历两个链表的方式来实现查找两个单向链表相交节点
 * @return: com.wp.algorithm.common.SingleNode 返回相交的节点
 * @date: 2021/3/25 1:28 下午
 * @auther: Mr_wenpan@163.com
 */
public static SingleNode findFistIntersectNode02(final SingleNode head1, final SingleNode head2) {
    if (head1 == null || head2 == null) {
        return null;
    }
    SingleNode current1 = head1;
    SingleNode current2 = head2;
    int listOneLength = 0;
    int listTwoLength = 0;

    // 统计链表1的长度,并且 current1 停在最后一个节点
    while (current1.next != null) {
      listOneLength++;
      current1 = current1.next;
    }
    // 统计链表2的长度,并且 current2 停在最后一个节点
    while (current2.next != null) {
      listTwoLength++;
      current2 = current2.next;
    }

    // 若相交,则两条链表最后一个节点一定是一样的。若不一样,则不相交。
    if (current1 != current2) {
        return null;
    }

    // 求两条链表的长度差值
    int d = listOneLength - listTwoLength;
    // 定义长、短链表指针,分别指向长链表和短链表头
    SingleNode longList;
    SingleNode shortList;
    // 如果d > 0则说明第一条链表是长链表,反之第二条链表是长链表
    if (d > 0) {
        longList = head1;
        shortList = head2;
    } else {
        longList = head2;
        shortList = head1;
    }

    d = Math.abs(d);

    // 长链表先走差值步
    for (int i = 0; i < d; i++) {
        longList = longList.next;
    }

    // 长短链表一起走,直到相遇时退出循环
    while (longList != shortList) {
        longList = longList.next;
        shortList = shortList.next;
    }
    // 返回相遇的节点
    return longList;
}

【情况二】、两个链表有一个公用环(这两个链表入环节点一样)


第一种情况下两个单链表没有环状结构,相遇节点就是相交节点。

在这种情况中,由于两个链表有环,所以就不能统计出每条链表的具体长度了,但仍然可以使用计算链表长度的方法来做。

解决方案有两种:

第一种是使用hash表的方式

第二种是先找到这个两个链表的环的入口节点,然后计算两个链表从头节点到入环节点的长度差值,让长链表先走差值步,然后两个链表指针再一起走,直到相遇,相遇的节点就是他们相交的节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g3GNA9DW-1620723986906)(algorithm-picture/单链表相遇情况2.png)]

方式一、使用hash表来实现

  1. 两条链表同时遍历,并且将两条链表的节点在遍历的同时都放入到hash表中,每放一个节点都去检查一下hash表中是否有同样的节点
  2. 如果有就直接结束遍历,返回该节点即可。

代码如下:

/**
 * @param head1 第一个链表的头节点
 * @param head2 第二个链表的头节点
 * @description: 通过hash表 + 遍历两个链表的方式来实现查找两个单向链表相交节点(相交的两条链表有无环结构都适用)
 * @return: com.wp.algorithm.common.SingleNode 返回相交的节点
 * @auther: Mr_wenpan@163.com
 */
public static SingleNode findFistIntersectNode03(final SingleNode head1, final SingleNode head2) {
    if (head1 == null || head1 == null) {
        return null;
    }
    final HashSet<SingleNode> hashSet = new HashSet<>();
    SingleNode current1 = head1;
    SingleNode current2 = head2;

    while (current1 != null && current2 != null) {
        // 先判断是否存在hash表中,不存在则添加,若存在,则该节点就是两条链表相交的节点
        if (hashSet.contains(current1)) {
            return current1;
        }
        hashSet.add(current1);
        if (hashSet.contains(current2)) {
            return current2;
        }
        hashSet.add(current2);

        current1 = current1.next;
        current2 = current2.next;
    }
    return null;
}

方式二、通过链表长度来寻找链表的相交节点

  • 首先找到这两条链表上的环以及环的入口节点(可以使用hash表或快慢指针来做)。
  • 然后从两条链表的链表头部开始遍历两条链表,一直遍历到环的入口节点位置,统计这段距离的长度n1、n2。
  • 然后求这两条链表的长度差 d = n1 - n2
  • 长的那条链表先走差值d步
  • 然后两条链表同时走,直到两条链表相遇,相遇的节点就是他们相交的节点。

代码实现如下:

/**
 * 情况二、相交有环、且入环的节点和相交节点不同,但入环节点一样
 * 通过遍历两条链表来做
 */
public static SingleNode findFistIntersectNode04(final SingleNode head1, final SingleNode head2) {

    SingleNode cur1 = head1;
    SingleNode cur2 = head2;

    // 两条链表从头节点开始,到两条链表入环节点的长度
    int length1 = 0;
    int length2 = 0;

    // 1、先找到入环节点
    final SingleNode cycleNode1 = findInCycleNode(cur1);
    final SingleNode cycleNode2 = findInCycleNode(cur2);

    // 两个链表不相交的情况
    if (cycleNode1 != cycleNode2
            || cycleNode1 == null
            || cycleNode2 == null) {
        return null;
    }

    // 2、计算两条链表从头节点到入环节点的长度
    while (cur1 != cycleNode1) {
        cur1 = cur1.next;
        length1++;
    }
    while (cur2 != cycleNode1) {
        cur2 = cur2.next;
        length2++;
    }

    // 3、找出长链表 并 计算两条链表从头节点到入环节点的长度差值
    SingleNode longList = length1 > length2 ? head1 : head2;
    SingleNode shortList = longList == head1 ? head2 : head1;
    final int diff = Math.abs(length1 - length2);

    // 4、长链表先走差值步
    for (int i = 0; i < diff; i++) {
        longList = longList.next;
    }

    // 5、长短链表一起走,直到相遇,返回相遇节点即是两条链表的相交节点
    while (longList != shortList) {
        longList = longList.next;
        shortList = shortList.next;
    }

    // 返回两条链表相交节点
    return longList;
}

/**
 * 查找一条链表的入环节点
 */
public static SingleNode findInCycleNode(final SingleNode head) {

    if (head == null) {
        return null;
    }

    SingleNode fast = head;
    SingleNode slow = head;

    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        // 慢指针追上快指针
        if (fast == slow) {
            break;
        }
    }

    // 两个链表不相交的情况
    if (fast == null || fast.next == null) {
        return null;
    }

    // 快指针从头开始走
    fast = head;
    while (fast != slow) {
        fast = fast.next;
        slow = slow.next;
    }
    // 返回相交节点
    return fast;
}

【情况三】、两个链表有一个公用环(这两个链表入环节点不一样)


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XR568Fww-1620723986907)(algorithm-picture/单链表相遇情况3.png)]

这种相交的情况比较特殊:他有两个相交点,并且入环节点都不一样。这两个入环节点都是链表的相交节点,只用返回任意一个即可。

查找步骤如下:

  1. 分别查找出两条链表的入环节点,两个指针分别停留在两个入环节点。
  2. 第一个指针停留在第一个链表的入环节点不动
  3. 第二个指针从第二个链表的入环节点开始一步一步的往下走一圈。
  4. 如果第二个指针在遍历他的环的过程中能遇上第一个入环节点,则返回该节点即可(该节点就是两条链表相交的节点)
  5. 如果第二个指针已经遍历了环一周,任然没有遇到第一个链表的入环节点。则直接返回空即可(说明两条链表不相交)。

代码如下:

public static SingleNode findFistIntersectNode05(final SingleNode head1, final SingleNode head2) {

    // 1、先找到两个链表的入环节点
    final SingleNode cycleNode1 = findInCycleNode(head1);
    final SingleNode cycleNode2 = findInCycleNode(head2);

    // 定义两个指针分别指向两个节点
    SingleNode cur1;
    final SingleNode cur2 = cycleNode2;

    // 入环节点不一样
    if (cycleNode1 != cycleNode2) {
        // 第一个指针停在第一个入环节点不动,第二个指针从第二个入环节点开始绕环一周,看看能不能遇到第一个入环节点
        cur1 = cycleNode2.next;
        while (cur1 != cycleNode2) {
            // 如果cur2在遍历环的时候遇到了cur1,则直接返回cur1,cur1或cur2就是他们相交的节点
            if (cur1 == cur2) {
                return cur1;
            }
            cur1 = cur1.next;
        }
    }
    return null;
}

二、整合情况二和情况三(有环的情况)

上面对链表相交的三种情况逐一做了分析与代码实现,此时我们就应该将三种情况进行整合。这里先整合情况二和情况三,这两种情况是相交且有环的情况。

这里比上面的分情况讨论的代码做了精简!!!!

代码示例

/**
 * @param head1 第一个单向链表的头节点
 * @param loop1 第一个入环节点,需要自己查找然后传入
 * @param head2 第二个单向链表的头节点
 * @param loop2 第二个入环节点,需要自己查找然后传入
 * @description: 查找两个单向链表相遇节点的代码演示,该方法能处理两个链表相交并且有环的情况
 * @return: com.wp.algorithm.common.SingleNode 返回相遇的节点
 * @auther: Mr_wenpan@163.com
 */
public static SingleNode bothLoop(final SingleNode head1, final SingleNode loop1, final SingleNode head2, final SingleNode loop2) {
    SingleNode cur1;
    SingleNode cur2;

    // 一、两个链表的入环节点是同一个
    if (loop1 == loop2) {
        cur1 = head1;
        cur2 = head2;
        int n = 0;

        // 求出两个链表的长度差值n(从链表头节点到入环节点的长度差值 n)
        while (cur1 != loop1) {
            n++;
            cur1 = cur1.next;
        }

        while (cur2 != loop2) {
            n--;
            cur2 = cur2.next;
        }
        // 如果 n>0 则说明head1这条链表比head2这条链表长
        // 这里的作用就是让cur1指向长的那条链表,让cur2指向短的那条链表
        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;
        // cur1从第一个入环节点走一圈
        while (cur1 != loop1) {
            // 如果能遇上第二个入环节点就返回,如果一直遇不上,则说明不存在,直接返回空
            if (cur1 == loop2) {
                return loop1;
            }
            cur1 = cur1.next;
        }
        // 如果找不到,则返回空
        return null;
    }
}

三、整合所有情况

将三种情况做一个统一的整合,整合为一个方法。

代码示例

public static SingleNode findCommonNode(final SingleNode head1, final SingleNode head2) {

    // 1、先找到两个链表的入环节点
    final SingleNode cycleNode1 = findInCycleNode(head1);
    final SingleNode cycleNode2 = findInCycleNode(head2);

    final boolean flag = (cycleNode1 == null && cycleNode2 != null) || (cycleNode2 == null && cycleNode1 != null);

    // 两个链表不相交,一个有环一个无环的情况
    if (flag) {
        return null;
    }

    // 一、没有环的情况
    if (cycleNode1 == null) {
        return findFistIntersectNode02(head1, head2);
    } else {
        // 二、有环的情况
        return bothLoop(head1, cycleNode1, head2, cycleNode2);
    }

}

四、测试

public static void main(final String[] args) {

  	// 创建第一条链表
    final SingleNode<String> node1 = new SingleNode<>("1");
    final SingleNode<String> node2 = new SingleNode<>("2");
    final SingleNode<String> node3 = new SingleNode<>("3");
    final SingleNode<String> node4 = new SingleNode<>("4");
    final SingleNode<String> node5 = new SingleNode<>("5");
    final SingleNode<String> node6 = new SingleNode<>("6");
    node1.next = node2;
    node2.next = node3;
    node3.next = node4;
    node4.next = node5;
    node5.next = node6;
    node6.next = node3;

  	// 创建第二条链表
    final SingleNode<String> node11 = new SingleNode<>("11");
    final SingleNode<String> node12 = new SingleNode<>("12");
    final SingleNode<String> node13 = new SingleNode<>("13");
    final SingleNode<String> node14 = new SingleNode<>("14");
    final SingleNode<String> node15 = new SingleNode<>("15");
    final SingleNode<String> node16 = new SingleNode<>("16");
    node11.next = node12;
    node12.next = node13;
    node13.next = node14;
    node14.next = node15;
    node15.next = node3;

    final SingleNode commonNode = findCommonNode(node1, node11);
    System.out.println(commonNode);
}

以上是关于java数据结构与算法之链表相交问题的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法(Java)之链表

JAVA数据结构与算法之链表

java数据结构与算法之链表找环入口

java之数据结构之链表及包装类包

数据结构与算法之链表

LeetCode系列160. 相交链表