面試 14:合併兩個排序鏈表

終於又回到了咱們的算法習題講解了。南塵發現最近文章閱讀量明顯比之前少了很多,就上門請教小夥伴緣由。他們都說做爲一名 Android 應用開發工程師,實在是在工做中沒有接觸到算法。作技術這個東西,學習了仍是得練,不練過幾天必定會忘掉。java

不過想必你們讀南塵的文章也是深有所感,基本都是站在一個極其普通的程序員角度思考的,層次感也不會突如其來。因此,你們還望多多思考呀,算法這個東西是練出來的,不是看出來的。程序員

不過呢,不喜歡算法系列推文的小夥伴也大可沒必要擔憂,在算法以後的板塊就是 Java 基礎啦。面試

有句話說的好,面試造航母,入職擰螺絲。實際上也是這樣,面試官極難經過簡單的面試瞭解到你這我的的能力,而手寫算法倒是最適合看出一我的寫代碼的習慣和程序思惟的,這也是大公司以及很多小公司慢慢轉向算法面試的一個緣由吧~算法

好了,話很少說,咱們直接來看今天的面試題。學習

面試題:輸入兩個遞增排序的單鏈表,data 域爲 int 型值。合併這兩個鏈表,並使新鏈表中的結點也是按照遞增排序的。例如鏈表 A:1->3->5,鏈表 B:2->4,那它們合併後就是 1->2->3->4->5測試

看到這樣的題,咱們必定要學會先在心中想好測試用例,再思考程序邏輯。寫完程序邏輯後,再把本身事先想好的測試用例測試經過後再交給面試官。事實上,面試官也是事先準備了測試用例的。this

而對於測試用例,就必定要注意好以前南塵給你們講的邊界值。只有能經過邊界值、錯誤值的程序,才擁有足夠的健壯性。spa

咱們回到題幹,輸入的是兩個遞增排序的單鏈表,咱們須要合併它,獲得的新鏈表也是遞增排序的。在心中不難擁有這樣的思路。指針

  1. 假設單鏈表 A:1->3,單鏈表 B:2->4->5;
  2. 先比較 A、B 鏈表的頭結點,這裏 1 < 2,因此把 1 做爲新鏈表的頭結點;
  3. 1 從 A 鏈表脫離,3 成爲了 A 鏈表的頭結點;
  4. 再進行第二步的比較,3 > 2,把 B 鏈表的頭結點值 2 接到新鏈表上,獲得 1->2;
  5. 一直執行相似 2~4 的步驟,直到 A 鏈表脫離完,新鏈表是 1->2->3 ,B 鏈表是 4->5,A 鏈表已經爲 null;
  6. 此時直接把 B 鏈表所有接到新鏈表上,獲得最後結構 1->2->3->4->5;
  7. 咱們不得不想到邊界值和錯誤值,假設輸入的 A 鏈表爲 null,則新鏈表直接爲 B。B 鏈表爲 null,新鏈表爲 A。 A、B 都爲 null,則新鏈表也爲 null。

咱們既然是重複的步驟,那咱們必定首先能想到遞歸的思路。咱們極容易獲得下面的代碼。code

public class Test14 { public static class Node { int data; Node next; Node(int data) { this.data = data; } } private static Node merge(Node head1, Node head2) { // 若是鏈表 1 爲 null ,新鏈表直接爲 2 鏈表; if (head1 == null) return head2; // 若是鏈表 2 爲 null,則新鏈表直接爲 1 鏈表 if (head2 == null) return head1; Node head = null; // 假設鏈表 1 的頭結點值小於等於鏈表 2;則直接把鏈表 1 的頭結點賦值爲新鏈表,並遞歸新的 1 鏈表 if (head1.data <= head2.data) { head = head1; head.next = merge(head1.next, head2); } else { // 不然,對鏈表 2 執行一樣的操做,並把脫離的值賦值上去 head = head2; head.next = merge(head1, head2.next); } return head; } public static void main(String[] args) { Node head1 = new Node(1); head1.next = new Node(3); head1.next.next = new Node(5); head1.next.next.next = new Node((7)); Node head2 = new Node(2); head2.next = new Node(4); head2.next.next = new Node(6); head2.next.next.next = new Node(8); Node head = merge(head1, head2); while (head != null) { System.out.print(head.data + "->"); head = head.next; } System.out.print("null"); } } 

寫出了代碼後,天然不能忘了用事先想好的測試用例去測試一遍流程,這裏測試是沒有問題的。

事實上,這也是《劍指 Offer》的標準解法。遞歸解法有個好處,就是比較容易想到,但這應該是創建在建模能力比較強的基礎之上。但每每很多人會以爲遞歸特別繞,容易把人繞暈,並且在空間利用率上一直都表現很差,有的面試官就是不能接受遞歸的代碼,因此本題咱們更加推薦的是迭代的方式處理。

迭代處理

回到咱們前面的思路中,上面咱們用的方式是不斷「脫落」,而後把暴露出來的結點當作一個新的頭結點來處理,相信很多小夥伴不怎麼能接受這個思路。那咱們換個更好理解的方式,咱們就直接用咱們鏈表經常使用的指針移動的方式來處理,咱們看行不行。

因此咱們開始直接編寫。

private static Node merge(Node head1, Node head2) { if (head1 == null) return head2; if (head2 == null) return head1; // 上面的不用說,就是處理傳入值爲 null 的狀況 Node head = null; // 當兩個鏈表都不爲空就能夠比較大小來肯定接哪一個 while (head1 != null && head2 != null) { if (head1.data <= head2.data) { head = head1; head1 = head1.next; } else { head = head2; head2 = head2.next; } } // 若是第一個鏈表的元素未處理完畢,則把剩餘的鏈表接到最後一個鏈表後 if (head1 != null) head.next = head1; // 同理,若是第二個鏈表的元素未處理完畢,就把剩餘的鏈表接到新鏈表的尾結點 if (head2 != null) head.next = head2; return head; } 

一樣寫完後,拿出咱們的測試用例開始測試,首先確定是功能測試。

  1. 假定咱們的鏈表 A 爲:1->3,鏈表 B 爲: 2->4->5;
  2. 都不爲空進入 while 循環,由於 1 < 2,執行 head = head1,因此新鏈表爲:1->3->null;head1 = head1.next,指針後移;
  3. 依然知足 whild 循環條件,由於 3 > 2,因此新鏈表爲,head = head2 ;等等,這裏出了問題,我根本沒接到前面放的 head1 的後面,因此這樣的賦值明顯是不對的。

功能測試就出了問題,咱們固然得思考如何修改,正常來講,咱們但願新鏈表的 1.next = 2,因此咱們確定不能直接用 head = head2 這樣的表達式來進行賦值。

咱們必定的有個相似 head.next = head2,而後用相似 head = head.next 這樣的方式向後遍歷纔是正確的。因此咱們修改一下代碼:

private static Node merge(Node head1, Node head2) { if (head1 == null) return head2; if (head2 == null) return head1; // 上面的不用說,就是處理傳入值爲 null 的狀況 // 爲了下面的 head.next,因此咱們首先確定得初始化一個 Node Node head = new Node(); // 當兩個鏈表都不爲空就能夠比較大小來肯定接哪一個 while (head1 != null && head2 != null) { if (head1.data <= head2.data) { head.next = head1; head1 = head1.next; } else { head.next = head2; head2 = head2.next; } head = head.next; } // 若是第一個鏈表的元素未處理完畢,則把剩餘的鏈表接到最後一個鏈表後 if (head1 != null) head.next = head1; // 同理,若是第二個鏈表的元素未處理完畢,就把剩餘的鏈表接到新鏈表的尾結點 if (head2 != null) head.next = head2; return head; } 

再次進行功能測試:

  1. 假定 A:1->3,B:2->4->5,先初始化 head,最開始確定兩個都不爲空的,因此直接比較;
  2. 由於 1 < 2 , 因此新鏈表爲 0->1->3->null,鏈表 A 指針後移;head = head.next,因此新的 head 值爲 1;
  3. 繼續循環,由於 3 > 2,因此獲得新的 head.next = head2, 即有 1->2;
  4. 以此類推,最終獲得 head 一直獲得的歷史爲 1,2,3。此時鏈表 A 已經遍歷完,鏈表 B 的指針指向 4;
  5. 退出 while 循環,知足 head2 != null,接到 head 後面, head.next = head2,此時的 head2 裏面還剩下 4->5->null;
  6. 返回 head。等等,這裏仍是有問題,咱們一直在讓新鏈表的指針後移,這樣返回出來的就是尾結點了。但咱們想要的答案必須是頭結點才能夠遍歷;

因此咱們必須在 while 循環前面放頭結點的值。須要注意的是,因爲咱們要用到 head.next,因此咱們最後真正的頭結點其實是 head.next。

完整代碼以下:

public class Test14 { public static class Node { int data; Node next; Node(int data) { this.data = data; } Node() { } } private static Node merge(Node head1, Node head2) { if (head1 == null) return head2; if (head2 == null) return head1; // 上面的不用說,就是處理傳入值爲 null 的狀況 // 爲了下面的 head.next,因此咱們首先確定得初始化一個 Node Node head = new Node(); Node temp = head; // 當兩個鏈表都不爲空就能夠比較大小來肯定接哪一個 while (head1 != null && head2 != null) { if (head1.data <= head2.data) { head.next = head1; head1 = head1.next; } else { head.next = head2; head2 = head2.next; } head = head.next; } // 若是第一個鏈表的元素未處理完畢,則把剩餘的鏈表接到最後一個鏈表後 if (head1 != null) head.next = head1; // 同理,若是第二個鏈表的元素未處理完畢,就把剩餘的鏈表接到新鏈表的尾結點 if (head2 != null) head.next = head2; return temp.next; } public static void main(String[] args) { Node head1 = new Node(1); head1.next = new Node(3); head1.next.next = new Node(5); head1.next.next.next = new Node((7)); Node head2 = new Node(2); head2.next = new Node(4); head2.next.next = new Node(6); head2.next.next.next = new Node(8); Node head = merge(head1, head2); while (head != null) { System.out.print(head.data + "->"); head = head.next; } System.out.print("null"); } } 

寫完後,仍是得過一遍測試用例。

  1. 當傳入的 head1 或者 head2 爲 null 的時候,直接返回另一條鏈表,都爲 null ,就返回 null;
  2. 當傳入 A:1->3,B:2->4->5,上面已經例證,符合條件;
  3. 當傳入相等值:A:1->3->5,B:1->2->4,符合條件;
  4. 當傳入兩個鏈表均只有一個結點的,A:1,B:2,符合條件。

基本仍是沒毛病,這時候咱們就能夠交上本身的答卷啦~

基本步驟很清晰:思考測試用例 => 思考程序邏輯 => 拿測試用例進去驗證 => 發現問題直接更改 => 測試用例驗證,注意邊界值 => 測試經過。

若是保持上面這樣的步驟,實在想不明白,在紙上畫畫思路,相信即便你沒有作到 100% 正確,你給面試官的感受也是足夠靠譜的。

好啦,今天的面試題就先到這裏,不知道你們有沒有被南塵繞暈呀,繞暈了沒事兒,多體驗幾回就行了。

仍是要提醒你們:千萬要本身去練習,看懂不思考,這樣的學習效率是極低的!

好啦,明天的習題咱們先放一下,別忘了先思考思考,並動手練練~

題目:輸入一個矩陣,按照從外向裏以順時針的順序依次打印每個數字。例如輸入:{{1,2,3},{4,5,6},{7,8,9}}則依次打印數字爲 一、二、三、六、九、八、七、四、5

相關文章
相關標籤/搜索