如何判断链表有环

前天晚上临睡觉前看到了公众号脚本之家推送的一篇文章,文章内容是一道算法题,并给出了思路解释,但没有具体源码实现,这让我觉得少了点什么,于是,趁周末,我补齐了缺失的内容,好了,no code, no bb,我们开始吧。

题目描述:

有一个单向链表,链表中有可能出现“环”,就像下图这样。那么,如何用程序来判断该链表是否为有环链表呢?(图片来自公众号)

 

方法1:

从头开始遍历整个单链表,每遍历一个新节点,就把它与之前遍历过的节点进行比较,如果值相同,那么就认为这两个节点是一个节点,则证明链表有环,停止遍历,否则继续遍历下一个节点,重复刚才的操作,直到遍历结束。结合上图来说,流程是这样的:

① 得到 “3”节点,把它与第一个节点 “5”比较,值不相等,继续遍历下一个节点 “7”。(从第二个节点开始遍历)

② 得到 “7”节点,把它依次与 “5”、“3”比较,值不相等,继续遍历下一个节点 “2”

③ 重复以上操作,直到遍历完节点 “1”

④ 得到节点 “2”,把它依次与 “5”、“3”、“7”、“2”、“6”、“8”、“1”进行比较,当比较到节点 “2”时,值相等,遍历结束,证明该链表有环。

假设链表的节点数量为n,则该解法的时间复杂度为O(n2),由于没有创建额外的存储空间,所以空间复杂度为O(1)

链表的实现比较简单,我只写了一个add方法,一个display方法:

 1 //单向链表
 2 public class SingleLinkedList {
 3     private Node head;//标识头节点
 4     private int size;//标识链表中节点个数
 5     
 6     public SingleLinkedList() {
 7         this.size = 0;
 8         this.head = null;
 9     }
10     
11     //node类
12     private class Node{
13         private Object data;//每个节点的数据
14         private Node next;//指向下一个节点的链接
15         
16         public Node(Object data) {
17             this.data = data;
18         }
19     }
20     
21     /**
22      * 将节点插入链表
23      * @param data 带插入的值
24      */
25     public void add(Object data) {
26         Node temp = head;
27         if (size == 0) {
28             head = new Node(data);
29             size++;
30             return;
31         }
32         while (temp.next != null) {
33             temp = temp.next;
34         }
35         temp.next = new Node(data);
36         size++;
37     }
38     
39     /**
40      * 从头开始遍历节点
41      */
42     public void display() {
43         if (size > 0) {
44             Node node = head;
45             if (size == 1) {
46                 System.out.println("[" + node.data + "]");
47                 return;
48             }
49             while (node != null) {
50                 System.out.println(node.data);
51                 node = node.next;
52             }
53         } else {
54             System.out.println("[]");
55         }
56     }
57 }

View Code

方法1如下:

 1     /**
 2      * 根据索引得到链表的某个节点的值
 3      * @param key
 4      * @return
 5      */
 6     public Object getNode(int key) {
 7         
 8         if (key < 0 || key > size - 1) {
 9             throw new ArrayIndexOutOfBoundsException("越界!");
10         } else {
11             Node temp = head;
12             int count = 0;
13             while (temp != null) {
14                 if (count == key) {
15                     return temp.data;
16                 }
17                 temp = temp.next;
18                 count++;
19             }
20             
21         }
22         return null;
23     }
24     
25     
26     /**
27      * 从头开始,依次与给定索引位置的节点的值进行比较,如果相同,则返回true,否则false
28      * @param key
29      * @return
30      */
31     public boolean havaSameElement(int key) {
32         boolean flag = false;
33         int count = 0;
34         Node temp = head;
35         while (temp != null && count < key) {
36             if (temp.data == getNode(key)) {
37                 flag = true;
38                 return flag;
39             }
40             count++;
41             temp = temp.next;
42         }
43         return flag;
44         
45     }
46     
47     /**
48      * 方式1
49      * @return
50      */
51     public boolean isAnnulate1() {
52         boolean flag = false;
53         for (int i = 1; i < size; i++) {
54             if (havaSameElement(i)) {
55                 flag = true;
56                 break;
57             }
58         }
59         return flag;
60     }

