面試 7:快慢指標法玩轉連結串列演算法面試(一)

nanchen2251發表於2018-07-12

面試 7:面試常見的連結串列類演算法捷徑

連結串列是我們資料結構面試中比較容易出錯的問題,所以很多面試官總喜歡在這上面下功夫,為了避免出錯,我們最好先進行全面的分析。在實際軟體開發週期中,設計的時間通常不會比編碼的時間短,在面試的時候我們不要著急於寫程式碼,而是一開始仔細分析和設計,這將給面試官留下一個很好的印象。

與其很快寫出一段千瘡百孔的程式碼,不容仔細分析後再寫出健壯性無敵的程式。

面試題:輸入一個單連結串列的頭結點,返回它的中間元素。為了方便,元素值用整型表示。

當應聘者看到這道題的時候,內心一陣狂喜,怎麼給自己遇到了這麼簡單的題。拿起筆就開始寫,先遍歷整個連結串列,拿到連結串列的長度 len,再次遍歷連結串列,位於 len/2 的元素就是連結串列的中間元素。

所以這個題最重要的點就是拿到連結串列的長度 len。而拿到這個 len 也比較簡單,只需要遍歷前設定一個 count 值,遍歷的時候 count++ ,第一次遍歷結束,就拿到單連結串列的長度 len 了。

於是我們很快寫出了這樣的程式碼:

public class Test15 {
    public static class LinkNode {
        int data;
        LinkNode next;

        public LinkNode(int data) {
            this.data = data;
        }
    }

    private static int getTheMid(LinkNode head) {
        int count = 0;
        LinkNode node = head;
        while (head != null) {
            head = head.next;
            count++;
        }
        for (int i = 0; i < count / 2; i++) {
            node = node.next;
        }
        return node.data;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
        System.out.println(getTheMid(head));
    }
}
複製程式碼

面試官看到這個程式碼的時候,他告訴我們上面程式碼迴圈了兩次,但是他期待的只有一次。

於是我們絞盡腦汁,突然想到了網上介紹過的一個概念:快慢指標法

假設我們設定兩個變數 slow、fast 起始都指向單連結串列的頭結點當中,然後依次向後面移動,fast 的移動速度是 slow 的 2 倍。這樣當 fast 指向末尾節點的時候,slow 就正好在正中間了。

想清楚這個思路後,我們很快就能寫出如下程式碼:

public class Test15 {
    public static class LinkNode {
        int data;
        LinkNode next;

        public LinkNode(int data) {
            this.data = data;
        }
    }

    private static int getTheMid(LinkNode head) {
        LinkNode slow = head;
        LinkNode fast = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow.data;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
        System.out.println(getTheMid(head));
    }
}
複製程式碼

快慢指標法舉一反三

快慢指標法 確實在連結串列類面試題中特別好用,我們不妨在這裡舉一反三,對原題稍微修改一下,其實也可以實現。

面試題:給定一個單連結串列的頭結點,判斷這個連結串列是否是迴圈連結串列。

和前面的問題一樣,我們只需要定義兩個變數 slow,fast,同時從連結串列的頭結點出發,fast 每次走連結串列,而 slow 每次只走一步。如果走得快的指標追上了走得慢的指標,那麼連結串列就是環形(迴圈)連結串列。如果走得快的指標走到了連結串列的末尾(fast.next 指向 null)都沒有追上走得慢的指標,那麼連結串列就不是環形連結串列。

有了這樣的思路,實現程式碼那還不是分分鐘的事兒。

public class Test15 {
    public static class LinkNode {
        int data;
        LinkNode next;

        public LinkNode(int data) {
            this.data = data;
        }
    }

    private static boolean isRingLink(LinkNode head) {
        LinkNode slow = head;
        LinkNode fast = head;
        while (slow != null && fast != null && fast.next != null) {
            if (slow == fast || fast.next = slow) {
                return true;
            }
            fast = fast.next.next;
            slow = slow.next;
        }
        return false;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
        System.out.println(isRingLink(head));
        head.next.next.next.next.next = head;
        System.out.println(isRingLink(head));
    }
}
複製程式碼

確實有意思,快慢指標法 再一次利用它的優勢巧妙解決了我們的問題。

