面試 7:快慢指針法玩轉鏈表算法面試(一)


面試 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 個結點。

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

好啦,咱們明天再見啦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM