链表问题

一、打印两个有序链表的公共部分

【题目】

  给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。

【分析】

  假设有如下两个有序链表

  

  整个流程是这样的:谁小动谁,一开始1<2,所以head1来到3的位置

  

  此时2<3,所以head2来到3的位置

  

  head1=head2,打印3,并且head1和head2共同往下走一步

  

  重复上述步骤,直到一个指针来到终点位置,整个流程就停止。

  总结:谁小谁就往右移动,如果相等,打印该数,并共同往右移动;head1和head2只要有1个来到终点位置,整个流程就停止。

【代码实现】

public class PrintCommonPart {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public static void printCommonPart(Node head1, Node head2) {
        System.out.print("Common Part: ");
        while (head1 != null && head2 != null) {
            if (head1.value < head2.value) {
                head1 = head1.next;
            } else if (head1.value > head2.value) {
                head2 = head2.next;
            } else {
                System.out.print(head1.value + " ");
                head1 = head1.next;
                head2 = head2.next;
            }
        }
        System.out.println();
    }

    public static void printLinkedList(Node node) {
        System.out.print("Linked List: ");
        while (node != null) {
            System.out.print(node.value + " ");
            node = node.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node node1 = new Node(2);
        node1.next = new Node(3);
        node1.next.next = new Node(5);
        node1.next.next.next = new Node(6);

        Node node2 = new Node(1);
        node2.next = new Node(2);
        node2.next.next = new Node(5);
        node2.next.next.next = new Node(7);
        node2.next.next.next.next = new Node(8);

        printLinkedList(node1);
        printLinkedList(node2);
        printCommonPart(node1, node2);
    }

}

二、判断一个链表是否为回文结构

【题目】

  给定一个链表的头节点head,请判断该链表是否为回文结构。 例如: 1->2->1,返回true。 1->2->2->1,返回true。15->6->15,返回true。 1->2->3,返回false。

【方法一】——额外空间复杂度O(N)

  假设有如下链表

  

  每遍历一个数都往栈压入相应的数

  

  因为栈是先进后出的,所以从栈顶到栈底其实就是原来链表顺序的逆序。

  然后再从头开始遍历链表,每遍历一个数,都从栈中取一个数出来比较,相当于原始顺序跟逆序依次比较,如果比到最后每一步都相等,则该链表是回文的;如果其中有任何一步不相等,就不是回文。

