劍指 offer (2) -- 鏈表篇

上一篇文章中對劍指 offer 中數組相關的題目進行了概括,這一篇文章是鏈表篇。一樣地,若是各位大佬發現程序有什麼 bug 或其餘更巧妙的思路,歡迎交流學習。java

6. 從尾到頭打印鏈表

題目描述node

輸入一個鏈表的頭節點,從尾到頭打印鏈表的每一個節點的值。算法

這裏能夠用顯式棧,或者遞歸來實現,都比較簡單,也就很少作解釋了。數組

遞歸實現bash

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    if(listNode == null){
        return new ArrayList<>();
    }
    ArrayList<Integer> list = printListFromTailToHead(listNode.next);
    list.add(listNode.val);
    return list;
}
複製代碼

棧實現dom

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> list = new ArrayList<Integer>();
    if(listNode == null){
        return list;
    }
    Deque<Integer> stack = new LinkedList<>();
    ListNode node = listNode;
    while(node != null) {
        stack.push(node.val);
        node = node.next;
    }
    
    while(!stack.isEmpty()) {
        list.add(stack.pop());
    }
    return list;
}
複製代碼

18. 刪除鏈表的節點

題目一描述oop

在 O(1) 時間內刪除鏈表指定節點。給定單鏈表的頭節點引用和一個節點引用,要求在 O(1) 時間內刪除該節點。學習

解題思路ui

通常來講,要在單向鏈表中刪除指定節點,須要獲得被刪除節點的前驅節點。但這須要從頭節點開始順序查找,時間複雜度確定不是 O(1) 了,因此須要換一種思路。spa

咱們能夠將後繼節點的值賦值給要刪除的指定節點,再刪除下一個節點,如此也一樣實現了刪除指定節點的功能。可是還須要注意兩種特殊狀況:

  • 第一種是要刪除的節點是頭節點,這時還須要對鏈表的頭結點進行更新;
  • 第二種是要刪除的節點是尾節點,它沒有下一個節點,這時就只能從頭節點開始順序查找要刪除節點的前驅節點了。

代碼實現

public Node deleteNode(Node head, Node node) {
    if (head == null || node == null) {
        return head;
    }
    if (head == node) {
        // 要刪除的節點是頭節點
        return head.next;
    } else if (node.next == null) {
        // 要刪除的節點是尾節點
        Node cur = head;
        while (cur.next != node) {
            cur = cur.next;
        }
        cur.next = null;
    } else {
        // 要刪除的節點在鏈表中間
        ListNode nextNode = node.next;
        node.val = nextNode.val;
        node.next = nextNode.next;
    }
    return head;
}
複製代碼

這裏除了最後一個節點,其餘節點均可以在 O(1) 時間內刪除,只有要刪除的節點是尾節點時,才須要對鏈表進行遍歷,因此,整體的時間複雜度仍是 O(1)

題目二描述

在一個排序的鏈表中,存在重複的結點,請刪除該鏈表中重複的結點,重複的結點不保留,返回鏈表頭指針。 例如,鏈表1->2->3->3->4->4->5 處理後爲 1->2->5。

解題思路

這裏要刪除排序鏈表中的重複節點,因爲頭節點也可能被刪除,因此須要對頭節點特殊處理,或者添加一個虛擬節點。這裏選擇使用虛擬節點。

因爲這裏須要判斷當前節點和下一個節點的值,因此循環中條件就是要判斷當前節點和下一個節點均不能爲空。若是這兩個值不相等,則繼續遍歷。

若是不相等,則循環判斷跳過連續重複的數個節點,最後 cur 指向這些重複節點的最後一個。因爲重複節點不保留,因此須要讓 pre.next 指向 cur.next,再更新 cur 爲下一個節點 pre.next,進而繼續判斷。

代碼實現

public Node deleteDuplication(Node head) {
    Node dummyHead = new Node(-1);
    dummyHead.next = head;

    Node pre = dummyHead;
    Node cur = head;
    while (cur != null && cur.next != null) {
        if (cur.value != cur.next.value) {
            pre = cur;
            cur = cur.next;
        } else {
            while (cur.next != null && cur.value == cur.next.value) {
                cur = cur.next;
            }
            pre.next = cur.next;
            cur = pre.next;
        }
    }

    return dummyHead.next;
}
複製代碼

這裏雖然有兩層嵌套循環,但實際上只對鏈表遍歷了一遍,因此其時間複雜度爲 O(n)。另外只申請了一個虛擬節點,因此空間複雜度爲 O(1)

22. 鏈表中倒數第 k 個節點

題目描述

輸入一個鏈表,輸出該鏈表中倒數第 k 個結點。(k 從 1 開始)

解題思路

這裏能夠定義兩個指針。第一個指針從鏈表頭開始遍歷,向前移動 k - 1 步。而後從 k 步開始,第二個指針也開始從鏈表頭開始遍歷。

因爲兩個指針的距離爲 k - 1,全部當第一個指針移動到鏈表的尾節點時,第二個指針正好移動到倒數第 k 個節點。

代碼實現

public static ListNode findKthToTail(ListNode head, int k) {
    if (head == null || k <= 0) {
        return null;
    }

    ListNode fast = head;
    for (int i = 0; i < k - 1; i++) {
        if (fast.next == null) {
            return null;
        }
        fast = fast.next;
    }

    ListNode slow = head;
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }

    return slow;
}
複製代碼

23. 鏈表中環的入口節點

題目描述

給一個鏈表,若其中包含環,請找出該鏈表的環的入口結點,不然,輸出null。

解題思路

首先須要判斷鏈表是否有環,可使用兩個指針,同時從鏈表的頭部開始遍歷,一個指針一次走一步,一個指針一次走兩步。若是快指針能追上慢指針,則表示鏈表有環;不然若是快指針走到了鏈表的末尾,表示沒有環。

在找到環以後,定義一個指針指向鏈表的頭節點,再選擇剛纔的慢指針從快慢指針的相遇節點開始,兩個指針同時以每次一步向前移動,它們相遇的節點就是鏈表的入口節點。

代碼實現

public ListNode EntryNodeOfLoop(ListNode pHead) {
    if(pHead == null || pHead.next == null) {
        return null;
    }
    
    ListNode slow = pHead.next;
    ListNode fast = slow.next;
    while(slow != fast) {
        if(fast == null || fast.next == null) {
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    
    ListNode p = pHead;
    while(slow != p) {
        slow = slow.next;
        p = p.next;
    }
    
    return slow;
}
複製代碼

24. 反轉鏈表

題目描述

輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。

循環解決

思路以下圖:

循環代碼

public ListNode reverseList1(ListNode head) {
    ListNode newHead = null;
    ListNode cur = head;
    ListNode nex;
    while (cur != null) {
        nex = cur.next;
        
        cur.next = newHead;
        newHead = cur;
        // 記錄
        cur = nex;
    }
    return newHead;
}
複製代碼

遞歸解決

遞歸代碼

public ListNode reverseList2(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode newHead = reverseList2(head.next);

    head.next.next = head;
    head.next = null;

    return newHead;
}
複製代碼

25. 合併兩個有序的鏈表

題目描述

輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,固然咱們須要合成後的鏈表知足單調不減規則。

循環解題

在使用循環時,首先須要肯定新鏈表的頭節點,若是鏈表 first 的頭節點的值小於鏈表 second 的頭節點的值,那麼鏈表 first 的頭節點即是新鏈表的頭節點。

而後循環處理兩個鏈表中剩餘的節點,若是鏈表 first 中的節點的值小於鏈表 second 中的節點的值,則將鏈表 first 中的節點添加到新鏈表的尾部,不然添加鏈表 second 中的節點。而後繼續循環判斷,直到某一條鏈表爲空。

當其中一條鏈表爲空後,只須要將另外一條鏈表所有連接到新鏈表的尾部。

思路圖以下:

循環代碼

public ListNode merge1(ListNode first, ListNode second) {
    if (first == null) {
        return second;
    }
    if (second == null) {
        return first;
    }
    ListNode p = first;
    ListNode q = second;
    ListNode newHead;
    if (p.val < q.val) {
        newHead = p;
        p = p.next;
    } else {
        newHead = q;
        q = q.next;
    }
    ListNode r = newHead;
    while (p != null && q != null) {
        if (p.val < q.val) {
            r.next = p;
            p = p.next;
        } else {
            r.next = q;
            q = q.next;
        }
        r = r.next;
    }
    if (p == null) {
        r.next = q;
    } else {
        r.next = p;
    }
    return newHead;
}
複製代碼

遞歸解題

使用遞歸解決,比較簡單。首先判斷兩條鏈表是否爲空,若是 first 爲空,則直接返回 second;若是 second 爲空,則直接返回 first

接着判斷鏈表 first 中節點的值和鏈表 second 中節點的值,若是 first 中節點的值較小,則遞歸地求 first.nextsecond 的合併鏈表,讓 first.next 指向新的鏈表頭節點,而後返回 first 便可。

另外一種狀況相似,這裏就再也不贅述了。

遞歸代碼

public ListNode merge2(ListNode first, ListNode second) {
    if (first == null) {
        return second;
    }
    if (second == null) {
        return first;
    }

    if (first.val < second.val) {
        first.next = merge2(first.next, second);
        return first;
    } else {
        second.next = merge2(first, second.next);
        return second;
    }
}
複製代碼

35. 複雜鏈表的複製

題目描述

輸入一個複雜鏈表(每一個節點中有節點值,以及兩個指針,一個指向下一個節點,另外一個特殊指針指向任意一個節點),返回結果爲複製後複雜鏈表的head。

解題思路

這能夠分爲三步來解決。第一步是根據原始鏈表的全部節點,將每一節點的複製節點連接到它的後面。

第二步設置複製出來的節點的特殊指針。若是原始鏈表的節點 p 的特殊指針指向節點 s,則複製出來的節點 cloned 的特殊指針就指向節點 s 的下一個節點。

第三部是將長鏈表拆分紅兩個鏈表,把全部偶數位置的節點鏈接起來就是新的複製出來的鏈表。

代碼實現

public RandomListNode Clone(RandomListNode head) {
    cloneNodes(head);
    connectSiblingNode(head);
    return reconnectNodes(head);
}

private void cloneNodes(RandomListNode head) {
    RandomListNode p = head;
    while(p != null) {
        RandomListNode newNode = new RandomListNode(p.label);
        newNode.next = p.next;
        p.next = newNode;
        p = newNode.next;
    }
}

private void connectSiblingNode(RandomListNode head) {
    RandomListNode p = head;
    while(p != null) {
        RandomListNode cloned = p.next;
        if(p.random != null) {
            cloned.random = p.random.next;
        }
        p = cloned.next;
    }
}

private RandomListNode reconnectNodes(RandomListNode head) {
    RandomListNode p = head;
    
    RandomListNode newHead = null;
    RandomListNode tail = null;
    
    if(p != null) {
        tail = newHead = p.next;
        p.next = tail.next;
        p = p.next;
    }
    
    while(p != null) {
        tail.next = p.next;
        tail = tail.next;
        p.next = tail.next;
        p = p.next;
    }
    
    return newHead;
}
複製代碼

36. 二叉搜索樹與雙向鏈表

題目描述

輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能建立任何新的結點,只能調整樹中結點指針的指向。

解題思路

這裏將二叉搜索樹轉換爲一個排序的雙向鏈表,能夠採用使用遞歸算法。

首先遞歸地轉換左子樹,返回其鏈表頭節點,而後須要遍歷該鏈表,找到鏈表的尾節點,這是爲了和根節點相鏈接。須要讓鏈表的尾節點的 right 指向根節點,讓根節點的 left 指向鏈表的尾節點。

而後遞歸地轉換右子樹,返回其鏈表頭節點,而後須要讓根節點的 right 指向鏈表頭節點,讓鏈表的頭節點指向根節點。

最後判斷若是左子樹轉換的鏈表爲空,則返回以 root 根節點爲頭節點的鏈表,不然返回以左子樹最小值爲頭節點的鏈表。

代碼實現

public TreeNode Convert(TreeNode root) {
    if(root == null) {
        return null;
    }
    
    TreeNode leftHead = Convert(root.left);
    TreeNode leftEnd = leftHead;
    while(leftEnd != null && leftEnd.right != null) {
        leftEnd = leftEnd.right;
    }
    if(leftEnd != null) {
        leftEnd.right = root;
        root.left = leftEnd;
    }
    
    TreeNode rightHead = Convert(root.right);
    if(rightHead != null) {
        root.right = rightHead;
        rightHead.left = root;
    }
    
    return leftHead == null ? root : leftHead;
}
複製代碼

52. 兩個鏈表的第一個公共節點

題目描述

輸入兩個鏈表,找出它們的第一個公共結點。

解題思路

對於兩個鏈表,若是有公共節點,要不它們就是同一條鏈表,要不它們的公共節點必定在公共鏈表的尾部。

能夠遍歷兩個鏈表獲得它們的長度,而後在較長的鏈表上,先走它們的長度差的步數,接着同時在兩個鏈表上遍歷,如此找到的第一個節點就是它們的第一個公共節點。

代碼實現

public ListNode findFirstCommonNode(ListNode first, ListNode second) {
    int length1 = getListLength(first);
    int length2 = getListLength(second);

    ListNode headLongList = first;
    ListNode headShortList = second;
    int diff = length1 - length2;

    if (length1 < length2) {
        headLongList = second;
        headShortList = first;
        diff = length2 - length1;
    }

    for (int i = 0; i < diff; i++) {
        headLongList = headLongList.next;
    }

    while (headLongList != null && headShortList != null) {
        if (headLongList == headShortList) {
            return headLongList;
        }
        headLongList = headLongList.next;
        headShortList = headShortList.next;
    }

    return null;
}

public int getListLength(ListNode head) {
    int length = 0;
    ListNode cur = head;
    while (cur != null) {
        length++;
        cur = cur.next;
    }
    return length;
}
複製代碼
相關文章
相關標籤/搜索