【聲明】
歡迎轉載,但請保留文章原始出處→_→
生命壹號:http://www.cnblogs.com/smyhvae/
文章來源:http://www.cnblogs.com/smyhvae/p/4782595.html
【正文】
這份筆記整理了整整一個星期,每一行代碼都是自己默寫完成,並測試運行成功,同時也回顧了一下《劍指offer》這本書中和鏈表有關的講解,希望對筆試和面試有所幫助。OMG!
本文包含鏈表的以下內容:
1、單鏈表的創建和遍歷
2、求單鏈表中節點的個數
3、查找單鏈表中的倒數第k個結點(劍指offer,題15)
4、查找單鏈表中的中間結點
5、合並兩個有序的單鏈表,合並之后的鏈表依然有序【出現頻率高】(劍指offer,題17)
6、單鏈表的反轉【出現頻率最高】(劍指offer,題16)
7、從尾到頭打印單鏈表(劍指offer,題5)
8、判斷單鏈表是否有環
9、取出有環鏈表中,環的長度
10、單鏈表中,取出環的起始點(劍指offer,題56)。本題需利用上面的第8題和第9題。
11、判斷兩個單鏈表相交的第一個交點(劍指offer,題37)
此外,《劍指offer》中還有如下和鏈表相關的題目暫時還沒有收錄:(以后再收錄)
劍指offer,題13:在O(1)時間刪除鏈表結點
劍指offer,題26:復雜鏈表的復制
劍指offer,題45:圓圈中最后剩下的數字
劍指offer,題57:刪除鏈表中
1、單鏈表的創建和遍歷:
1 public class LinkList { 2 public Node head; 3 public Node current; 4 5 //方法:向鏈表中添加數據 6 public void add(int data) { 7 //判斷鏈表為空的時候 8 if (head == null) {//如果頭結點為空,說明這個鏈表還沒有創建,那就把新的結點賦給頭結點 9 head = new Node(data); 10 current = head; 11 } else { 12 //創建新的結點,放在當前節點的后面(把新的結點合鏈表進行關聯) 13 current.next = new Node(data); 14 //把鏈表的當前索引向后移動一位 15 current = current.next; //此步操作完成之后,current結點指向新添加的那個結點 16 } 17 } 18 19 //方法:遍歷鏈表(打印輸出鏈表。方法的參數表示從節點node開始進行遍歷 20 public void print(Node node) { 21 if (node == null) { 22 return; 23 } 24 25 current = node; 26 while (current != null) { 27 System.out.println(current.data); 28 current = current.next; 29 } 30 } 31 32 33 class Node { 34 //注:此處的兩個成員變量權限不能為private,因為private的權限是僅對本類訪問。 35 int data; //數據域 36 Node next;//指針域 37 38 public Node(int data) { 39 this.data = data; 40 } 41 } 42 43 44 public static void main(String[] args) { 45 LinkList list = new LinkList(); 46 //向LinkList中添加數據 47 for (int i = 0; i < 10; i++) { 48 list.add(i); 49 } 50 51 list.print(list.head);// 從head節點開始遍歷輸出 52 } 53 54 }
上方代碼中,這里面的Node節點采用的是內部類來表示(33行)。使用內部類的最大好處是可以和外部類進行私有操作的互相訪問。
注:內部類訪問的特點是:內部類可以直接訪問外部類的成員,包括私有;外部類要訪問內部類的成員,必須先創建對象。
為了方便添加和遍歷的操作,在LinkList類中添加一個成員變量current,用來表示當前節點的索引(03行)。
這里面的遍歷鏈表的方法(20行)中,參數node表示從node節點開始遍歷,不一定要從head節點遍歷。
2、求單鏈表中節點的個數:
注意檢查鏈表是否為空。時間復雜度為O(n)。這個比較簡單。
核心代碼:
1 //方法:獲取單鏈表的長度 2 public int getLength(Node head) { 3 if (head == null) { 4 return 0; 5 } 6 7 int length = 0; 8 Node current = head; 9 while (current != null) { 10 length++; 11 current = current.next; 12 } 13 14 return length; 15 }
3、查找單鏈表中的倒數第k個結點:
3.1 普通思路:
先將整個鏈表從頭到尾遍歷一次,計算出鏈表的長度size,得到鏈表的長度之后,就好辦了,直接輸出第(size-k)個節點就可以了(注意鏈表為空,k為0,k為1,k大於鏈表中節點個數時的情況
)。時間復雜度為O(n),大概思路如下:
1 public int findLastNode(int index) { //index代表的是倒數第index的那個結點 2 3 //第一次遍歷,得到鏈表的長度size 4 if (head == null) { 5 return -1; 6 } 7 8 current = head; 9 while (current != null) { 10 size++; 11 current = current.next; 12 } 13 14 //第二次遍歷,輸出倒數第index個結點的數據 15 current = head; 16 for (int i = 0; i < size - index; i++) { 17 current = current.next; 18 } 19 20 return current.data; 21 }
如果面試官不允許你遍歷鏈表的長度,該怎么做呢?接下來就是。
3.2 改進思路:(這種思路在其他題目中也有應用)
這里需要聲明兩個指針:即兩個結點型的變量first和second,首先讓first和second都指向第一個結點,然后讓second結點往后挪k-1個位置,此時first和second就間隔了k-1個位置,然后整體向后移動這兩個節點,直到second節點走到最后一個結點的時候,此時first節點所指向的位置就是倒數第k個節點的位置。時間復雜度為O(n)
代碼實現:(初版)
1 public Node findLastNode(Node head, int index) { 2 3 if (node == null) { 4 return null; 5 } 6 7 Node first = head; 8 Node second = head; 9 10 //讓second結點往后挪index個位置 11 for (int i = 0; i < index; i++) { 12 second = second.next; 13 } 14 15 //讓first和second結點整體向后移動,直到second結點為Null 16 while (second != null) { 17 first = first.next; 18 second = second.next; 19 } 20 21 //當second結點為空的時候,此時first指向的結點就是我們要找的結點 22 return first; 23 }
代碼實現:(最終版)(考慮k大於鏈表中結點個數時的情況時,拋出異常)
上面的代碼中,看似已經實現了功能,其實還不夠健壯:
要注意k等於0的情況;
如果k大於鏈表中節點個數時,就會報空指針異常,所以這里需要做一下判斷。
核心代碼如下:
1 public Node findLastNode(Node head, int k) { 2 if (k == 0 || head == null) { 3 return null; 4 } 5 6 Node first = head; 7 Node second = head; 8 9 //讓second結點往后挪k-1個位置 10 for (int i = 0; i < k - 1; i++) { 11 System.out.println("i的值是" + i); 12 second = second.next; 13 if (second == null) { //說明k的值已經大於鏈表的長度了 14 //throw new NullPointerException("鏈表的長度小於" + k); //我們自己拋出異常,給用戶以提示 15 return null; 16 } 17 } 18 19 //讓first和second結點整體向后移動,直到second走到最后一個結點 20 while (second.next != null) { 21 first = first.next; 22 second = second.next; 23 } 24 25 //當second結點走到最后一個節點的時候,此時first指向的結點就是我們要找的結點 26 return first; 27 }
4、查找單鏈表中的中間結點:
同樣,面試官不允許你算出鏈表的長度,該怎么做呢?
思路:
和上面的第2節一樣,也是設置兩個指針first和second,只不過這里是,兩個指針同時向前走,second指針每次走兩步,first指針每次走一步,直到second指針走到最后一個結點時,此時first指針所指的結點就是中間結點。注意鏈表為空,鏈表結點個數為1和2的情況。時間復雜度為O(n)。
代碼實現:
1 //方法:查找鏈表的中間結點 2 public Node findMidNode(Node head) { 3 4 if (head == null) { 5 return null; 6 } 7 8 Node first = head; 9 Node second = head; 10 //每次移動時,讓second結點移動兩位,first結點移動一位 11 while (second != null && second.next != null) { 12 first = first.next; 13 second = second.next.next; 14 } 15 16 //直到second結點移動到null時,此時first指針指向的位置就是中間結點的位置 17 return first; 18 }
上方代碼中,當n為偶數時,得到的中間結點是第n/2 + 1個結點。比如鏈表有6個節點時,得到的是第4個節點。
5、合並兩個有序的單鏈表,合並之后的鏈表依然有序:
這道題經常被各公司考察。
例如:
鏈表1:
1->2->3->4
鏈表2:
2->3->4->5
合並后:
1->2->2->3->3->4->4->5
解題思路:
挨着比較鏈表1和鏈表2。
這個類似於歸並排序。尤其要注意兩個鏈表都為空、和其中一個為空的情況。只需要O (1) 的空間。時間復雜度為O (max(len1,len2))
代碼實現:
1 //兩個參數代表的是兩個鏈表的頭結點 2 public Node mergeLinkList(Node head1, Node head2) { 3 4 if (head1 == null && head2 == null) { //如果兩個鏈表都為空 5 return null; 6 } 7 if (head1 == null) { 8 return head2; 9 } 10 if (head2 == null) { 11 return head1; 12 } 13 14 Node head; //新鏈表的頭結點 15 Node current; //current結點指向新鏈表 16 17 // 一開始,我們讓current結點指向head1和head2中較小的數據,得到head結點 18 if (head1.data < head2.data) { 19 head = head1; 20 current = head1; 21 head1 = head1.next; 22 } else { 23 head = head2; 24 current = head2; 25 head2 = head2.next; 26 } 27 28 while (head1 != null && head2 != null) { 29 if (head1.data < head2.data) { 30 current.next = head1; //新鏈表中,current指針的下一個結點對應較小的那個數據 31 current = current.next; //current指針下移 32 head1 = head1.next; 33 } else { 34 current.next = head2; 35 current = current.next; 36 head2 = head2.next; 37 } 38 } 39 40 //合並剩余的元素 41 if (head1 != null) { //說明鏈表2遍歷完了,是空的 42 current.next = head1; 43 } 44 45 if (head2 != null) { //說明鏈表1遍歷完了,是空的 46 current.next = head2; 47 } 48 49 return head; 50 }
代碼測試:
1 public static void main(String[] args) { 2 LinkList list1 = new LinkList(); 3 LinkList list2 = new LinkList(); 4 //向LinkList中添加數據 5 for (int i = 0; i < 4; i++) { 6 list1.add(i); 7 } 8 9 for (int i = 3; i < 8; i++) { 10 list2.add(i); 11 } 12 13 LinkList list3 = new LinkList(); 14 list3.head = list3.mergeLinkList(list1.head, list2.head); //將list1和list2合並,存放到list3中 15 16 list3.print(list3.head);// 從head節點開始遍歷輸出 17 }
上方代碼中用到的add方法和print方法和第1小節中是一致的。
運行效果:
注:《劍指offer》中是用遞歸解決的,感覺有點難理解。
6、單鏈表的反轉:【出現頻率最高】
例如鏈表:
1->2->3->4
反轉之后:
4->3->2->1
思路:
從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)
方法1:(遍歷)
1 //方法:鏈表的反轉 2 public Node reverseList(Node head) { 3 4 //如果鏈表為空或者只有一個節點,無需反轉,直接返回原鏈表的頭結點 5 if (head == null || head.next == null) { 6 return head; 7 } 8 9 Node current = head; 10 Node next = null; //定義當前結點的下一個結點 11 Node reverseHead = null; //反轉后新鏈表的表頭 12 13 while (current != null) { 14 next = current.next; //暫時保存住當前結點的下一個結點,因為下一次要用 15 16 current.next = reverseHead; //將current的下一個結點指向新鏈表的頭結點 17 reverseHead = current; 18 19 current = next; // 操作結束后,current節點后移 20 } 21 22 return reverseHead; 23 }
上方代碼中,核心代碼是第16、17行。
方法2:(遞歸)
這個方法有點難,先不講了。
7、從尾到頭打印單鏈表:
對於這種顛倒順序的問題,我們應該就會想到棧,后進先出。所以,這一題要么自己使用棧,要么讓系統使用棧,也就是遞歸。注意鏈表為空的情況。時間復雜度為O(n)
注:不要想着先將單鏈表反轉,然后遍歷輸出,這樣會破壞鏈表的結構,不建議。
方法1:(自己新建一個棧)
1 //方法:從尾到頭打印單鏈表 2 public void reversePrint(Node head) { 3 4 if (head == null) { 5 return; 6 } 7 8 Stack<Node> stack = new Stack<Node>(); //新建一個棧 9 Node current = head; 10 11 //將鏈表的所有結點壓棧 12 while (current != null) {- 13 stack.push(current); //將當前結點壓棧 14 current = current.next; 15 } 16 17 //將棧中的結點打印輸出即可 18 while (stack.size() > 0) { 19 System.out.println(stack.pop().data); //出棧操作 20 } 21 }
方法2:(使用系統的棧:遞歸,代碼優雅簡潔)
1 public void reversePrint(Node head) { 2 3 4 if (head == null) { 5 return; 6 } 7 reversePrint(head.next); 8 System.out.println(head.data); 9 }
總結:方法2是基於遞歸實現的,戴安看起來簡潔優雅,但有個問題:當鏈表很長的時候,就會導致方法調用的層級很深,有可能造成棧溢出。而方法1的顯式用棧,是基於循環實現的,代碼的魯棒性要更好一些。
8、判斷單鏈表是否有環:
這里也是用到兩個指針,如果一個鏈表有環,那么用一個指針去遍歷,是永遠走不到頭的。
因此,我們用兩個指針去遍歷:first指針每次走一步,second指針每次走兩步,如果first指針和second指針相遇,說明有環。時間復雜度為O (n)。
方法:
1 //方法:判斷單鏈表是否有環 2 public boolean hasCycle(Node head) { 3 4 if (head == null) { 5 return false; 6 } 7 8 Node first = head; 9 Node second = head; 10 11 while (second != null) { 12 first = first.next; //first指針走一步 13 second = second.next.next; second指針走兩步 14 15 if (first == second) { //一旦兩個指針相遇,說明鏈表是有環的 16 return true; 17 } 18 } 19 20 return false; 21 }
完整版代碼:(包含測試部分)
這里,我們還需要加一個重載的add(Node node)方法,在創建單向循環鏈表時要用到。
LinkList.java:
1 public class LinkList { 2 public Node head; 3 public Node current; 4 5 //方法:向鏈表中添加數據 6 public void add(int data) { 7 //判斷鏈表為空的時候 8 if (head == null) {//如果頭結點為空,說明這個鏈表還沒有創建,那就把新的結點賦給頭結點 9 head = new Node(data); 10 current = head; 11 } else { 12 //創建新的結點,放在當前節點的后面(把新的結點合鏈表進行關聯) 13 current.next = new Node(data); 14 //把鏈表的當前索引向后移動一位 15 current = current.next; 16 } 17 } 18 19 20 //方法重載:向鏈表中添加結點 21 public void add(Node node) { 22 if (node == null) { 23 return; 24 } 25 26 if (head == null) { 27 head = node; 28 current = head; 29 } else { 30 current.next = node; 31 current = current.next; 32 } 33 } 34 35 36 //方法:遍歷鏈表(打印輸出鏈表。方法的參數表示從節點node開始進行遍歷 37 public void print(Node node) { 38 if (node == null) { 39 return; 40 } 41 42 current = node; 43 while (current != null) { 44 System.out.println(current.data); 45 current = current.next; 46 } 47 } 48 49 //方法:檢測單鏈表是否有環 50 public boolean hasCycle(Node head) { 51 52 if (head == null) { 53 return false; 54 } 55 56 Node first = head; 57 Node second = head; 58 59 while (second != null) { 60 first = first.next; //first指針走一步 61 second = second.next.next; //second指針走兩步 62 63 if (first == second) { //一旦兩個指針相遇,說明鏈表是有環的 64 return true; 65 } 66 } 67 68 return false; 69 } 70 71 class Node { 72 //注:此處的兩個成員變量權限不能為private,因為private的權限是僅對本類訪問。 73 int data; //數據域 74 Node next;//指針域 75 76 public Node(int data) { 77 this.data = data; 78 } 79 } 80 81 public static void main(String[] args) { 82 LinkList list = new LinkList(); 83 //向LinkList中添加數據 84 for (int i = 0; i < 4; i++) { 85 list.add(i); 86 } 87 88 list.add(list.head); //將頭結點添加到鏈表當中,於是,單鏈表就有環了。備注:此時得到的這個環的結構,是下面的第8小節中圖1的那種結構。 89 90 System.out.println(list.hasCycle(list.head)); 91 } 92 }
檢測單鏈表是否有環的代碼是第50行。
88行:我們將頭結點繼續往鏈表中添加,此時單鏈表就環了。最終運行效果為true。
如果刪掉了88行代碼,此時單鏈表沒有環,運行效果為false。
9、取出有環鏈表中,環的長度:
我們平時碰到的有環鏈表是下面的這種:(圖1)
上圖中環的長度是4。
但有可能也是下面的這種:(圖2)
此時,上圖中環的長度就是3了。
那怎么求出環的長度呢?
思路:
這里面,我們需要先利用上面的第7小節中的hasCycle方法(判斷鏈表是否有環的那個方法),這個方法的返回值是boolean型,但是現在要把這個方法稍做修改,讓其返回值為相遇的那個結點。然后,我們拿到這個相遇的結點就好辦了,這個結點肯定是在環里嘛,我們可以讓這個結點對應的指針一直往下走,直到它回到原點,就可以算出環的長度了。
方法:
1 //方法:判斷單鏈表是否有環。返回的結點是相遇的那個結點 2 public Node hasCycle(Node head) { 3 4 if (head == null) { 5 return null; 6 } 7 8 Node first = head; 9 Node second = head; 10 11 while (second != null) { 12 first = first.next; 13 second = second.next.next; 14 15 if (first == second) { //一旦兩個指針相遇,說明鏈表是有環的 16 return first; //將相遇的那個結點進行返回 17 } 18 } 19 20 return null; 21 } 22 23 //方法:有環鏈表中,獲取環的長度。參數node代表的是相遇的那個結點 24 public int getCycleLength(Node node) { 25 26 if (head == null) { 27 return 0; 28 } 29 30 Node current = node; 31 int length = 0; 32 33 while (current != null) { 34 current = current.next; 35 length++; 36 if (current == node) { //當current結點走到原點的時候 37 return length; 38 } 39 } 40 41 return length; 42 }
完整版代碼:(包含測試部分)
1 public class LinkList { 2 public Node head; 3 public Node current; 4 5 public int size; 6 7 //方法:向鏈表中添加數據 8 public void add(int data) { 9 //判斷鏈表為空的時候 10 if (head == null) {//如果頭結點為空,說明這個鏈表還沒有創建,那就把新的結點賦給頭結點 11 head = new Node(data); 12 current = head; 13 } else { 14 //創建新的結點,放在當前節點的后面(把新的結點合鏈表進行關聯) 15 current.next = new Node(data); 16 //把鏈表的當前索引向后移動一位 17 current = current.next; //此步操作完成之后,current結點指向新添加的那個結點 18 } 19 } 20 21 22 //方法重載:向鏈表中添加結點 23 public void add(Node node) { 24 if (node == null) { 25 return; 26 } 27 if (head == null) { 28 head = node; 29 current = head; 30 } else { 31 current.next = node; 32 current = current.next; 33 } 34 } 35 36 37 //方法:遍歷鏈表(打印輸出鏈表。方法的參數表示從節點node開始進行遍歷 38 public void print(Node node) { 39 if (node == null) { 40 return; 41 } 42 43 current = node; 44 while (current != null) { 45 System.out.println(current.data); 46 current = current.next; 47 } 48 } 49 50 //方法:判斷單鏈表是否有環。返回的結點是相遇的那個結點 51 public Node hasCycle(Node head) { 52 53 if (head == null) { 54 return null; 55 } 56 57 Node first = head; 58 Node second = head; 59 60 while (second != null) { 61 first = first.next; 62 second = second.next.next; 63 64 if (first == second) { //一旦兩個指針相遇,說明鏈表是有環的 65 return first; //將相遇的那個結點進行返回 66 } 67 } 68 69 return null; 70 } 71 72 //方法:有環鏈表中,獲取環的長度。參數node代表的是相遇的那個結點 73 public int getCycleLength(Node node) { 74 75 if (head == null) { 76 return 0; 77 } 78 79 Node current = node; 80 int length = 0; 81 82 while (current != null) { 83 current = current.next; 84 length++; 85 if (current == node) { //當current結點走到原點的時候 86 return length; 87 } 88 } 89 90 return length; 91 } 92 93 class Node { 94 //注:此處的兩個成員變量權限不能為private,因為private的權限是僅對本類訪問。 95 int data; //數據域 96 Node next;//指針域 97 98 public Node(int data) { 99 this.data = data; 100 } 101 } 102 103 104 public static void main(String[] args) { 105 LinkList list1 = new LinkList(); 106 107 Node second = null; //把第二個結點記下來 108 109 //向LinkList中添加數據 110 for (int i = 0; i < 4; i++) { 111 list1.add(i); 112 113 if (i == 1) { 114 second = list1.current; //把第二個結點記下來 115 } 116 } 117 118 list1.add(second); //將尾結點指向鏈表的第二個結點,於是單鏈表就有環了,備注:此時得到的環的結構,是本節中圖2的那種結構 119 Node current = list1.hasCycle(list1.head); //獲取相遇的那個結點 120 121 System.out.println("環的長度為" + list1.getCycleLength(current)); 122 } 123 124 }
運行效果:
如果將上面的104至122行的測試代碼改成下面這樣的:(即:將圖2中的結構改成圖1中的結構)
1 public static void main(String[] args) { 2 LinkList list1 = new LinkList(); 3 //向LinkList中添加數據 4 for (int i = 0; i < 4; i++) { 5 list1.add(i); 6 } 7 8 list1.add(list1.head); //將頭結點添加到鏈表當中(將尾結點指向頭結點),於是,單鏈表就有環了。備注:此時得到的這個環的結構,是本節中圖1的那種結構。 9 10 Node current = list1.hasCycle(list1.head); 11 12 System.out.println("環的長度為" + list1.getCycleLength(current)); 13 }
運行結果:
如果把上面的代碼中的第8行刪掉,那么這個鏈表就沒有環了,於是運行的結果為0。
10、單鏈表中,取出環的起始點:
我們平時碰到的有環鏈表是下面的這種:(圖1)
上圖中環的起始點1。
但有可能也是下面的這種:(圖2)
此時,上圖中環的起始點是2。
方法1:
這里我們需要利用到上面第8小節的取出環的長度的方法getCycleLength,用這個方法來獲取環的長度length。拿到環的長度length之后,需要用到兩個指針變量first和second,先讓second指針走length步;然后讓first指針和second指針同時各走一步,當兩個指針相遇時,相遇時的結點就是環的起始點。
注:為了找到環的起始點,我們需要先獲取環的長度,而為了獲取環的長度,我們需要先判斷是否有環。所以這里面其實是用到了三個方法。
代碼實現:
方法1的核心代碼:
1 //方法:獲取環的起始點。參數length表示環的長度 2 public Node getCycleStart(Node head, int cycleLength) { 3 4 if (head == null) { 5 return null; 6 } 7 8 Node first = head; 9 Node second = head; 10 //先讓second指針走length步 11 for (int i = 0; i < cycleLength; i++) { 12 second = second.next; 13 } 14 15 //然后讓first指針和second指針同時各走一步 16 while (first != null && second != null) { 17 first = first.next; 18 second = second.next; 19 20 if (first == second) { //如果兩個指針相遇了,說明這個結點就是環的起始點 21 return first; 22 } 23 } 24 25 return null; 26 }
完整版代碼:(含測試部分)
1 public class LinkList { 2 public Node head; 3 public Node current; 4 5 public int size; 6 7 //方法:向鏈表中添加數據 8 public void add(int data) { 9 //判斷鏈表為空的時候 10 if (head == null) {//如果頭結點為空,說明這個鏈表還沒有創建,那就把新的結點賦給頭結點 11 head = new Node(data); 12 current = head; 13 } else { 14 //創建新的結點,放在當前節點的后面(把新的結點合鏈表進行關聯) 15 current.next = new Node(data); 16 //把鏈表的當前索引向后移動一位 17 current = current.next; //此步操作完成之后,current結點指向新添加的那個結點 18 } 19 } 20 21 22 //方法重載:向鏈表中添加結點 23 public void add(Node node) { 24 if (node == null) { 25 return; 26 } 27 if (head == null) { 28 head = node; 29 current = head; 30 } else { 31 current.next = node; 32 current = current.next; 33 } 34 } 35 36 37 //方法:遍歷鏈表(打印輸出鏈表。方法的參數表示從節點node開始進行遍歷 38 public void print(Node node) { 39 if (node == null) { 40 return; 41 } 42 43 current = node; 44 while (current != null) { 45 System.out.println(current.data); 46 current = current.next; 47 } 48 } 49 50 51 //方法:判斷單鏈表是否有環。返回的結點是相遇的那個結點 52 public Node hasCycle(Node head) { 53 54 if (head == null) { 55 return null; 56 } 57 58 Node first = head; 59 Node second = head; 60 61 while (second != null) { 62 first = first.next; 63 second = second.next.next; 64 65 if (first == second) { //一旦兩個指針相遇,說明鏈表是有環的 66 return first; //將相遇的那個結點進行返回 67 } 68 } 69 70 return null; 71 } 72 //方法:有環鏈表中,獲取環的長度。參數node代表的是相遇的那個結點 73 public int getCycleLength(Node node) { 74 75 if (head == null) { 76 return 0; 77 } 78 79 Node current = node; 80 int length = 0; 81 82 while (current != null) { 83 current = current.next; 84 length++; 85 if (current == node) { //當current結點走到原點的時候 86 return length; 87 } 88 } 89 90 return length; 91 } 92 93 //方法:獲取環的起始點。參數length表示環的長度 94 public Node getCycleStart(Node head, int cycleLength) { 95 96 if (head == null) { 97 return null; 98 } 99 100 Node first = head; 101 Node second = head; 102 //先讓second指針走length步 103 for (int i = 0; i < cycleLength; i++) { 104 second = second.next; 105 } 106 107 //然后讓first指針和second指針同時各走一步 108 while (first != null && second != null) { 109 first = first.next; 110 second = second.next; 111 112 if (first == second) { //如果兩個指針相遇了,說明這個結點就是環的起始點 113 return first; 114 } 115 } 116 117 return null; 118 } 119 120 class Node { 121 //注:此處的兩個成員變量權限不能為private,因為private的權限是僅對本類訪問。 122 int data; //數據域 123 Node next;//指針域 124 125 public Node(int data) { 126 this.data = data; 127 } 128 } 129 130 131 public static void main(String[] args) { 132 LinkList list1 = new LinkList(); 133 134 Node second = null; //把第二個結點記下來 135 136 //向LinkList中添加數據 137 for (int i = 0; i < 4; i++) { 138 list1.add(i); 139 140 if (i == 1) { 141 second = list1.current; //把第二個結點記下來 142 } 143 } 144 145 list1.add(second); //將尾結點指向鏈表的第二個結點,於是單鏈表就有環了,備注:此時得到的環的結構,是本節中圖2的那種結構 146 Node current = list1.hasCycle(list1.head); //獲取相遇的那個結點 147 148 int length = list1.getCycleLength(current); //獲取環的長度 149 150 System.out.println("環的起始點是" + list1.getCycleStart(list1.head, length).data); 151 152 } 153 154 }
11、判斷兩個單鏈表相交的第一個交點:
《劍指offer》P193,5.3,面試題37就有這道題。
面試時,很多人碰到這道題的第一反應是:在第一個鏈表上順序遍歷每個結點,每遍歷到一個結點的時候,在第二個鏈表上順序遍歷每個結點。如果在第二個鏈表上有一個結點和第一個鏈表上的結點一樣,說明兩個鏈表在這個結點上重合。顯然該方法的時間復雜度為O(len1 * len2)。
方法1:采用棧的思路
我們可以看出兩個有公共結點而部分重合的鏈表,拓撲形狀看起來像一個Y,而不可能是X型。 如下圖所示:
如上圖所示,如果單鏈表有公共結點,那么最后一個結點(結點7)一定是一樣的,而且是從中間的某一個結點(結點6)開始,后續的結點都是一樣的。
現在的問題是,在單鏈表中,我們只能從頭結點開始順序遍歷,最后才能到達尾結點。最后到達的尾節點卻要先被比較,這聽起來是不是像“先進后出”?於是我們就能想到利用棧的特點來解決這個問題:分別把兩個鏈表的結點放入兩個棧中,這樣兩個鏈表的尾結點就位於兩個棧的棧頂,接下來比較下一個棧頂,直到找到最后一個相同的結點。
這種思路中,我們需要利用兩個輔助棧,空間復雜度是O(len1+len2),時間復雜度是O(len1+len2)。和一開始的蠻力法相比,時間效率得到了提高,相當於是利用空間消耗換取時間效率。
那么,有沒有更好的方法呢?接下來要講。
方法2:判斷兩個鏈表相交的第一個結點:用到快慢指針,推薦(更優解)
我們在上面的方法2中,之所以用到棧,是因為我們想同時遍歷到達兩個鏈表的尾結點。其實為解決這個問題我們還有一個更簡單的辦法:首先遍歷兩個鏈表得到它們的長度。在第二次遍歷的時候,在較長的鏈表上走 |len1-len2| 步,接着再同時在兩個鏈表上遍歷,找到的第一個相同的結點就是它們的第一個交點。
這種思路的時間復雜度也是O(len1+len2),但是我們不再需要輔助棧,因此提高了空間效率。當面試官肯定了我們的最后一種思路的時候,就可以動手寫代碼了。
核心代碼:
1 //方法:求兩個單鏈表相交的第一個交點 2 public Node getFirstCommonNode(Node head1, Node head2) { 3 if (head1 == null || head == null) { 4 return null; 5 } 6 7 int length1 = getLength(head1); 8 int length2 = getLength(head2); 9 int lengthDif = 0; //兩個鏈表長度的差值 10 11 Node longHead; 12 Node shortHead; 13 14 //找出較長的那個鏈表 15 if (length1 > length2) { 16 longHead = head1; 17 shortHead = head2; 18 lengthDif = length1 - length2; 19 } else { 20 longHead = head2; 21 shortHead = head1; 22 lengthDif = length2 - length1; 23 } 24 25 //將較長的那個鏈表的指針向前走length個距離 26 for (int i = 0; i < lengthDif; i++) { 27 longHead = longHead.next; 28 } 29 30 //將兩個鏈表的指針同時向前移動 31 while (longHead != null && shortHead != null) { 32 if (longHead == shortHead) { //第一個相同的結點就是相交的第一個結點 33 return longHead; 34 } 35 longHead = longHead.next; 36 shortHead = shortHead.next; 37 } 38 39 return null; 40 } 41 42 43 //方法:獲取單鏈表的長度 44 public int getLength(Node head) { 45 if (head == null) { 46 return 0; 47 } 48 49 int length = 0; 50 Node current = head; 51 while (current != null) { 52 53 length++; 54 current = current.next; 55 } 56 57 return length;
參考:
鏈接:http://blog.csdn.net/fightforyourdream/article/details/16353519
書籍:《劍指offer》
明天就是騰訊的在線筆試了,加油!!!