面試 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->2->3->4->5;
- 現在我們要求倒數第三個結點的值,即順數第 3 個結點,它的值為 3;
- 定義兩個變數 slow、fast,它們均指向結點 1;
- 先讓 fast 向前走 k-1 即 2 步,這時候 fast 指向了第 3 個結點,它的值是 3;
- 現在 fast 和 slow 同步向右移動;
- 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 個結點。
哈哈,和上面的題目僅僅只是把獲得它的值變成了刪除,不少小夥伴肯定都偷著樂了,但南塵還是先提醒大家,不要太得意忘形喲~
好啦,我們們明天再見啦~
我是南塵,只做比心的公眾號,歡迎關注我。
做不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公眾號搜尋「nanchen」關注我的微信公眾號,目前多運營 Android ,儘自己所能為你提升。如果你喜歡,為我點贊分享吧~