PS:這是一道出境率極高的題目,記得去年參加校園招聘時我看到了3次,但是每次寫的都不完善。
一、題目:鏈表的倒數第k個節點
題目:輸入一個鏈表,輸出該鏈表中倒數第k個結點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾結點是倒數第1個結點。例如一個鏈表有6個結點,從頭結點開始它們的值依次是1、2、3、4、5、6。這個鏈表的倒數第3個結點是值為4的結點。
鏈表的節點定義如下,這里使用的是C#來定義:
public class Node { public int Data { get; set; } public Node Next { get; set; } public Node(int data) { this.Data = data; } public Node(int data, Node next) { this.Data = data; this.Next = next; } }
二、解題思路
2.1 不可行的常規解法
為了得到倒數第k個結點,很自然的想法是先走到鏈表的尾端,再從尾端回溯k步。當時,從鏈表結點的定義可以看出本題中的鏈表是單向鏈表,單向鏈表的結點只有從前往后的指針而沒有從后往前的指針,因此這種思路行不通,它只適用於雙向鏈表。
如果鏈表定義中有指向前一個節點的指針,那么此解法是可行的,我們可以修改鏈表定義:
public class Node { public int Data { get; set; } // 指向后一個節點 public Node Next { get; set; } // 指向前一個節點Prev public Node Prev { get; set; } public Node(int data) { this.Data = data; } }
2.2 可行但不高效的常規解法
假設整個鏈表有n個結點,那么倒數第k個結點就是從頭結點開始的第n-k+1個結點。如果我們能夠得到鏈表中結點的個數n,那我們只要從頭結點開始往后走n-k+1步就可以了。
那么,這里的重點就在於如何求鏈表中節點的個數n,只需要從頭開始遍歷鏈表,每經過一個結點,計數器加1就行了。
但是,問題來了:這種思路需要遍歷鏈表兩次,第一次統計出鏈表中結點的個數,第二次才能找到倒數第k個結點。
2.3 可行且高效的解法
為了能夠只遍歷一次就能找到倒數第k個節點,可以定義兩個指針:
(1)第一個指針從鏈表的頭指針開始遍歷向前走k-1,第二個指針保持不動;
(2)從第k步開始,第二個指針也開始從鏈表的頭指針開始遍歷;
(3)由於兩個指針的距離保持在k-1,當第一個(走在前面的)指針到達鏈表的尾結點時,第二個指針(走在后面的)指針正好是倒數第k個結點。
下圖展示了在有6個結點的鏈表上找倒數第3個結點的過程:
舉一反三:當我們用一個指針遍歷鏈表不能解決問題的時候,可以嘗試用兩個指針來遍歷鏈表。可以讓其中一個指針遍歷的速度快一些(比如一次在鏈表上走兩步),或者讓它先在鏈表上走若干步。
三、解決問題
3.1 代碼實現
public static Node FindKthToTail(Node head, uint k) { Node ahead = head; Node behind = null; for (int i = 0; i < k - 1; i++) { ahead = ahead.Next; } behind = head; while (ahead.Next != null) { ahead = ahead.Next; behind = behind.Next; } return behind; }
3.2 代碼完善
上面的代碼存在3處魯棒性問題:
(1)輸入的head為空指針。由於代碼會試圖訪問空指針指向的內存,程序崩潰。
解決:在處理前增加判斷空指針的代碼
(2)輸入的以head為頭結點的鏈表的結點總數少於k。由於在for循環中會在鏈表上向前走k-1步,仍然會由於空指針造成程序崩潰。
解決:在for循環中增加判斷下一個節點是否是空指針的代碼
(3)輸入的參數k為0。由於k是一個無符號整數,那么在for循環中k-1得到的將不是-1,而是4294967295(無符號的0xFFFFFFFF)。因此for循環執行的次數遠遠超出我們的預計,同樣也會造成程序崩潰。
解決:同(1),在處理前的判斷中也判斷參數k是否為0。
public static Node FindKthToTail(Node head, uint k) { if(head == null || k == 0) { return null; } Node ahead = head; Node behind = null; for (int i = 0; i < k - 1; i++) { if(ahead.Next != null) { ahead = ahead.Next; } else { return null; } } behind = head; while (ahead.Next != null) { ahead = ahead.Next; behind = behind.Next; } return behind; }
《劍指Offer》這本書另外的一大優點就在於作者以一個開發老鳥的角度時時刻刻地站在了魯棒性、可維護性、可擴展性地角度來告訴即將進入開發一線的菜鳥們做提醒,在開發中需要考慮這些東西,並通過設計測試用例進行單元測試驗證結果。
3.3 單元測試
(1)功能測試
// 01.測試要找的結點在鏈表中間 [TestMethod] public void FindKthNodeTest1() { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); node1.Next = node2; node2.Next = node3; node3.Next = node4; node4.Next = node5; Assert.AreEqual(NodeHelper.FindKthToTail(node1, 4), node2); } // 02.測試要找的結點是鏈表的尾結點 [TestMethod] public void FindKthNodeTest2() { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); node1.Next = node2; node2.Next = node3; node3.Next = node4; node4.Next = node5; Assert.AreEqual(NodeHelper.FindKthToTail(node1, 1), node5); } // 03.測試要找的結點是鏈表的頭結點 [TestMethod] public void FindKthNodeTest3() { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); node1.Next = node2; node2.Next = node3; node3.Next = node4; node4.Next = node5; Assert.AreEqual(NodeHelper.FindKthToTail(node1, 5), node1); }
(2)特殊輸入測試
// 04.測試空鏈表 [TestMethod] public void FindKthNodeTest4() { Assert.AreEqual(NodeHelper.FindKthToTail(null, 100), null); } // 05.測試輸入的第二個參數大於鏈表的結點總數 [TestMethod] public void FindKthNodeTest5() { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); node1.Next = node2; node2.Next = node3; node3.Next = node4; node4.Next = node5; Assert.AreEqual(NodeHelper.FindKthToTail(node1, 6), null); } // 06.測試輸入的第二個參數等於0 [TestMethod] public void FindKthNodeTest6() { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); node1.Next = node2; node2.Next = node3; node3.Next = node4; node4.Next = node5; Assert.AreEqual(NodeHelper.FindKthToTail(node1, 0), null); }
(3)測試結果
①測試用例通過情況
②代碼覆蓋率情況