[演算法總結] 17 題搞定 BAT 面試——連結串列題

wwwxmu發表於2018-09-04

本文首發於我的個人部落格:尾尾部落

連結串列是面試過程中經常被問到的,這裡把劍指offer 和 LeetCode 中的相關題目做一個彙總,方便複習。

1. 在 O(1) 時間刪除連結串列節點

題目描述:給定單向連結串列的頭指標和一個節點指標,定義一個函式在O(1)時間刪除該節點。 解題思路:常規的做法是從連結串列的頭結點開始遍歷,找到需要刪除的節點的前驅節點,把它的 next 指向要刪除節點的下一個節點,平均時間複雜度為O(n),不滿足題目要求。 那是不是一定要得到被刪除的節點的前一個節點呢?其實不用的。我們可以很方面地得到要刪除節點的下一個節點,如果我們把下一個節點的內容複製到要刪除的節點上覆蓋原有的內容,再把下一個節點刪除,那就相當於把當前要刪除的節點刪除了。舉個例子,我們要刪除的節點i,先把i的下一個節點j的內容複製到i,然後把i的指標指向節點j的下一個節點。此時再刪除節點j,其效果剛好是把節點i給刪除了。 要注意兩種情況:

  1. 如果連結串列中只有一個節點,即頭節點等於要刪除的節點,此時我們在刪除節點之後,還需要把連結串列的頭節點設定為NULL。
  2. 如果要刪除的節點位於連結串列的尾部,那麼它就沒有下一個節點,這時我們就要從連結串列的頭節點開始,順序遍歷得到該節點的前序節點,並完成刪除操作。

參考程式碼

public static ListNode deleteNode(ListNode head, ListNode toBeDeleted) {  
    // 如果輸入引數有空值就返回表頭結點  
    if (head == null || toBeDeleted == null) {  
        return head;  
    }  
    // 如果刪除的是頭結點,直接返回頭結點的下一個結點  
    if (head == toBeDeleted) {  
        return head.next;  
    }  
      // 下面的情況連結串列至少有兩個結點  
    // 在多個節點的情況下,如果刪除的是最後一個元素  
    if (toBeDeleted.next == null) {  
        // 找待刪除元素的前驅  
        ListNode tmp = head;  
        while (tmp.next != toBeDeleted) {  
            tmp = tmp.next;  
        }  
        // 刪除待結點  
        tmp.next = null;  
    }  
    // 在多個節點的情況下,如果刪除的是某個中間結點  
    else {  
        // 將下一個結點的值輸入當前待刪除的結點  
        toBeDeleted.value = toBeDeleted.next.value;  
        // 待刪除的結點的下一個指向原先待刪除引號的下下個結點,即將待刪除的下一個結點刪除  
        toBeDeleted.next = toBeDeleted.next.next;  
    }  
    // 返回刪除節點後的連結串列頭結點  
    return head;  
}  
複製程式碼

2. 翻轉單連結串列

題目描述:輸出一個單連結串列的逆序反轉後的連結串列。 解題思路:用三個臨時指標 prev、cur、next 在連結串列上迴圈一遍即可。

[劍指offer] 從尾到頭列印連結串列 [劍指offer] 反轉連結串列

3. 翻轉部分單連結串列:

題目描述:給定一個單向連結串列的頭結點head,以及兩個整數from和to,在單連結串列上把第from個節點和第to個節點這一部分進行反轉

舉例:1->2->3->4->5->null, from = 2, to = 4 結果:1->4->3->2->5->null

public ListNode reverseBetween(ListNode head, int m, int n) {
    if (head == null) return null;
    if (head.next == null) return head;
    int i = 1;
    ListNode reversedNewHead = null;// 反轉部分連結串列反轉後的頭結點
    ListNode reversedTail = null;// 反轉部分連結串列反轉後的尾結點
    ListNode oldHead = head;// 原連結串列的頭結點
    ListNode reversePreNode = null;// 反轉部分連結串列反轉前其頭結點的前一個結點
    ListNode reverseNextNode = null;
    while (head != null) {
        if (i > n) {
            break;
        }
        if (i == m - 1) {
            reversePreNode = head;
        }
        if (i >= m && i <= n) {
            if (i == m) {
                reversedTail = head;
            }
            reverseNextNode = head.next;
            head.next = reversedNewHead;
            reversedNewHead = head;
            head = reverseNextNode;
        } else {
            head = head.next;
        }
        i++;
    }
    reversedTail.next = reverseNextNode;
    if (reversePreNode != null) {
        reversePreNode.next = reversedNewHead;
        return oldHead;
    } else {
        return reversedNewHead;
    }
}
複製程式碼

4. 旋轉單連結串列

題目描述:給定一個單連結串列,設計一個演算法實現連結串列向右旋轉 K 個位置。 舉例: 給定 1->2->3->4->5->6->NULL, K=3 則4->5->6->1->2->3->NULL 解題思路

  • 方法一 雙指標,快指標先走k步,然後兩個指標一起走,當快指標走到末尾時,慢指標的下一個位置是新的順序的頭結點,這樣就可以旋轉連結串列了。
  • 方法二 先遍歷整個連結串列獲得連結串列長度n,然後此時把連結串列頭和尾連結起來,在往後走n - k % n個節點就到達新連結串列的頭結點前一個點,這時斷開連結串列即可。

方法二程式碼:

public class Solution { {
    public ListNode rotateRight(ListNode head, int k) {
        if (!head) return null;
        int n = 1;
        ListNode cur = head;
        while (cur.next) {
            ++n;
            cur = cur.next;
        }
        cur.next = head;
        int m = n - k % n;
        for (int i = 0; i < m; ++i) {
            cur = cur.next;
        }
        ListNode newhead = cur.next;
        cur.next = NULL;
        return newhead;
    }
};
複製程式碼

5. 刪除單連結串列倒數第 n 個節點

題目描述:刪除單連結串列倒數第 n 個節點,1 <= n <= length,儘量在一次遍歷中完成。 解題思路:雙指標法,找到倒數第 n+1 個節點,將它的 next 指向倒數第 n-1個節點。

[劍指offer] 連結串列中倒數第k個結點

6. 求單連結串列的中間節點

題目描述:求單連結串列的中間節點,如果連結串列的長度為偶數,返回中間兩個節點的任意一個,若為奇數,則返回中間節點。 解題思路:快慢指標,慢的走一步,快的走兩步,當快指標到達尾節點時,慢指標移動到中間節點。

// 遍歷一次,找出單連結串列的中間節點
public ListNode findMiddleNode(ListNode head) {
    if (null == head) {
        return;
    }
    ListNode slow = head;
    ListNode fast = head;
 
    while (null != fast && null != fast.next) {
        fast = fast.next.next;
        slow = slow.next;
    }
    return slow;
}
複製程式碼

7. 連結串列劃分

題目描述: 給定一個單連結串列和數值x,劃分連結串列使得所有小於x的節點排在大於等於x的節點之前。

public class Solution {
    /**
     * @param head: The first node of linked list.
     * @param x: an integer
     * @return: a ListNode 
     */
    public ListNode partition(ListNode head, int x) {
        // write your code here
        if(head == null) return null;
        ListNode leftDummy = new ListNode(0);
        ListNode rightDummy = new ListNode(0);
        ListNode left = leftDummy, right = rightDummy;
        
        while (head != null) {
            if (head.val < x) {
                left.next = head;
                left = head;
            } else {
                right.next = head;
                right = head;
            }
            head = head.next;
        }
        
        right.next = null;
        left.next = rightDummy.next;
        return leftDummy.next;
    }
}
複製程式碼

8. 連結串列求和

題目描述:你有兩個用連結串列代表的整數,其中每個節點包含一個數字。數字儲存按照在原來整數中相反的順序,使得第一個數字位於連結串列的開頭。寫出一個函式將兩個整數相加,用連結串列形式返回和。 解題思路:做個大迴圈,對每一位進行操作:

當前位:(A[i]+B[i])%10 進位:(A[i]+B[i])/10

public class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode c1 = l1;
        ListNode c2 = l2;
        ListNode sentinel = new ListNode(0);
        ListNode d = sentinel;
        int sum = 0;
        while (c1 != null || c2 != null) {
            sum /= 10;
            if (c1 != null) {
                sum += c1.val;
                c1 = c1.next;
            }
            if (c2 != null) {
                sum += c2.val;
                c2 = c2.next;
            }
            d.next = new ListNode(sum % 10);
            d = d.next;
        }
        if (sum / 10 == 1)
            d.next = new ListNode(1);
        return sentinel.next;
    }
}
複製程式碼

