鏈表是面試中常出現的一類題目,本文用Java實現了面試中常見的鏈表相關題目。本文主要參考整合重寫了《輕鬆搞定面試中的鏈表題目》和 《演算法大全(1)單鏈表》兩篇大作。兩篇大神的實現分別是C和C#,因為我更喜歡用Java面試,所以用Java重寫了所有實現,並附上自己的一些思考注釋。演算法大全(1)單鏈表 尚未有一些問題尚未整合進來,很快我會更新本文。接下來還會出關於二叉樹的大總結和棧和隊列的大總結。因為這些都屬於面試中的送分題。必須毫無偏差地拿下。至於像動態規劃這些比較“高端”的演算法,就只能靠日積月累,而不能像這樣臨時突擊了。
第二篇: 《面試大總結之二:Java搞定面試中的二叉樹題目》已經出爐。
package LinkedListSummary;import java.util.HashMap;import java.util.Stack;/** * http://blog.csdn.net/luckyxiaoqiang/article/details/7393134 輕鬆搞定面試中的鏈表題目 * http://www.cnblogs.com/jax/archive/2009/12/11/1621504.html 演算法大全(1)單鏈表 * * 目錄: * 1. 求單鏈表中結點的個數: getListLength * 2. 將單鏈表反轉: reverseList(遍曆),reverseListRec(遞迴) * 3. 尋找單鏈表中的倒數第K個結點(k > 0): reGetKthNode * 4. 尋找單鏈表的中間結點: getMiddleNode * 5. 從尾到頭列印單鏈表: reversePrintListStack,reversePrintListRec(遞迴) * 6. 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合并成一個鏈表依然有序: mergeSortedList, mergeSortedListRec * 7. 判斷一個單鏈表中是否有環: hasCycle * 8. 判斷兩個單鏈表是否相交: isIntersect * 9. 求兩個單鏈表相交的第一個節點: getFirstCommonNode * 10. 已知一個單鏈表中存在環,求進入環中的第一個節點: getFirstNodeInCycle, getFirstNodeInCycleHashMap * 11. 給出一單鏈表頭指標pHead和一節點指標pToBeDeleted,O(1)時間複雜度刪除節點pToBeDeleted: delete * */public class Demo {public static void main(String[] args) {Node n1 = new Node(1);Node n2 = new Node(2);Node n3 = new Node(3);Node n4 = new Node(4);Node n5 = new Node(5);n1.next = n2;n2.next = n3;n3.next = n4;n4.next = n5;printList(n1);//System.out.println(getListLength(n1));//Node head = reverseList(n1);//Node head = reverseListRec(n1);//printList(head);Node x = reGetKthNode(n1, 2);System.out.println(x.val);reGetKthNodeRec(n1, 2);//x = getMiddleNode(head);//System.out.println(x.val);//System.out.println("reversePrintListStack:");//reversePrintListStack(head);//System.out.println("reversePrintListRec:");//reversePrintListRec(head);}//public static void main(String[] args) {//Node n1 = new Node(1);//Node n2 = new Node(3);//Node n3 = new Node(5);//n1.next = n2;//n2.next = n3;////Node m1 = new Node(1);//Node m2 = new Node(4);//Node m3 = new Node(6);//m1.next = m2;//m2.next = m3;//////Node ret = mergeSortedList(n1, m1);//printList(ret);//}private static class Node {int val;Node next;public Node(int val) {this.val = val;}}public static void printList(Node head) {while (head != null) {System.out.print(head.val + " ");head = head.next;}System.out.println();}// 求單鏈表中結點的個數// 注意檢查鏈表是否為空白。時間複雜度為O(n)public static int getListLength(Node head) {// 注意頭結點為空白情況if (head == null) {return 0;}int len = 0;Node cur = head;while (cur != null) {len++;cur = cur.next;}return len;}// 翻轉鏈表(遍曆)// 從頭到尾遍曆原鏈表,每遍曆一個結點,// 將其摘下放在新鏈表的最前端。// 注意鏈表為空白和只有一個結點的情況。時間複雜度為O(n)public static Node reverseList(Node head) {// 如果鏈表為空白或只有一個節點,無需反轉,直接返回原鏈表表頭if (head == null || head.next == null) {return head;}Node reHead = null; // 反轉後新鏈表指標Node cur = head;while (cur != null) {Node preCur = cur; // 用preCur儲存住對要處理節點的引用cur = cur.next; // cur更新到下一個節點preCur.next = reHead;// 更新要處理節點的next引用reHead = preCur; // reHead指向要處理節點的前一個節點}return reHead;}// 翻轉遞迴(遞迴)// 遞迴的精髓在於你就預設reverseListRec已經成功幫你解決了子問題了。但別去想如何解決的// 現在只要處理當前node和子問題之間的關係。最後就能圓滿解決整個問題。/* head1 -> 2 -> 3 -> 4 head1-------------- | 4 -> 3 -> 2// Node reHead = reverseListRec(head.next); reHead head.next 4 -> 3 -> 2 -> 1// head.next.next = head; reHead 4 -> 3 -> 2 -> 1 -> null// head.next = null; reHead */public static Node reverseListRec(Node head){if(head == null || head.next == null){return head;}Node reHead = reverseListRec(head.next);head.next.next = head;// 把head接在reHead串的最後一個後面head.next = null;// 防止迴圈鏈表return reHead;}/** * 尋找單鏈表中的倒數第K個結點(k > 0) * 最普遍的方法是,先統計單鏈表中結點的個數,然後再找到第(n-k)個結點。注意鏈表為空白,k為0,k為1,k大於鏈表中節點個數時的情況 * 。時間複雜度為O(n)。代碼略。 這裡主要講一下另一個思路,這種思路在其他題目中也會有應用。 * 主要思路就是使用兩個指標,先讓前面的指標走到正向第k個結點 * ,這樣前後兩個指標的距離差是k-1,之後前後兩個指標一起向前走,前面的指標走到最後一個結點時,後面指標所指結點就是倒數第k個結點 */public static Node reGetKthNode(Node head, int k) {// 這裡k的計數是從1開始,若k為0或鏈表為空白返回nullif (k == 0 || head == null) {return null;}Node q = head; // q在p前面 p--qNode p = head; // p在q後面// 讓q領先p距離kwhile (k > 1 && q != null) {q = q.next;k--;}// 當節點數小於k,返回nullif (k > 1 || q == null) {return null;}// 前後兩個指標一起走,直到前面的指標指向最後一個節點while (q.next != null) {p = p.next;q = q.next;}// 當前面的指標指向最後一個節點時,後面的指標指向倒數k個節點return p;}/** * 遞迴列印出倒數第k位的值 * @param head * @param dist */static int level = 0;public static void reGetKthNodeRec(Node head, int k) { if(head == null){ return; } if(k == 1){ return; } reGetKthNodeRec(head.next, k); level++; if(level == k) { System.out.println(head.val); } }// 尋找單鏈表的中間結點/** * 此題可應用於上一題類似的思想。也是設定兩個指標,只不過這裡是,兩個指標同時向前走,前面的指標每次走兩步,後面的指標每次走一步, * 前面的指標走到最後一個結點時,後面的指標所指結點就是中間結點,即第(n/2+1)個結點。注意鏈表為空白,鏈表結點個數為1和2的情況。時間複雜度O(n */public static Node getMiddleNode(Node head) {if (head == null || head.next == null) {return head;}Node q = head;// p---q Node p = head;// 前面指標每次走兩步,直到指向最後一個結點,後面指標每次走一步while (q.next != null) {q = q.next;p = p.next;if (q.next != null) {q = q.next;}}return p;}/** * 從尾到頭列印單鏈表 * 對於這種顛倒順序的問題,我們應該就會想到棧,後進先出。所以,這一題要麼自己使用棧,要麼讓系統使用棧,也就是遞迴。注意鏈表為空白的情況 * 。時間複雜度為O(n) */public static void reversePrintListStack(Node head) {Stack<Node> s = new Stack<Node>();Node cur = head;while (cur != null) {s.push(cur);cur = cur.next;}while (!s.empty()) {cur = s.pop();System.out.print(cur.val + " ");}System.out.println();}/** * 從尾到頭列印鏈表,使用遞迴(優雅。) */public static void reversePrintListRec(Node head) {if (head == null) {return;} else {reversePrintListRec(head.next);System.out.print(head.val + " ");}}/** * 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合并成一個鏈表依然有序 * 這個類似歸併排序。尤其注意兩個鏈表都為空白,和其中一個為空白時的情況。只需要O(1)的空間。時間複雜度為O(max(len1, len2)) */public static Node mergeSortedList(Node head1, Node head2) {// 其中一個鏈表為空白的情況,直接返回另一個鏈表頭,O(1)if (head1 == null) {return head2;}if (head2 == null) {return head1;}Node mergeHead = null;// 先確定下來mergeHead是在哪裡if (head1.val < head2.val) {mergeHead = head1;head1 = head1.next; // 跳過已經合并了的元素mergeHead.next = null; // 斷開mergeHead和後面的聯絡} else {mergeHead = head2;head2 = head2.next;mergeHead.next = null;}Node mergeCur = mergeHead;while (head1 != null && head2 != null) {if (head1.val < head2.val) {mergeCur.next = head1; // 把找到較小的元素合并到merge中head1 = head1.next; // 跳過已經合并了的元素mergeCur = mergeCur.next; // 找到下一個準備合并的元素mergeCur.next = null; // 斷開mergeCur和後面的聯絡} else {mergeCur.next = head2;head2 = head2.next;mergeCur = mergeCur.next;mergeCur.next = null;}}// 合并剩餘的元素if (head1 != null) {mergeCur.next = head1;} else if (head2 != null) {mergeCur.next = head2;}return mergeHead;}/** * 遞迴合并兩鏈表(優雅。) */public static Node mergeSortedListRec(Node head1, Node head2) {if (head1 == null) {return head2;}if (head2 == null) {return head1;}Node mergeHead = null;if (head1.val < head2.val) {mergeHead = head1;// 串連已解決的子問題mergeHead.next = mergeSortedListRec(head1.next, head2);} else {mergeHead = head2;mergeHead.next = mergeSortedListRec(head1, head2.next);}return mergeHead;}/** * 判斷一個單鏈表中是否有環 * 這裡也是用到兩個指標。如果一個鏈表中有環,也就是說用一個指標去遍曆,是永遠走不到頭的。因此,我們可以用兩個指標去遍曆,一個指標一次走兩步 * ,一個指標一次走一步,如果有環,兩個指標肯定會在環中相遇。時間複雜度為O(n) */public static boolean hasCycle(Node head) {Node fast = head; // 快指標每次前進兩步Node slow = head; // 慢指標每次前進一步while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) { // 相遇,存在環return true;}}return false;}// 判斷兩個單鏈表是否相交/** * 如果兩個鏈表相交於某一節點,那麼在這個相交節點之後的所有節點都是兩個鏈表所共有的。 也就是說,如果兩個鏈表相交,那麼最後一個節點肯定是共有的。 * 先遍曆第一個鏈表,記住最後一個節點,然後遍曆第二個鏈表, 到最後一個節點時和第一個鏈表的最後一個節點做比較,如果相同,則相交, * 否則不相交。時間複雜度為O(len1+len2),因為只需要一個額外指標儲存最後一個節點地址, 空間複雜度為O(1) */public static boolean isIntersect(Node head1, Node head2) {if (head1 == null || head2 == null) {return false;}Node tail1 = head1;// 找到鏈表1的最後一個節點while (tail1.next != null) {tail1 = tail1.next;}Node tail2 = head2;// 找到鏈表2的最後一個節點while (tail2.next != null) {tail2 = tail2.next;}return tail1 == tail2;}/** * 求兩個單鏈表相交的第一個節點 對第一個鏈表遍曆,計算長度len1,同時儲存最後一個節點的地址。 * 對第二個鏈表遍曆,計算長度len2,同時檢查最後一個節點是否和第一個鏈表的最後一個節點相同,若不相同,不相交,結束。 * 兩個鏈表均從前端節點開始,假設len1大於len2 * ,那麼將第一個鏈表先遍曆len1-len2個節點,此時兩個鏈表當前節點到第一個相交節點的距離就相等了,然後一起向後遍曆,直到兩個節點的地址相同。 * 時間複雜度,O(len1+len2) * * ---- len2 * |__________ * | * --------- len1 * |---|<- len1-len2 */public static Node getFirstCommonNode(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}int len1 = 1;Node tail1 = head1;while (tail1.next != null) {tail1 = tail1.next;len1++;}int len2 = 1;Node tail2 = head2;while (tail2.next != null) {tail2 = tail2.next;len2++;}// 不相交直接返回NULLif (tail1 != tail2) {return null;}Node n1 = head1;Node n2 = head2;// 略過較長鏈表多餘的部分if (len1 > len2) {int k = len1 - len2;while (k != 0) {n1 = n1.next;k--;}} else {int k = len2 - len1;while (k != 0) {n2 = n2.next;k--;}}// 一起向後遍曆,直到找到交點while (n1 != n2) {n1 = n1.next;n2 = n2.next;}return n1;}/** * 求進入環中的第一個節點 用快慢指標做(本題用了Crack the Coding Interview的解法,因為更簡潔易懂。) */public static Node getFirstNodeInCycle(Node head) {Node slow = head;Node fast = head;// 1) 找到快慢指標相遇點while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) { // Collisionbreak;}}// 錯誤檢查,這是沒有環的情況if (fast == null || fast.next == null) {return null;}// 2)現在,相遇點離環的開始處的距離等於鏈表頭到環開始處的距離,// 這樣,我們把慢指標放在鏈表頭,快指標保持在相遇點,然後// 同速度前進,再次相遇點就是環的開始處。slow = head;while (slow != fast) {slow = slow.next;fast = fast.next;}// 再次相遇點就是環的開始處return fast;}/** * 求進入環中的第一個節點 用HashMap做 一個無環的鏈表,它每個結點的地址都是不一樣的。 * 但如果有環,指標沿著鏈表移動,那這個指標最終會指向一個已經出現過的地址 以地址為雜湊表的索引值,每出現一個地址,就將該索引值對應的實值置為true。 * 那麼當某個索引值對應的實值已經為true時,說明這個地址之前已經出現過了, 直接返回它就OK了 */public static Node getFirstNodeInCycleHashMap(Node head) {HashMap<Node, Boolean> map = new HashMap<Node, Boolean>();while (head != null) {if (map.get(head) == true) {return head; // 這個地址之前已經出現過了,就是環的開始處} else {map.put(head, true);head = head.next;}}return head;}/** * 給出一單鏈表頭指標head和一節點指標toBeDeleted,O(1)時間複雜度刪除節點tBeDeleted * 對於刪除節點,我們普通的思路就是讓該節點的前一個節點指向該節點的下一個節點 * ,這種情況需要遍曆找到該節點的前一個節點,時間複雜度為O(n)。對於鏈表, * 鏈表中的每個節點結構都是一樣的,所以我們可以把該節點的下一個節點的資料複製到該節點 * ,然後刪除下一個節點即可。要注意最後一個節點的情況,這個時候只能用常見的方法來操作,先找到前一個節點,但總體的平均時間複雜度還是O(1) */public void delete(Node head, Node toDelete){if(toDelete == null){return;}if(toDelete.next != null){// 要刪除的是一個中間節點toDelete.val = toDelete.next.val;// 將下一個節點的資料複製到本節點!toDelete.next = toDelete.next.next;}else{// 要刪除的是最後一個節點。if(head == toDelete){// 鏈表中只有一個節點的情況 head = null;}else{Node node = head;while(node.next != toDelete){// 找到倒數第二個節點node = node.next;}node.next = null;}}}}
謝謝@buding12321 指出的bug~ 已經修複了