  这个因为要准备一个栈,所以额外空间是O(N)

public class IsPalindromeList {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    /**
     * 需要N的额外空间,空间复杂度为O(N)
     * @param head
     * @return
     */
    public static boolean isPalindrome1(Node head) {
        Stack<Node> stack = new Stack<>();
        Node cur = head;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        while (head != null) {
            if (head.value != stack.pop().value) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

  public static void printLinkedList(Node node) { System.out.print("Linked List: "); while (node != null) { System.out.print(node.value + " "); node = node.next; } System.out.println(); } public static void main(String[] args) { Node head = null; printLinkedList(head); System.out.print(isPalindrome1(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); printLinkedList(head); System.out.print(isPalindrome1(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); printLinkedList(head); System.out.print(isPalindrome1(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); head.next.next = new Node(3); printLinkedList(head); System.out.print(isPalindrome1(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); head.next.next = new Node(2); head.next.next.next = new Node(1); printLinkedList(head); System.out.print(isPalindrome1(head) + " | "); printLinkedList(head); System.out.println("========================="); } }

【方法二】——额外空间复杂度O(N)

  设置两个指针,一个快指针(一次走2步),一个慢指针(一次走1步)。

  快指针走完的时候,慢指针会来到中点的位置;慢指针来到中点位置后,将右半部分压到栈里

  

  快指针走完的时候,慢指针来到3的位置,然后将后面的部分(2,1)压到栈里。

    

  然后前面的部分(1,2)和栈中元素比较,如果每一步都相等,就是回文。

  这个方法的实质就是:假设有一段线,从中点开始,让右半部分折回去,然后每个数再比较,如果每一步都相等,就是回文。

  

  用这个方法的好处是:栈会少一半空间,虽然额外空间还是O(N),但实际的结果是省了一半的空间

public class IsPalindromeList {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }/**
     * 需要n/2的额外空间,但是空间复杂度仍是O(N)
     * @param head
     * @return
     */
    public static boolean isPalindrome2(Node head) {
        if (head == null || head.next == null) {
            return true;
        }
        //慢指针——指向第2个数
        Node slow = head.next;
        //快指针——指向第1个数
        Node fast = head;

        while (fast.next != null && fast.next.next != null) {
            //慢指针一次走一步
            slow = slow.next;
            //快指针一次走两步
            fast = fast.next.next;
        }

        // 上面过程结束之后,slow就是指向中点的后一位.如果是偶数个数的话,那么就是指向后半段的第一个位置
        Stack<Node> stack = new Stack<>();
        while (slow != null) {
            stack.push(slow);
            slow = slow.next;
        }
        while (!stack.isEmpty()) {
            if (head.value != stack.pop().value) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

    public static void printLinkedList(Node node) {
        System.out.print("Linked List: ");
        while (node != null) {
            System.out.print(node.value + " ");
            node = node.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node head = null;
        printLinkedList(head);
        System.out.print(isPalindrome2(head) + " | ");
        printLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        printLinkedList(head);
        System.out.print(isPalindrome2(head) + " | ");
        printLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        head.next = new Node(2);
        printLinkedList(head);
        System.out.print(isPalindrome2(head) + " | ");
        printLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(3);
        printLinkedList(head);
        System.out.print(isPalindrome2(head) + " | ");
        printLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(2);
        head.next.next.next = new Node(1);
        printLinkedList(head);
        System.out.print(isPalindrome2(head) + " | ");
        printLinkedList(head);
        System.out.println("=========================");
    }
}

【方法三】——额外空间复杂度O(1)

  设置两个指针,一个快指针(一次走2步),一个慢指针(一次走1步)
  快指针走完的时候,慢指针会来到中点的位置。从中点位置开始,让右半部分逆序。

  

  然后分别从两边的1开始遍历,往中间逼近,中途有任何一个对不上,就不是回文,如果全部都能对的上,就是回文。
  但是不管是不是回文,你在返回结果前,都要把后半部分调回原来的样子。

public class IsPalindromeList {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }/**
     * 彻底不用额外空间
     * @param head
     * @return
     */
    public static boolean isPalindrome3(Node head) {
        if (head == null || head.next == null) {
            return true;
        }
        //慢指针
        Node slow = head;
        //快指针
        Node fast = head;
        /*
        快指针一次走两步,慢指针一次走一步
        当快指针走完后,慢指针来到中间位置
         */
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        //右半部分的第一个元素
        fast = slow.next;
        slow.next = null;
        Node n3 = null;
        /*
        后半部分逆序
         */
        while (fast != null) {
            n3 = fast.next;
            fast.next = slow;
            slow = fast;
            fast = n3;
        }
        n3 = slow;
        fast = head;
        boolean res = true;
        //检查每一步的数是否相等
        while (slow != null && fast != null) {
            if (slow.value != fast.value) {
                res = false;
                break;
            }
            slow = slow.next;
            fast = fast.next;
        }
        slow = n3.next;
        n3.next = null;
        //返回结果之前,将后半部分逆序的部分调回来
        while (slow != null) {
            fast = slow.next;
            slow.next = n3;
            n3 = slow;
            slow = fast;
        }

        return res;
    }
  
public static void printLinkedList(Node node) { System.out.print("Linked List: "); while (node != null) { System.out.print(node.value + " "); node = node.next; } System.out.println(); } public static void main(String[] args) { Node head = null; printLinkedList(head); System.out.print(isPalindrome3(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); printLinkedList(head); System.out.print(isPalindrome3(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); printLinkedList(head); System.out.print(isPalindrome3(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); head.next.next = new Node(3); printLinkedList(head); System.out.print(isPalindrome3(head) + " | "); printLinkedList(head); System.out.println("========================="); head = new Node(1); head.next = new Node(2); head.next.next = new Node(2); head.next.next.next = new Node(1); printLinkedList(head); System.out.print(isPalindrome3(head) + " | "); printLinkedList(head); System.out.println("========================="); } }

三、将单向链表按某值划分成左边小、中间相等、右边大的形式

【题目】

  给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整 数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于 pivot的节点。除这个要求外,对调整后的节点顺序没有更多的要求。 例如:链表9->0->4->5->1,pivot=3。 调整后链表可以是1->0->4->9->5,也可以是0->1->9->5->4。总之,满 足左部分都是小于3的节点,中间部分都是等于3的节点(本例中这个部分为空),右部分都是大于3的节点即可。对某部分内部的节点顺序不做要求。

【分析】

  最简单的做法:
  把链表的每一个位置放在一个容器里,即生成一个Node类型的数组,然后在数组里面对Node的值进行划分:小于指定值的放左边,等于的放中间,大于的放右边。然后从容器开始重新连接这个链表,连完之后返回即可。此时的额外空间复杂度为O(N)

public class SmallerEqualBigger {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public static Node listPartition1(Node head, int pivot) {
        if (head == null) {
            return head;
        }
        Node cur = head;
        int i = 0;
        while (cur != null) {
            i++;
            cur = cur.next;
        }
        Node[] nodeArr = new Node[i];
        i = 0;
        cur = head;
        //将原链表的每个Node节点放在数组里
        for (i = 0; i != nodeArr.length; i++) {
            nodeArr[i] = cur;
            cur = cur.next;
        }
        arrPartition(nodeArr, pivot);
        //从数组容器里重新连接成链表结构
        for (i = 1; i != nodeArr.length; i++) {
            nodeArr[i - 1].next = nodeArr[i];
        }
        nodeArr[i - 1].next = null;
        return nodeArr[0];
    }

    /**
     * 等同于荷兰国旗问题
     * @param nodeArr
     * @param pivot
     */
    public static void arrPartition(Node[] nodeArr, int pivot) {
        int small = -1;
        int big = nodeArr.length;
        int index = 0;
        while (index != big) {
            if (nodeArr[index].value < pivot) {
                swap(nodeArr, ++small, index++);
            } else if (nodeArr[index].value == pivot) {
                index++;
            } else {
                swap(nodeArr, --big, index);
            }
        }
    }

    public static void swap(Node[] nodeArr, int a, int b) {
        Node tmp = nodeArr[a];
        nodeArr[a] = nodeArr[b];
        nodeArr[b] = tmp;
    }

    public static void printLinkedList(Node node) {
        System.out.print("Linked List: ");
        while (node != null) {
            System.out.print(node.value + " ");
            node = node.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node head1 = new Node(7);
        head1.next = new Node(9);
        head1.next.next = new Node(1);
        head1.next.next.next = new Node(8);
        head1.next.next.next.next = new Node(5);
        head1.next.next.next.next.next = new Node(2);
        head1.next.next.next.next.next.next = new Node(5);
        printLinkedList(head1);
        head1 = listPartition1(head1, 4);
        printLinkedList(head1);
    }
}

【进阶】—— 在原问题的要求之上再增加如下两个要求。
  在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左 到右的顺序与原链表中节点的先后次序一致。 例如:链表9->0->4->5->1,pivot=3。调整后的链表是0->1->9->4->5。 在满足原问题要求的同时,左部分节点从左到右为0、1。在原链表中也 是先出现0,后出现1;中间部分在本例中为空,不再讨论;右部分节点 从左到右为9、4、5。在原链表中也是先出现9,然后出现4,最后出现5。如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。

【分析】

  假设有如下链表,按照4来划分,设置3个引用(small、equal、big)。

  遍历一次链表,找到第一个小于4的节点,找到第一个等于4的节点,找到第一个大于4的节点.。
  再遍历一遍链表,如果找到的是某个区域的头节点,就省略,比如说再遍历的时候找到的是7,就省略,因为这个7和big之前找到的节点是同一个(注意这里比较的是Node的内存地址,而不是Node的值)。遍历到6的时候,6>4,就往大于的区域里放,依次下去。然后再将3个区域连接起来即可

  

public class SmallerEqualBigger {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }/**
     * 不需要额外的空间复杂度,且能达到稳定性
     * @param head
     * @param pivot
     * @return
     */
    public static Node listPartition2(Node head, int pivot) {
        if (head == null) {
            return null;
        }
        //小于部分链表的head和tail
        Node sH = null, sT = null;
        //等于部分链表的head和tail
        Node eH = null, eT = null;
        //大于部分链表的head和tail
        Node bH = null, bT = null;

        //用来保存下一个结点
        Node next = null;

        //划分到三个不同的链表
        while (head != null) {
            next = head.next;
            //为了链表拼接后,最后一个就不用再去赋值其next域为null了
            head.next = null;
            //向small部分 分布
            if (head.value < pivot) {
                //small部分的第一个结点
                if (sH == null) {
                    sH = head;
                    sT = head;
                } else {
                    //把head放到small最后一个
                    sT.next = head;
                    //更新small部分的sT
                    sT = head;
                }
            } else if (head.value == pivot) {
                if (eH == null) {
                    eH = head;
                    eT = head;
                } else {
                    eT.next = head;
                    eT = head;
                }
            } else {
                if (bH == null) {
                    bH = head;
                    bT = head;
                } else {
                    bT.next = head;
                    bT = next;
                }
            }
            head = next;
        }

        ///将三个链表合并(注意边界的判断)
        if (sT != null) {
            //合并small和equal部分
            sT.next = eH;
            eT = eT == null ? sT : eT;
        }
        if (eT != null) {
            eT.next = bH;
        }
        return sH != null ? sH : eH != null ? eH : bH;
    }
  
  public static void printLinkedList(Node node) { System.out.print("Linked List: "); while (node != null) { System.out.print(node.value + " "); node = node.next; } System.out.println(); } public static void main(String[] args) { Node head1 = new Node(7); head1.next = new Node(3); head1.next.next = new Node(4); head1.next.next.next = new Node(6); head1.next.next.next.next = new Node(0); head1.next.next.next.next.next = new Node(4); printLinkedList(head1); head1 = listPartition2(head1, 4); printLinkedList(head1); } }

四、复制含有随机指针节点的链表

【题目】

  一种特殊的链表节点类描述如下:

public class Node {
    public int value;
    public Node next;
    public Node rand;

    public Node(int data) {
        this.value = data;
    }
}

  Node类中的value是节点值,next指针和正常单链表中next指针的意义一 样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。 给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。

【分析】

  这题的题意是,假设有下面链表,节点1的next指向2,节点2的next指向3,节点3的next指向null。另外,假设节点1的rand指针指向节点3,节点2的rand指针指向节点1,节点3的rand指针指向null。

  

  需要做的是:复制该链表的所有结构(深度拷贝),最后返回1'节点

  

  我们可以用HashMap来实现,具体做法是:

  从节点1开始依次遍历到节点3,每遍历一个节点,就克隆出一个新的节点,并把这2个节点放在HashMap中,key是原链表节点,value是新链表节点。

  怎么实现克隆呢?如果一个节点是node,克隆后的节点为newNode,可以将node节点传入newNode节点中:Node newNode=new Node(node.value);

  但是现在对于newNode来说,next指针和rand指针都是null。怎么办呢?我们可以在hash表里记一个记录,例如key就是节点1,value就是节点1’。

  

  再次遍历原始链表,当我得到节点1的时候,在map中可以把节点1‘取出;因为节点1的next指针是指向节点2的,所以拷贝后的节点1‘的next指针也应该指向节点2’。而在map中,我们可以通过节点2找到节点2‘,所以就可以将1’的next指针指向2‘;我们还可以通过节点1的rand指针找到节点3,节点1’的rand指针也应该指向3'节点,其中3‘节点可以在map中找到。这是在遍历节点1的时候,1’的指针是怎么设置的。按照此方式设置2‘和3’的指针。

public class CopyListWithRandom {
    public static class Node {
        public int value;
        public Node next;
        public Node rand;

        public Node(int data) {
            this.value = data;
        }
    }

    /**
     * 利用HashMap,key存的是原链表的节点,value存的是该节点的克隆节点
     * @param head
     * @return
     */
    public static Node copyListWithRand1(Node head) {
        HashMap<Node, Node> map = new HashMap<>();
        Node cur = head;
        while (cur != null) {
            //map的value里面是纯净的节点,开始是没有next跟rand的指向,需要自己去指定
            map.put(cur, new Node(cur.value));
            cur = cur.next;
        }
        Node X = head;
        //以下while跑完后,克隆节点之间的结构就设置完毕了
        while (X != null) {
            // map.get(x)是x的拷贝节点x'
            map.get(X).next = map.get(X.next);
            map.get(X).rand = map.get(X.rand);
            X = X.next;
        }
        return map.get(head);
    }

    public static void printRandLinkedList(Node head) {
        Node cur = head;
        System.out.print("Order: ");
        while (cur != null) {
            System.out.print(cur.value + " ");
            cur = cur.next;
        }
        System.out.println();
        cur = head;
        System.out.print("rand: ");
        while (cur != null) {
            System.out.print(cur.rand == null ? "- " : cur.rand.value + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node head = null;
        Node res = null;
        printRandLinkedList(head);
        res = copyListWithRand1(head);
        printRandLinkedList(res);
        printRandLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(3);
        head.next.next.next = new Node(4);
        head.next.next.next.next = new Node(5);
        head.next.next.next.next.next = new Node(6);

        head.rand = head.next.next.next.next.next; // 1 -> 6
        head.next.rand = head.next.next.next.next.next; // 2 -> 6
        head.next.next.rand = head.next.next.next.next; // 3 -> 5
        head.next.next.next.rand = head.next.next; // 4 -> 3
        head.next.next.next.next.rand = null; // 5 -> null
        head.next.next.next.next.next.rand = head.next.next.next; // 6 -> 4

        printRandLinkedList(head);
        res = copyListWithRand1(head);
        printRandLinkedList(res);
        printRandLinkedList(head);
        System.out.println("=========================");
    }
}

【进阶】
  不使用额外的数据结构,只用有限几个变量,且在时间复杂度为 O(N)内完成原问题要实现的函数。

【分析】

  不用hash表又怎么做呢?还是这个链表,但是拷贝后的节点是放在老链表的下一个,然后再和老链表的下一个相连。注意rand指针原来的位置并没变,只是在原来的节点1和节点2之间加了一个节点1‘。

  

  这样做有什么好处呢?接下来遍历的时候,每一次遍历拿出2个节点,比如拿出节点1和节点1‘。1通过rand指针可以找到3,而3的克隆节点3’就是3的下一个节点,在这种结构中,通过3的next就可以找到3‘,然后把1’的rand指向3‘。第一种方法使用hash表的目的就是为了知道老链表和新链表的对应关系,而用这种方法也能把新老链表的对应关系留下来。按照这种思路,依次拿出节点2和节点2’,节点3和节点3‘即可实现所求。

  

  最后再将新链表和老链表分离出来,整个过程就结束了。

public class CopyListWithRandom {
    public static class Node {
        public int value;
        public Node next;
        public Node rand;

        public Node(int data) {
            this.value = data;
        }
    }public static Node copyListWithRand2(Node head) {
        if (head == null) {
            return null;
        }
        Node cur = head;
        Node next = null;

        //复制链表
        //原来的链表 : 1 -> 2 -> 3 -> 4 -> 5 -> 6
        //复制的链表 : 1 -> 1' -> 2 -> 2' -> 3-> 3' -> 4 -> 4' -> 5 -> 5' -> 6 -> 6'
        while (cur != null) {
            next = cur.next;
            cur.next = new Node(cur.value);
            cur.next.next = next;
            cur = next;
        }
        cur = head;
        //curCopy相当于是一个引线
        Node curCopy = null;

        //设置随机指向
        while (cur != null) {
            next = cur.next.next;
            curCopy = cur.next;
            //注意curCopy的rand指向的是rand.next.因为原节点的next才是纯净的节点,这些next节点到时需要分隔出去
            curCopy.rand = cur.rand != null ? cur.rand.next : null;
            cur = next;
        }
        //这个节点就是分隔出来的链表的头节点
        Node res = head.next;
        cur = head;

        //分离
        while (cur != null) {
            next = cur.next.next;
            curCopy = cur.next;
            //这一步是连接后面的
            cur.next = next;
            curCopy.next = next != null ? next.next : null;
            cur = next;
        }
        return res;
    }

    public static void printRandLinkedList(Node head) {
        Node cur = head;
        System.out.print("Order: ");
        while (cur != null) {
            System.out.print(cur.value + " ");
            cur = cur.next;
        }
        System.out.println();
        cur = head;
        System.out.print("rand: ");
        while (cur != null) {
            System.out.print(cur.rand == null ? "- " : cur.rand.value + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node head = null;
        Node res = null;
        printRandLinkedList(head);
        res = copyListWithRand2(head);
        printRandLinkedList(res);
        printRandLinkedList(head);
        System.out.println("=========================");

        head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(3);
        head.next.next.next = new Node(4);
        head.next.next.next.next = new Node(5);
        head.next.next.next.next.next = new Node(6);

        head.rand = head.next.next.next.next.next; // 1 -> 6
        head.next.rand = head.next.next.next.next.next; // 2 -> 6
        head.next.next.rand = head.next.next.next.next; // 3 -> 5
        head.next.next.next.rand = head.next.next; // 4 -> 3
        head.next.next.next.next.rand = null; // 5 -> null
        head.next.next.next.next.next.rand = head.next.next.next; // 6 -> 4

        printRandLinkedList(head);
        res = copyListWithRand2(head);
        printRandLinkedList(res);
        printRandLinkedList(head);
        System.out.println("=========================");
    }
}

五、两个单链表相交的一系列问题

【题目】

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

【分析】

  先解决一个问题:怎么判断一个链表是否有环?如果一个链表有环,返回第一个入环的节点;如果一个链表无环,返回null;

  方法一:使用hash表。

  在遍历过程中,把每个节点放到hashSet里(没有value,只有key),如果有环的话,就能重复回到一个节点。因为set把你遍历过的节点都放到里面去了,所以就能发现一个节点有没有遍历过。如果发现一个节点有遍历过,就是有环,并且返回第一个入环的节点;如果走到null,就是无环,返回null。注意:set里面存的不是节点的值,而是节点的内存地址。

/**
 * 返回链表的第一个入环节点——使用HashSet,需要额外空间
 * @param head
 * @return
 */
public static Node getFirstLoopNode(Node head) {
    HashSet<Node> set = new HashSet<>();
    while (head != null) {
        if (set.contains(head)) {
            return head;
        }
        set.add(head);
        head = head.next;
    }
    return null;
}

  方法二:不用额外空间,准备两个指针,一个快指针,一个慢指针。

  快指针一次走2步,慢指针一次走1步。如果走的过程中,快指针走到了null,肯定无环;如果有环,快指针和慢指针一定会在环上相遇(相遇并不是值相等,而是内存地址是同一个)。相遇后,快指针回到开头,然后变成每次走一步;接下来,快指针和慢指针一起走,它们会在第一个入环节点处相遇,这是一个数学结论。此过程时间复杂度还是O(N).

  

/**
 * 返回链表的第一个入环节点——使用快慢指针,不需要额外空间
 * @param head
 * @return
 */
public static 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;
}

  现在有两个链表,head1是链表1的头节点,head2是链表2的头节点,调用getLoopNode()函数后,就可以得到head1的第一个入环节点loop1, 也能得到head2整个链表中第一个入环节点loop2,如果loop1=null&&loop2=null,这就是两个无环链表的相交问题。两个无环链表的相交问题有两种可能:一种是不相交,一种是相交

   

  假如有两个无环链表h1、h2,怎么求两条无环链表相交的第一个节点

  方法一:使用hash表

  hash表遍历h1,把h1上的所有节点都加到set里去,然后在遍历h2的过程中,每遍历一个节点,都检查该节点是否存在于set中,如果存在,这个节点就是和h1相交的第一个节点。
  举个例子,第一遍把左侧h1的节点全部放到set中,然后依次遍历右侧的h2节点,前两个节点set中都不存在 ,来到第三个节点时,发现在set中,所以这个节点就是第一个相交的节点。

  

  方法二:不使用hash表

  两个无环链表h1和h2。先遍历h1,因为是无环,所以能找到结尾,统计h1的长度(len1)以及找到h1最后一个节点(e1)。再遍历h2,统计h2的长度(len2)并找到h2最后一个节点(e2),如果e1和e2不是同一个节点,那么h1和h2不可能相交,返回null。如果e1=e2,h1和h2肯定相交,那它俩第一个相交的节点是什么呢?举个例子,如果len1=100,len2=80,还是让h1和h2都从头开始遍历,但是让h1先走20步,然后和h2一起往下走,它俩一定会共同进入第一个相交的节点。

  

/**
 * 两个链表都无环
 * @param head1
 * @param head2
 * @return 返回第一个相交的节点
 */
public static 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;
    }
    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;
}

  如果一个链表有环,另外一个链表无环,不可能相交。如果有相交,都会破坏单链表只有一个next的结构。

  如果两个链表都有环,即loop1!=null,loop2!=null。形成的结构有3种:

  

  怎么判断是哪一种结构?

  如果loop1=loop2,是第二种结构。怎么求相交的第一个节点?把环里的部分忽略掉,把loop1、loop2作为终止,这时候就等同于两个无环链表的相交问题;

  如果loop1!=loop2,让loop1通过next指针往下走,因为loop1是第一个入环的节点,它往下走一定能回到自己,如果它回到自己了,都没有遇到loop2,就是第一种结构;

  如果loop1往下走后,中途遇到了loop2,就是第三种情况。如果中途能遇到loop2,返回loop1作为第一个相交的节点或者返回loop2作为第一个相交的节点都对,因为loop1是距离h1更近的,loop2是距离h2更近的,它们都可以叫做h1和h2两个链表第一个相交的节点。

/**
 * 两个链表都有环
 * @param head1
 * @param loop1 head1的第一个入环节点
 * @param head2
 * @param loop2 head2的第一个入环节点
 * @return
 */
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
    Node cur1 = null;
    Node cur2 = null;
    //如果是第二种结构,把环里的部分忽略掉,把loop1、loop2作为终止,等同于两个无环链表的相交问题
    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 {
        //如果loop1!=loop2,让loop1通过next指针往下走
        cur1 = loop1.next;
        while (cur1 != loop1) {
            //如果loop1走的过程中遇到loop2,就是第三种情况
            if (cur1 == loop2) {
                return loop1;
            }
            cur1 = cur1.next;
        }
        return null;
    }
}

【两个链表相交系列问题的代码实现】

public class FindFirstIntersectNode {
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public static Node getIntersectNode(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        //head1的第一个入环节点
        Node loop1 = getLoopNode(head1);
        //head2的第一个入环节点
        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;
    }public static void main(String[] args) {
        // head1链表:1->2->3->4->5->6->7->null
        Node head1 = new Node(1);
        head1.next = new Node(2);
        head1.next.next = new Node(3);
        head1.next.next.next = new Node(4);
        head1.next.next.next.next = new Node(5);
        head1.next.next.next.next.next = new Node(6);
        head1.next.next.next.next.next.next = new Node(7);

        // head2链表:0->9->8->6->7->null
        Node head2 = new Node(0);
        head2.next = new Node(9);
        head2.next.next = new Node(8);
        //head2的8节点指向head1的6节点
        head2.next.next.next = head1.next.next.next.next.next;
        //打印两个无环链表的第一个相交节点,结果为6
        System.out.println(getIntersectNode(head1, head2).value);

        // head1链表:1->2->3->4->5->6->7->4...
        head1 = new Node(1);
        head1.next = new Node(2);
        head1.next.next = new Node(3);
        head1.next.next.next = new Node(4);
        head1.next.next.next.next = new Node(5);
        head1.next.next.next.next.next = new Node(6);
        head1.next.next.next.next.next.next = new Node(7);
        //7节点的next指向4节点,形成有环
        head1.next.next.next.next.next.next = head1.next.next.next;

        // 0->9->8->2...
        head2 = new Node(0);
        head2.next = new Node(9);
        head2.next.next = new Node(8);
        //head2的8节点的next指针指向head1的2节点
        head2.next.next.next = head1.next;
        //两个有环链表的相交问题(第二种结构)
        System.out.println(getIntersectNode(head1, head2).value);

        // head2链表:0->9->8->6->4->5->6..
        head2 = new Node(0);
        head2.next = new Node(9);
        head2.next.next = new Node(8);
        //head2的8节点的next指针指向head1的6节点
        head2.next.next.next = head1.next.next.next.next.next;
        //两个有环链表的相交问题(第三种结构),返回loop1或loop2
        System.out.println(getIntersectNode(head1, head2).value);
    }

}

 

posted @ 2019-05-25 14:48  yi0123  阅读(257)  评论(0编辑  收藏  举报