9. 單連結串列排序

題目描述:在O(nlogn)時間內對連結串列進行排序。 快速排序

public ListNode sortList(ListNode head) {
    //採用快速排序
   quickSort(head, null);
   return head;
}
public static void quickSort(ListNode head, ListNode end) {
    if (head != end) {
        ListNode node = partion(head, end);
        quickSort(head, node);
        quickSort(node.next, end);
    }
}

public static ListNode partion(ListNode head, ListNode end) {
    ListNode p1 = head, p2 = head.next;

    //走到末尾才停
    while (p2 != end) {

        //大於key值時,p1向前走一步,交換p1與p2的值
        if (p2.val < head.val) {
            p1 = p1.next;

            int temp = p1.val;
            p1.val = p2.val;
            p2.val = temp;
        }
        p2 = p2.next;
    }

    //當有序時,不交換p1和key值
    if (p1 != head) {
        int temp = p1.val;
        p1.val = head.val;
        head.val = temp;
    }
    return p1;
}
複製程式碼

歸併排序

public ListNode sortList(ListNode head) {
    //採用歸併排序
    if (head == null || head.next == null) {
        return head;
    }
    //獲取中間結點
    ListNode mid = getMid(head);
    ListNode right = mid.next;
    mid.next = null;
    //合併
    return mergeSort(sortList(head), sortList(right));
}

/**
 * 獲取連結串列的中間結點,偶數時取中間第一個
 *
 * @param head
 * @return
 */
private ListNode getMid(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    //快慢指標
    ListNode slow = head, quick = head;
    //快2步,慢一步
    while (quick.next != null && quick.next.next != null) {
        slow = slow.next;
        quick = quick.next.next;
    }
    return slow;
}

/**
 *
 * 歸併兩個有序的連結串列
 *
 * @param head1
 * @param head2
 * @return
 */
private ListNode mergeSort(ListNode head1, ListNode head2) {
    ListNode p1 = head1, p2 = head2, head;
   //得到頭節點的指向
    if (head1.val < head2.val) {
        head = head1;
        p1 = p1.next;
    } else {
        head = head2;
        p2 = p2.next;
    }

    ListNode p = head;
    //比較連結串列中的值
    while (p1 != null && p2 != null) {

        if (p1.val <= p2.val) {
            p.next = p1;
            p1 = p1.next;
            p = p.next;
        } else {
            p.next = p2;
            p2 = p2.next;
            p = p.next;
        }
    }
    //第二條連結串列空了
    if (p1 != null) {
        p.next = p1;
    }
    //第一條連結串列空了
    if (p2 != null) {
        p.next = p2;
    }
    return head;
}
複製程式碼

10. 合併兩個排序的連結串列

題目描述:輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。

[劍指offer] 合併兩個排序的連結串列

11. 複雜連結串列的複製

題目描述:輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為複製後複雜連結串列的head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)

[劍指offer] 複雜連結串列的複製

12. 刪除連結串列中重複的結點

題目描述:在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5

[劍指offer] 刪除連結串列中重複的結點

13. 判斷單連結串列是否存在環

題目描述:判斷一個單連結串列是否有環 分析:快慢指標,慢指標每次移動一步,快指標每次移動兩步,如果存在環,那麼兩個指標一定會在環內相遇。

14. 單連結串列是否有環擴充套件:找到環的入口點

題目描述:判斷單連結串列是否有環,如果有,找到環的入口點 解題思路:在第 5 題兩個指標相遇後,讓其中一個指標回到連結串列的頭部,另一個指標在原地,同時往前每次走一步,當它們再次相遇時,就是在環路的入口點。

[劍指offer] 連結串列中環的入口結點

15. 判斷兩個無環單連結串列是否相交

