鏈表問題

1、打印兩個有序鏈表的公共部分

【題目】node

  給定兩個有序鏈表的頭指針head1和head2,打印兩個鏈表的公共部分。數組

【分析】數據結構

  假設有以下兩個有序鏈表dom

  

  整個流程是這樣的:誰小動誰,一開始1<2,因此head1來到3的位置函數

  

  此時2<3,因此head2來到3的位置oop

  

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

  

  重複上述步驟,直到一個指針來到終點位置,整個流程就中止。spa

  總結:誰小誰就往右移動,若是相等,打印該數,並共同往右移動;head1和head2只要有1個來到終點位置,整個流程就中止。3d

【代碼實現】指針

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);
    }

}

2、判斷一個鏈表是否爲迴文結構

【題目】

  給定一個鏈表的頭節點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("========================="); } }

3、將單向鏈表按某值劃分紅左邊小、中間相等、右邊大的形式

【題目】

  給定一個單向鏈表的頭節點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;中間部分在本例中爲空,再也不討論;右部分節點 從左到右爲九、四、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); } }

4、複製含有隨機指針節點的鏈表

【題目】

  一種特殊的鏈表節點類描述以下:

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("=========================");
    }
}

5、兩個單鏈表相交的一系列問題

【題目】

  在本題中,單鏈表可能有環,也可能無環。給定兩個單鏈表的頭節點 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,這就是兩個無環鏈表的相交問題。兩個無環鏈表的相交問題有兩種可能:一種是不相交,一種是相交

   

  假若有兩個無環鏈表h一、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,是第二種結構。怎麼求相交的第一個節點?把環裏的部分忽略掉,把loop一、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;
    //若是是第二種結構,把環裏的部分忽略掉,把loop一、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);
    }

}
相關文章
相關標籤/搜索