如何判断链表有环
前天晚上临睡觉前看到了公众号脚本之家推送的一篇文章,文章内容是一道算法题,并给出了思路解释,但没有具体源码实现,这让我觉得少了点什么,于是,趁周末,我补齐了缺失的内容,好了,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 }
方法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 }
如有错误,欢迎指正。
代码已上传至github: