算法图解-约瑟夫环

286 阅读7分钟

算法-约瑟夫环问题

题目: 给定两个可能有环也可能无环的单链表,头结点 head1 和 head2。 请实现一个函数,如果两个链表相交,请返回相交的 第一个节点 。如果不相交,返回 null 【要求】 如果两个链表的长度之和为 N ,时间复杂度请达到 O(N) , 额外空间复杂度请达到 O(1) 。

image-20240226025133721

使用容器定位

  • 思路一 (空间复杂度无法达到要求)使用容器完成约瑟夫环:

    比如无环链表: head->A->B->C->D->E->null 与 有环链表: head->A->B->C->D->B->E->null 可以使用 hash 表进行存储,当因为 hash 表中存储的元素都是唯一的,也就是说当我们在遍历链表的过程当中, 将元素置入 hash 表当中,如果链表遍历但最后一个 null 都没有出现重复的元素,则表示该链表不存在环。 如果在遍历的过程当中,一但出现重复的元素,则表示该链表已经形成了一个环。

    image-20240226025157819

    /**
     * 利用容器实现环的查找
     *
     * @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))使用快慢指针:

    通过快慢两个指针的移动,完成对该链表的遍历。如果慢指针最终指向空,则表示该链表是不存在环的,如果慢指针最终追上了快指针,则表示必定有环,因为当没有环的时候,本身快指针就比慢指针走的快,是不可能相遇的。

    image-20240226025239273

虽然是相遇,但是却无法定位到入环节点。当两个指针相遇的时候,快指针回到开头节点,快指针走一步,而慢指针走两步,而当快慢指针再次相遇的时候,便是入环的节点。

image-20240226025301633

代码示例

/**
 * 利用快慢指针,获取有环链表的入环节点
 * @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;
}

单链表相交问题

  • 思路一: 还是采用容器的方法,在之前将其中一个链表遍历完,再去遍历另一条链表,如果两个链表相交。 必定在容器当中存在该节点,而该节点就是入环节点,代码与之前的一样。

  • 思路二:

    需要分三种情况进行研究

    • 第一种,两个链表的节点都是无环;

      image-20240228003251827

      两个链表都是无环的分别遍历两个链表,直到最后一个节点。如果两个链表是相交的,那么直到最后一个节点,两个链表的最后的节点必定指向的是同一个节点。 确定相交的节点,则只需要确定两个链表的长度,让两个链表中的长链表先走两个链表的差值步,让后在此基础上同时遍历两个链表,直到两个链表的指针指向同一个节点,该节点就是相交节点。

    • 第二种,一个链表有环,一个链表无环;

      这种情况下,两个链表永远不可能相交。一个已经形成环,另一个是单链表。

    • 第三种,两个链表都是有环存在的;

      image-20240228004405091

      两个链表都存在环,有两种相交情况,相交只有一个入环节点,和相交有两个入环节点。

      那么对于第三种情况,我们该怎么判断呢?

      image-20240228162824333

      判断有环链表的相交情况

      ​ 对于第三种情况,仔细观察可以看到。当 链表 A链表 B 的入环节点 loop1==loop2 的时候,一定是属于 图1 当中的情况的。然而进一步对于相交节点的确定只需要将,链表 A 与链表 B 从头结点到入环节点(loop1 or loop2)终止,就会变成入下图的结构,将有环相交变为无环相交的求解问题,而求解过程与两个无环单链表相交的求解过程一致。利用, 指针即可。

      image-20240228164110511转存失败,建议直接上传图片文件

      image-20240228162900133

      ​ 而当 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("无环-无相交元素");
        }
    

    算法文件地址