//leetcode中還有花樣鏈表題,這里幾個例子,冰山一角
- 求單鏈表中結點的個數----時間復雜度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)(也可以先inplace的翻轉,再遍歷,空間復雜度降到O(1))
- 從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。
- 如果鏈表為空或只有一個節點,無需反轉,直接返回原鏈表表頭。
注意鏈表為空和只有一個結點的情況。參考代碼如下:
public static Node reverseList(Node head) {
if (head == null || head.next == null) {
return head;
}
Node reHead = null; // 反轉后新鏈表最前面的node
Node cur = head;
while (cur != null) {
Node preCur = cur; // 用preCur保存住對要處理節點的引用
cur = cur.next; // cur更新到下一個節點
preCur.next = reHead; // 更新當前節點的next引用,當前.next指向 翻轉后鏈表最前面的node ==>當前節點成為翻轉后鏈表最前面的node
reHead = preCur; // reHead更新
}
return reHead;
}
- leetcode例題
206. Reverse LinkedList
沒做過一上來感覺不太好想,有個視頻,看到一半恍然大悟:https://www.youtube.com/watch?v=sYcOK51hl-A
這類題都開始要求用iteration寫一次再用recursive寫一個
//假設1->2->3->4,先從head=1開始,要翻轉,最后一個會變成head,所以head一步一步向后挪,每一步也一起翻轉指向
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
while(head!=null){
ListNode nextHead = head.next;
head.next = prev;
prev=head;
//這里得到的prev即下一個head前面的數,也就是下一個head要指向的數,當head=最后一個node(tail)時,prev=tail,循環結束
head = nextHead;
}
return prev;
}
//recursive
public ListNode reverseList(ListNode head) {
return reverseRecursive(head,null);
}
public ListNode reverseRecursive(ListNode head,ListNode prev){
if(head==null) return prev;
ListNode nextHead = head.next;
head.next=prev;
//下面傳參其實就相當于這兩句:prev=head;head = nextHead;
return reverseRecursive(nextHead,head);
}
}
92. Reverse Linked List II
For example:
Given 1->2->3->4->5->NULL, m = 2 and n = 4,
return 1->4->3->2->5->NULL.
根據題中的例子,第二個for循環開始
loop1:
1 --> 2 --> 3 --> 4 --> 5 --> NULL
p c n
cur.next = next.next;
2 --> 4
next.next = prev.next;
3 --> 2
prev.next = next;
1 --> 3
==> 1 --> 3 --> 2 --> 4 --> 5 --> NULL
p c n
loop2:
cur.next = next.next;
2 --> 5
next.next = prev.next;
4 --> 3
prev.next = next;
1 --> 4
==> 1 --> 4 --> 3 --> 2 --> 5 --> NULL
public ListNode reverseBetween(ListNode head, int m, int n) {
if(m == n) return head;
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
ListNode cur = head;
//在reverse之前的部分平移
for(int i = 0; i < m - 1; i++){
prev = prev.next;
cur = cur.next;
}
for(int i = 0; i < n - m; i++){
ListNode next = cur.next;
cur.next = next.next;
next.next = prev.next;
prev.next = next;
}
return dummy.next;
}
- 查找單鏈表中的倒數第K個結點(k > 0)----時間復雜度O(n)
- 最普遍的方法是,先統計單鏈表中結點的個數,然后再找到第(n-k)個結點。注意鏈表為空,k為0,k為1,k大于鏈表中節點個數時這三種情況情況。代碼不寫了。
- 另一個思路主要就是使用兩個指針,先讓前面的指針走到正向第k個結點,這樣前后兩個指針的距離差是k-1,之后前后兩個指針一起向前走,前面的指針走到最后一個結點時,后面指針所指結點就是倒數第k個結點。
19. Remove Nth Node From End of List
walker and runner, init walker,runner both as dummy, move runner n steps, so that the gap between runner and walker =n, then move runner and walker together, when runner get to the end of List, walker is before the nth from the end node, walker.next=walke.next.next, skip original walker.next
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode walker = dummy;
ListNode runner = dummy;
// after for loop, gap between runner and walker =n
for(int i = 1; i <= n; i++){
runner = runner.next;
}
while(runner.next!=null){
runner = runner.next;
walker = walker.next;
}
walker.next=walker.next.next;//skip nth node
return dummy.next;
}
- 查找單鏈表的中間結點----時間復雜度O(n)
此題可應用于上一題類似的思想。也是設置兩個指針,只不過這里是,兩個指針同時向前走,walker one step each time,runner two steps each time,runner get to the end, walker==mid,即第(n/2+1)個結點。還是分三種情況:鏈表結點個數為1和2的情況和其他。
public static Node getMiddleNode(Node head) {
if (head == null || head.next == null) {
return head;
}
Node walker = head; ]
Node runner = head;
// 前面指針每次走兩步,直到指向最后一個結點,后面指針每次走一步
while (walker.next != null) {
walker = walker.next;
runner = runner.next;
if (runner.next != null) {
runner = runner.next;
}
}
return walker;
}
- 從尾到頭打印單鏈表----時間復雜度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 + " ");
}
}
/**
使用遞歸(優雅!)
*/
public static void reversePrintListRec(Node head) {
if (head == null) {
return;
} else {
reversePrintListRec(head.next);
System.out.print(head.val + " ");
}
}
- Merge two sorted linked list----時間復雜度為O(max(len1, len2)),O(1)的空間
已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合并成一個鏈表依然有序
這個類似歸并排序。尤其注意兩個鏈表都為空,和其中一個為空時的情況。
21. Merge Two Sorted Lists
還是iteration和recursion,iteration代碼太長了,由此可見遞歸的好處,代碼簡介易懂
iteration注意 l1,l2挨個merge的時候為了方便,l1,l2在merge后指向自己next,即后移,同時head即新鏈表的當前node也后移,另外這里也是head不確定的情況,所以用dummy
//recursion
public ListNode mergeTwoLists(ListNode l1, ListNode l2){
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
//iteration
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; // 跳過已經合并了的元素,指向已merge的node的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;
}
}
// 合并剩余的元素,這個很重要,而且大部分merge算法最后都需要merge剩余東西這一步
if (head1 != null) {
mergeCur.next = head1;
} else if (head2 != null) {
mergeCur.next = head2;
}
return mergeHead;
}
23. Merge k Sorted Lists
根據priority queue的特性,我們可以通過重寫compare方法利用priority queue實現,還有dummy,從后向前拼接。
和下面sort里179一樣,都重寫了compare。一個是sort方法內,一個是priority queue
public ListNode mergeKLists(ListNode[] lists) {
if (lists==null||lists.length==0) return null;
PriorityQueue<ListNode> queue= new PriorityQueue<ListNode>(lists.length,new Comparator<ListNode>(){
@Override
/*
1. 這里compare方法可以直接return n1.val-n2.val;
*/
public int compare(ListNode n1, ListNode n2){
if(n1.val<n2.val) return -1;
else if(n1.val==n2.val) return 0;
else return 1;
}
});
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for(ListNode n:lists){
if(n!=null) queue.add(n);
}
while(!queue.isEmpty()){
tail.next = queue.poll();
tail=tail.next;
if(tail.next!=null){
queue.add(tail.next);
}
}
return dummy.next;
}
- 判斷一個單鏈表中是否有環----時間復雜度為O(n)
這里也是用到兩個指針。如果一個鏈表中有環,也就是說用一個指針去遍歷,是永遠走不到頭的。因此,我們可以用兩個指針去遍歷,一個指針一次走兩步,一個指針一次走一步,如果有環,兩個指針肯定會在環中相遇。
141. Linked List Cycle
- 用雙指針的思路,walker moves step by step. runner moves two steps at time. if the Linked List has a cycle walker and runner will meet at some
point. - 解法代碼下一題中其實是包含的,但我還是把這個代碼貼出來了,因為判定條件那里需要注意,這道題的寫法是,先判斷了head==null,之后while中判斷runner.next和runner.next.next,個人理解是runner跑的快,需要注意判斷runner而不是walker。下一題的寫法看起來跟這個不同,其實一樣
public boolean hasCycle(ListNode head) {
if(head==null) return false;
ListNode walker = head;
ListNode runner = head;
// runner跑的快,在前面,所以判斷runner.next, runner.next.next
while(runner.next!=null&&runner.next.next!=null){
walker = walker.next;
runner = runner.next.next;
if(walker==runner) return true;
}
return false;
}
142. Linked List Cycle2
關于判定條件的一個問題上道題中解釋了
這個題目的思路不太好想,discuss中有一個很好的解釋,貼過來,其中關鍵的兩點是,walker走過的距離和cycle長度的關系,以及walker,runner相遇之后再通過head和walker一齊走,相遇點是cycle起點這層關系
Definitions:
Cycle = length of the cycle, if exists.
C is the beginning of Cycle, S is the distance of slow pointer from C when slow pointer meets fast pointer.
Distance(slow) = C + S, Distance(fast) = 2 * Distance(slow) = 2 * (C + S). To let slow poiner meets fast pointer, only if fast pointer run 1 cycle more than slow pointer. Distance(fast) - Distance(slow) = Cycle
=> 2 * (C + S) - (C + S) = Cycle
=> C + S = Cycle
=> C = Cycle - S
=> This means if slow pointer runs (Cycle - S) more, it will reaches C. So at this time, if there's another point2(we use head Here) running from head
=> After C distance, point2 will meet slow pointer at C, where is the beginning of the cycle.
public ListNode detectCycle(ListNode head) {
ListNode walker = head;
ListNode runner = head;
//這里不加runner.next.next!=null也ac
while(runner!=null&&runner.next!=null&&runner.next.next!=null){
runner = runner.next.next;
walker = walker.next;
if(runner==walker){
while(head!=walker){
head = head.next;
walker = walker.next;
}
return walker;
}
}
return null;
}
- 判斷兩個單鏈表是否相交----時間復雜度為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;
}
- 求兩個單鏈表相交的第一個節點----時間復雜度,O(len1+len2)
對第一個鏈表遍歷,計算長度len1,同時保存最后一個節點的地址。
對第二個鏈表遍歷,計算長度len2,同時檢查最后一個節點是否和第一個鏈表的最后一個節點相同,若不相同,不相交,結束。
兩個鏈表均從頭節點開始,假設len1大于len2,那么將第一個鏈表先遍歷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++;
}
// 不相交直接返回NULL
if (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;
}
160. Intersection of Two Linked Lists
- 一個general的方法, 比較兩個linked list的長度,把較長的一個鏈表后移幾位,從長度和另一鏈表相等處開始比較node是否相同。
一開始在想相交之后還會不會分開,比如一開始就相交,那長度不等情況下先向后移就說不過去了,但是這里應該是利用了鏈表特性,每個node都指向另一個node,所以相交之后就一定都一樣了。 - 一個很機智的方法,感覺用到了類似single linked list中判斷是否有cycle時候用的runner 和walker雙指針的方法,這個題中的“雙指針”總會在intersection處相遇或者沒有intersection在最后的null相遇.
disscuss區大神的分析: - use two iterations here. In the first iteration, we will reset the pointer of one linkedlist to the head of another linkedlist after it reaches the tail node. In the second iteration, we will move two pointers until they points to the same node. Our operations in first iteration will help us counteract the difference.
So if two linkedlist intersects, the meeting point in second iteration must be the intersection point. If the two linked lists have no intersection at all, then the meeting pointer in second iteration must be the tail node of both lists, which is null - The problem description especially required the code to run in O(n) time and O(1) space. Thus I came up with the most direct way.
Just count the lengths of both lists, set two pointers from the list heads, align them to equipotential position and move'em forward until they coincide.
That would be the answer we seek.
Time complexity should be O(n + m), if you name the lengths of both lists to be "n" and "m". Extra space required is O(1). - Notice:只貼一下第二個方法,第一個方法很簡單,分別遍歷鏈表直到空,通過counter獲取長度,然后通過兩個長度差值移動指向較長鏈表的node的位置,在等長之后比較node是否相同,是就返回該node。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode a = headA;
ListNode b = headB;
while( a != b){
a = a == null? headB : a.next;
b = b == null? headA : b.next;
}
return a;
}
- 已知一個單鏈表中存在環,求進入環中的第一個節點
上面貼的leetcode142
首先判斷是否存在環,若不存在結束。在環中的一個節點處斷開(當然函數結束時不能破壞原鏈表),這樣就形成了兩個相交的單鏈表,求進入環中的第一個節點也就轉換成了求兩個單鏈表相交的第一個節點。參考代碼如下:
/**
* 求進入環中的第一個節點 用快慢指針做(本題用了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) { // Collision
break;
}
}
// 錯誤檢查,這是沒有環的情況
if (fast == null || fast.next == null) {
return null;
}
// 2)現在,相遇點離環的開始處的距離等于鏈表頭到環開始處的距離,
// 這樣,我們把慢指針放在鏈表頭,快指針保持在相遇點,然后
// 同速度前進,再次相遇點就是環的開始處!
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
// 再次相遇點就是環的開始處
return fast;
}
- 給出一單鏈表頭指針pHead和一節點指針pToBeDeleted,O(1)時間復雜度刪除節點pToBeDeleted----總體的平均時間復雜度還是O(1)
對于刪除節點,我們普通的思路就是讓該節點的前一個節點指向該節點的下一個節點,這種情況需要遍歷找到該節點的前一個節點,時間復雜度為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;
}
}
}
規律總結
- DummyNode
做鏈表題目時,如果head可能被改變,我們需要創建一個虛擬節點,叫DummyNode,把頭部掛在它的后面。這樣就算頭部變化了之后,只要返回DummyNode.next就能輕松得到新頭部。 - Merge LinkedList是相當基礎的題目,merge的半成品代碼上面也有提到。
- Reverse linkedList最簡單的寫法就是創建DummyNode,然后把舊的鏈表不斷插入到DummyNode的后面,就能輕松地返回鏈表了。
- 操作鏈表的時候,我們經常會改變某些Node。如果后面還需要再用到被改變掉節點的原始值,請一定記得用tmp先把它保存起來。