方法2:


这种方法用到了HashSet中add方法去重的特点,思路是这样的:

① new一个HashSet,用来存储之前遍历过的节点值

②从头节点head开始,依次遍历链表中的节点,并把它add到集合中

③ 如果在集合中已经有一个相同的值,那么会返回false,这样便证明链表有环,退出遍历

方法2如下:

 1     /**
 2      * 方式2
 3      * @return
 4      */
 5     public boolean isAnnulate2() {
 6         boolean flag = false;
 7         Set<Object> set = new HashSet<>();
 8         Node temp = head;
 9         while (temp != null) {
10             if (!set.add(temp.data)) {
11                 flag = true;
12                 break;
13             }
14             temp = temp.next;
15         }
16         return flag;
17 
18     }

 

方法3:

定义两个指针tortoise与rabbit,让它们一开始均指向head头节点,之后,tortoise每次向后移动一个节点,rabbit每次向后移动2个节点,只要这个链表是有环的,它们必定会在某一次移动完后相遇,什么?你问我为什么?我们来思考这样一个问题,两个人在运动场跑步,他们的起始位置都是一样的,当开跑后,只有在两种情况下,他们的位置会重合,第一就是他们的速度始终一致,第二就是跑得快的那个人套圈,如下图所示:

 

我们假设两位跑步的同学速度始终不变,即tortoise以V的速度进行移动,rabbit以2V的速度进行移动,在经过了相同的时间T后,他们相遇了,此时tortoise移动的距离为VT,而rabbit移动的距离为2VT,他们移动的距离差VT,即为这个链表中 “环”的周长,如上图所示,节点A表示为环入口,节点B表示他们第一次相遇,我们将head头节点至节点A的距离记为a,将节点A至他们第一次相遇的节点B的距离记为b,通过我们刚才的分析,不难得出,tortoise移动的距离VT = a + b,等量代换,他们移动的距离差也为 a+ b,所以链表中环的周长为 a + b,现在已知节点A至节点B的距离为b,那么节点B至节点A的距离便为a,至此,发现什么了?head头节点到节点A的距离同样为a,我们建立一个指针 start 指向头节点,它与B节点处的tortoise同时以一个节点的速度进行移动,一段时间后,它们将在环入口相遇。我们不光能证明一个链表是否有环,还能找到环的入口。

方法3如下:

 1     public Node getIntersect() {
 2         Node temp = head;
 3         Node tortoise = temp;
 4         Node rabbit = temp;
 5         while (rabbit != null && rabbit.next != null) {
 6             tortoise = tortoise.next;
 7             rabbit = rabbit.next.next;
 8             if (tortoise == rabbit) {
 9                 return tortoise;
10             }
11         }
12         return null;
13     }
14     
15     public Object isAnnulate3() {
16         Node temp = head;
17         if (temp == null) {
18             return null;
19         }
20         Node intersect = getIntersect();
21         if (intersect == null) {
22             return null;
23         }
24         Node startNode = head;
25         while (startNode != intersect) {
26             startNode = startNode.next;
27             intersect = intersect.next;
28         }
29         return startNode.data;
30         
31     }

我要说明的是,方法3中的代码只是 “伪代码”,它并不能真的证明链表有环,并返回环的入口节点值。至于为什么,我的理解是,因为单链表的结构特点,它并不会真的存在 “环”,我们这里说的环只是把它抽象出来,实际上,单链表中一个节点只保留有对它后面那个节点的引用,并没有对它前面节点的引用,所以,并不存在真正的 “环”,不过,这种思路还是值得学习的。假设链表的节点数量为n,则该算法的时间复杂度为O(n),除指针外,没有占用任何额外的存储空间,所以空间复杂度为O(1)。

完整代码如下:

  1 package judgeLinkedListCircle;
  2 
  3 import java.util.HashSet;
  4 import java.util.Set;
  5 
  6 
  7 /**
  8  * 单向链表
  9  * @author Cone
 10  * @since 2019年7月27日
 11  *
 12  */
 13 public class SingleLinkedList {
 14     private Node head;//标识头节点
 15     private int size;//标识链表中节点个数
 16     
 17     public SingleLinkedList() {
 18         this.size = 0;
 19         this.head = null;
 20     }
 21     
 22     //node类
 23     private class Node{
 24         private Object data;//每个节点的数据
 25         private Node next;//指向下一个节点的链接
 26         
 27         public Node(Object data) {
 28             this.data = data;
 29         }
 30     }
 31     
 32     /**
 33      * 将节点插入链表
 34      * @param data 带插入的值
 35      */
 36     public void add(Object data) {
 37         Node temp = head;
 38         if (size == 0) {
 39             head = new Node(data);
 40             size++;
 41             return;
 42         }
 43         while (temp.next != null) {
 44             temp = temp.next;
 45         }
 46         temp.next = new Node(data);
 47         size++;
 48     }
 49     
 50     /**
 51      * 从头开始遍历节点
 52      */
 53     public void display() {
 54         if (size > 0) {
 55             Node node = head;
 56             if (size == 1) {
 57                 System.out.println("[" + node.data + "]");
 58                 return;
 59             }
 60             while (node != null) {
 61                 System.out.println(node.data);
 62                 node = node.next;
 63             }
 64         } else {
 65             System.out.println("[]");
 66         }
 67     }
 68     
 69     /**
 70      * 根据索引得到链表的某个节点的值
 71      * @param key
 72      * @return
 73      */
 74     public Object getNode(int key) {
 75         
 76         if (key < 0 || key > size - 1) {
 77             throw new ArrayIndexOutOfBoundsException("越界!");
 78         } else {
 79             Node temp = head;
 80             int count = 0;
 81             while (temp != null) {
 82                 if (count == key) {
 83                     return temp.data;
 84                 }
 85                 temp = temp.next;
 86                 count++;
 87             }
 88             
 89         }
 90         return null;
 91     }
 92     
 93     
 94     /**
 95      * 从头开始,依次与给定索引位置的节点的值进行比较,如果相同,则返回true,否则false
 96      * @param key
 97      * @return
 98      */
 99     public boolean havaSameElement(int key) {
100         boolean flag = false;
101         int count = 0;
102         Node temp = head;
103         while (temp != null && count < key) {
104             if (temp.data == getNode(key)) {
105                 flag = true;
106                 return flag;
107             }
108             count++;
109             temp = temp.next;
110         }
111         return flag;
112         
113     }
114     
115     /**
116      * 方式1
117      * @return
118      */
119     public boolean isAnnulate1() {
120         boolean flag = false;
121         for (int i = 1; i < size; i++) {
122             if (havaSameElement(i)) {
123                 flag = true;
124                 break;
125             }
126         }
127         return flag;
128     }
129     
130     
131     /**
132      * 方式2
133      * @return
134      */
135     public boolean isAnnulate2() {
136         boolean flag = false;
137         Set<Object> set = new HashSet<>();
138         Node temp = head;
139         while (temp != null) {
140             if (!set.add(temp.data)) {
141                 flag = true;
142                 break;
143             }
144             temp = temp.next;
145         }
146         return flag;
147 
148     }
149     
150     public Node getIntersect() {
151         Node temp = head;
152         Node tortoise = temp;
153         Node rabbit = temp;
154         while (rabbit != null && rabbit.next != null) {
155             tortoise = tortoise.next;
156             rabbit = rabbit.next.next;
157             if (tortoise == rabbit) {
158                 return tortoise;
159             }
160         }
161         return null;
162     }
163     
164     /**
165      * 方式3
166      * @return
167      */
168     public Object isAnnulate3() {
169         Node temp = head;
170         if (temp == null) {
171             return null;
172         }
173         Node intersect = getIntersect();
174         if (intersect == null) {
175             return null;
176         }
177         Node startNode = head;
178         while (startNode != intersect) {
179             startNode = startNode.next;
180             intersect = intersect.next;
181         }
182         return startNode.data;
183         
184     }
185 
186 }

View Code

如有错误,欢迎指正。

代码已上传至github:

https://github.com/Thinker-Mars/ByteDance

 

版权声明:本文为cone原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/cone/p/11257063.html