鏈表面試題Java實現【重要】


 

【聲明】 

歡迎轉載,但請保留文章原始出處→_→ 

生命壹號: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小節中是一致的。

運行效果:

adc768ee-a891-4ad1-9f36-8eadf1ed6437

注:《劍指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

d28e487b-e5c1-4f4b-99a0-7c5d3d0e7b20

上圖中環的長度是4。

但有可能也是下面的這種:(圖2

062fff31-70cc-45fe-aef8-80ed6d51b666

此時,上圖中環的長度就是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 }

 運行效果:

0d5dd16d-69fb-43b4-a99f-32c7c0a7a624

如果將上面的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     }

運行結果:

703c26a6-a04c-450a-9fb7-00fa92a3eb79

如果把上面的代碼中的第8行刪掉,那么這個鏈表就沒有環了,於是運行的結果為0。

 

10、單鏈表中,取出環的起始點:

我們平時碰到的有環鏈表是下面的這種:(圖1

d28e487b-e5c1-4f4b-99a0-7c5d3d0e7b20[1]

上圖中環的起始點1。

但有可能也是下面的這種:(圖2

062fff31-70cc-45fe-aef8-80ed6d51b666[1]

此時,上圖中環的起始點是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型。 如下圖所示:   

ff56631d-76e3-44f9-a32b-cae01e5307e6

如上圖所示,如果單鏈表有公共結點,那么最后一個結點(結點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》

 

明天就是騰訊的在線筆試了,加油!!! 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM