• 链表问题


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

    【题目】

      给定两个有序链表的头指针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);
        }
    
    }
  • 相关阅读:
    C#线程池
    C#.Net前台线程与后台线程的区别
    ASP.NET Core 2.1 : 十五.图解路由(2.1 or earler)(转)
    ASP.NET Core 2.1 : 十四.静态文件与访问授权、防盗链(转)
    ASP.NET Core 2.1 : 十三.httpClient.GetAsync 报SSL错误的问题(转)
    ASP.NET Core 2.1 : 十二.内置日志、使用Nlog将日志输出到文件(转)
    ASP.NET Core 2.1 : 十一. 如何在后台运行一个任务(转)
    ASP.NET Core 2.1 : 十.升级现有Core2.0 项目到2.1(转)
    ASP.NET Core 2.0 : 九.从Windows发布到CentOS的跨平台部署(转)
    ASP.NET Core 2.0 : 八.图说管道,唐僧扫塔的故事(转)
  • 原文地址:https://www.cnblogs.com/yft-javaNotes/p/10737450.html
Copyright © 2020-2023  润新知