快慢指標法的延展

我們上面講解的「快慢指標法」均是一個變數走 1 步,一個變數走 n 步。我們其實還可以擴充它。這個「快慢」並不是說一定要同時遍歷。

比如《劍指Offer》中的第 15 道面試題,就運用到了「快慢指標法」的延展。

面試題:輸入一個單連結串列的頭結點,輸出該連結串列中倒數第 k 個節點的值。

初一看這個似乎並不像我們前面學習到的「快慢指標法」的考察。所以大多數人就迷糊了,進入到常規化思考。依然還是設定一個整型變數 count,然後每次迴圈的時候 count++,拿到連結串列的長度 n。那麼倒數第 k 個節點也就是順數第 n-k+1 個結點。所以我們只需要在拿到長度 n 後再進行一次 n-k+1 次迴圈就可以拿到這個倒數第 k 個節點的值了。

但面試官顯然不會太滿意這個臃腫的解法,他依然希望我們一次迴圈就能搞定這個事。

為了實現只遍歷一次連結串列就能找到倒數第 k 個結點,我們依然可以定義兩個遍歷 slow 和 fast。我們讓 fast 變數先往前遍歷 k-1 步,slow 保持不動。從第 k 步開始,slow 變數也跟著 fast 變數從連結串列的頭結點開始遍歷。由於兩個變數指向的結點距離始終保持在 k-1,那麼當 fast 變數到達連結串列的尾結點的時候,slow 變數指向的結點正好是我們所需要的倒數第 k 個結點。

我們依然可以在心中預設一遍程式碼:

  1. 假設輸入的連結串列是:1->2->3->4->5;
  2. 現在我們要求倒數第三個結點的值,即順數第 3 個結點,它的值為 3;
  3. 定義兩個變數 slow、fast,它們均指向結點 1;
  4. 先讓 fast 向前走 k-1 即 2 步,這時候 fast 指向了第 3 個結點,它的值是 3;
  5. 現在 fast 和 slow 同步向右移動;
  6. fast 再經過了 2 步到達了連結串列尾結點;fast 正好指向了第 3 個結點,這顯然是符合我們的猜想的。

在心中默走了一遍程式碼後,我們顯然很容易寫出下面的程式碼。

public class Test15 {
    public static class LinkNode {
        int data;
        LinkNode next;

        public LinkNode(int data) {
            this.data = data;
        }
    }

    private static int getSpecifiedNodeReverse(LinkNode head, int k) {
        LinkNode slow = head;
        LinkNode fast = head;
        if (fast == null) {
            throw new RuntimeException("your linkNode is null");
        }
        // 先讓 fast 先走 k-1 步
        for (int i = 0; i < k - 1; i++) {
            if (fast.next == null) {
                // 說明輸入的 k 已經超過了連結串列長度,直接報錯
                throw new RuntimeException("the value k is too large.");
            }
            fast = fast.next;

        }
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow.data;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
        System.out.println(getSpecifiedNodeReverse(head, 3));
        System.out.println(getSpecifiedNodeReverse(null, 1));
    }
}
複製程式碼

總結

連結串列類面試題,真是可以玩出五花八門,當我們用一個變數遍歷連結串列不能解決問題的時候,我們可以嘗試用兩個變數來遍歷連結串列,可以讓其中一個變數遍歷的速度快一些,比如一次走兩步,或者是走若干步。我們在遇到這類面試的時候,千萬不要自亂陣腳,學會理性分析問題。

原本是想給我的小夥伴說再見了,但唯恐大家還沒學到真本事,所以在這裡再留一個擴充題。

面試題:給定一個單連結串列的頭結點,刪除倒數第 k 個結點。

哈哈,和上面的題目僅僅只是把獲得它的值變成了刪除,不少小夥伴肯定都偷著樂了,但南塵還是先提醒大家,不要太得意忘形喲~

好啦,我們們明天再見啦~

我是南塵,只做比心的公眾號,歡迎關注我。

面試 7:快慢指標法玩轉連結串列演算法面試(一)
做不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公眾號搜尋「nanchen」關注我的微信公眾號,目前多運營 Android ,儘自己所能為你提升。如果你喜歡,為我點贊分享吧~
nanchen

相關文章