題目描述:給出兩個無環單連結串列 解題思路

  • 方法一 最直接的方法是判斷 A 連結串列的每個節點是否在 B 連結串列中,但是這種方法的時間複雜度為 O(Length(A) * Length(B))。
  • 方法二 轉化為環的問題。把 B 連結串列接在 A 連結串列後面,如果得到的連結串列有環,則說明兩個連結串列相交。可以之前討論過的快慢指標來判斷是否有環,但是這裡還有更簡單的方法。如果 B 連結串列和 A 連結串列相交,把 B 連結串列接在 A 連結串列後面時,B 連結串列的所有節點都在環內,所以此時只需要遍歷 B 連結串列,看是否會回到起點就可以判斷是否相交。這個方法需要先遍歷一次 A 連結串列,找到尾節點,然後還要遍歷一次 B 連結串列,判斷是否形成環,時間複雜度為 O(Length(A) + Length(B))。
  • 方法三 除了轉化為環的問題,還可以利用“如果兩個連結串列相交於某一節點,那麼之後的節點都是共有的”這個特點,如果兩個連結串列相交,那麼最後一個節點一定是共有的。所以可以得出另外一種解法,先遍歷 A 連結串列,記住尾節點,然後遍歷 B 連結串列,比較兩個連結串列的尾節點,如果相同則相交,不同則不相交。時間複雜度為 O(Length(A) + Length(B)),空間複雜度為 O(1),思路比解法 2 更簡單。

方法三的程式碼:

public boolean isIntersect(ListNode headA, ListNode headB) {
    if (null == headA || null == headB) {
        return false;
    }
    if (headA == headB) {
        return true;
    }
    while (null != headA.next) {
        headA = headA.next;
    }
    while (null != headB.next) {
        headB = headB.next;
    }
    return headA == headB;
}
複製程式碼

16. 兩個連結串列相交擴充套件:求兩個無環單連結串列的第一個相交點

題目描述:找到兩個無環單連結串列第一個相交點,如果不相交返回空,要求線上性時間複雜度和常量空間複雜度內完成。 解題思路

  • 方法一 如果兩個連結串列存在公共結點,那麼它們從公共結點開始一直到連結串列的結尾都是一樣的,因此我們只需要從連結串列的結尾開始,往前搜尋,找到最後一個相同的結點即可。但是題目給出的單向連結串列,我們只能從前向後搜尋,這時,我們就可以藉助棧來完成。先把兩個連結串列依次裝到兩個棧中,然後比較兩個棧的棧頂結點是否相同,如果相同則出棧,如果不同,那最後相同的結點就是我們要的返回值。
  • 方法二 先找出2個連結串列的長度,然後讓長的先走兩個連結串列的長度差,然後再一起走,直到找到第一個公共結點。
  • 方法三 由於2個連結串列都沒有環,我們可以把第二個連結串列接在第一個連結串列後面,這樣就把問題轉化為求環的入口節點問題。
  • 方法四 兩個指標p1和p2分別指向連結串列A和連結串列B,它們同時向前走,當走到尾節點時,轉向另一個連結串列,比如p1走到連結串列 A 的尾節點時,下一步就走到連結串列B,p2走到連結串列 B 的尾節點時,下一步就走到連結串列 A,當p1==p2 時,就是連結串列的相交點

方法四的程式碼:

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (null == headA || null == headB) {
        return null;
    }
    if (headA == headB) {
        return headA;
    }

    ListNode p1 = headA;
    ListNode p2 = headB;
    while (p1 != p2) {
        // 遍歷完所在連結串列後從另外一個連結串列再開始
        // 當 p1 和 p2 都換到另一個連結串列時,它們對齊了:
        // (1)如果連結串列相交,p1 == p2 時為第一個相交點
        // (2)如果連結串列不相交,p1 和 p2 同時移動到末尾,p1 = p2 = null,然後退出迴圈
        p1 = (null == p1) ? headB : p1.next;
        p2 = (null == p2) ? headA : p2.next;
    }
    return p1;
}
複製程式碼

[劍指offer] 兩個連結串列的第一個公共結點

17. 兩個連結串列相交擴充套件:判斷兩個有環單連結串列是否相交

題目描述:上面的問題是針對無環連結串列的,如果是連結串列有環呢? 解題思路:如果兩個有環單連結串列相交,那麼它們一定共有一個環,即環上的任意一個節點都存在於兩個連結串列上。因此可以先用之前快慢指標的方式找到兩個連結串列中位於環內的兩個節點,如果相交的話,兩個節點在一個環內,那麼移動其中一個節點,在一次迴圈內肯定可以與另外一個節點相遇。

相關文章