Skip to content

Commit f9bdbee

Browse files
authored
Merge pull request #45 from miguelHx/miguel_2.8_loop_detection
Miguel 2.8 - Loop Detection [Python]
2 parents 67dc158 + fcea761 commit f9bdbee

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
"""
2+
Python version 3.7.0
3+
2.8 - Loop Detection
4+
Given a circular linked list, implement an algorithm that
5+
returns the node at the beginning of the loop.
6+
DEFINITION
7+
Circular linked list: A (corrupt) linked list in which
8+
a node's next pointer points to an earlier node, so as
9+
to make a loop in the linked list.
10+
EXAMPLE
11+
Input: A -> B -> C -> D -> E -> C [the same C as earlier]
12+
Output: C
13+
"""
14+
import unittest
15+
from typing import Optional, NamedTuple
16+
17+
18+
class Node:
19+
def __init__(self, d: int):
20+
self.data = d
21+
self.next = None
22+
23+
def __repr__(self):
24+
return self.__str__()
25+
26+
def __str__(self):
27+
return '<Node Value: {}>'.format(self.data)
28+
29+
def __eq__(self, other: object):
30+
if not isinstance(other, Node):
31+
return NotImplemented
32+
return self.data == other.data
33+
34+
def __hash__(self):
35+
"""
36+
Hash based on node's memory address.
37+
:return:
38+
"""
39+
return id(self)
40+
41+
42+
class LinkedList:
43+
def __init__(self, *numbers: int):
44+
self.head = None
45+
self.tail = None
46+
self.size = 0
47+
for num in numbers:
48+
self.append_to_tail(num)
49+
50+
def append_to_tail(self, e) -> None:
51+
if isinstance(e, int):
52+
self._append_num(e)
53+
elif isinstance(e, Node):
54+
self._append_node(e)
55+
56+
def _append_num(self, d: int) -> None:
57+
if self.head is None:
58+
self.head = Node(d)
59+
self.tail = self.head
60+
else:
61+
end = Node(d)
62+
self.tail.next = end
63+
self.tail = end
64+
self.size += 1
65+
66+
def _append_node(self, n: Node) -> None:
67+
if self.head is None:
68+
self.head = n
69+
self.tail = self.head
70+
else:
71+
end = n
72+
self.tail.next = end
73+
self.tail = end
74+
self.size += 1
75+
76+
def append_to_head(self, d: int) -> None:
77+
new_head = Node(d)
78+
new_head.next = self.head
79+
if self.head is None:
80+
# if list is empty and we add
81+
# out first element, head AND tail
82+
# must point to same node
83+
self.tail = new_head
84+
self.head = new_head
85+
self.size += 1
86+
87+
def get_node_at(self, index: int) -> Node:
88+
if index < 0 or index >= self.size:
89+
raise IndexError('list index out of range')
90+
n = self.head
91+
for i in range(self.size):
92+
if i == index:
93+
return n
94+
n = n.next
95+
96+
def get_value_at(self, index: int) -> int:
97+
if index < 0 or index >= self.size:
98+
raise IndexError('list index out of range')
99+
n = self.head
100+
for i in range(self.size):
101+
if i == index:
102+
return n.data
103+
n = n.next
104+
105+
def pop_head(self) -> Node:
106+
if self.head is None:
107+
raise IndexError('no head to pop')
108+
h = self.head
109+
h.next = None
110+
self.head = self.head.next
111+
self.size -= 1
112+
return h
113+
114+
def append(self, ll: 'LinkedList') -> None:
115+
self.tail.next = ll.head
116+
self.tail = ll.tail
117+
self.size += ll.size
118+
ll.head = None
119+
ll.size = 0
120+
121+
def reverse(self) -> None:
122+
"""
123+
Reverses this linked list in place
124+
:return:
125+
"""
126+
if self.head is None:
127+
return
128+
prev = self.head
129+
self.tail = prev
130+
curr = prev.next
131+
self.tail.next = None
132+
while curr is not None:
133+
old_next = curr.next
134+
curr.next = prev
135+
prev = curr
136+
curr = old_next
137+
self.head = prev
138+
139+
def __repr__(self):
140+
return self.__str__()
141+
142+
def __str__(self):
143+
if self.head is None:
144+
return '<empty>'
145+
ll = []
146+
n = self.head
147+
while n.next is not None:
148+
ll.append('{} -> '.format(n.data))
149+
n = n.next
150+
ll.append(str(n.data))
151+
return ''.join(ll)
152+
153+
def __eq__(self, other: object):
154+
if not isinstance(other, LinkedList):
155+
return NotImplemented
156+
a = self.head
157+
b = other.head
158+
while a is not None and b is not None:
159+
if a.data != b.data:
160+
return False
161+
# otherwise, advance both pointers
162+
a = a.next
163+
b = b.next
164+
return a is None and b is None
165+
166+
167+
def loop_detection_linear_time_const_space(ll: LinkedList):
168+
"""
169+
This function will determine if there is a
170+
cycle in the input linked list.
171+
A linked list is 'circular' when a node's
172+
next pointer points to an earlier node, so
173+
as to make a loop in the linked list.
174+
Floyd cycle-finding algorithm:
175+
https://www.geeksforgeeks.org/detect-loop-in-a-linked-list/
176+
Runtime: O(n)
177+
Space Complexity: O(1)
178+
:param ll: an input linked list
179+
:return: the corrupt node or None
180+
"""
181+
# for the case of a linked list with
182+
# a single node, non-corrupt
183+
if ll.head and ll.head.next is None:
184+
return None
185+
slow_ptr = ll.head
186+
fast_ptr = ll.head
187+
while slow_ptr and fast_ptr and fast_ptr.next:
188+
slow_ptr = slow_ptr.next
189+
fast_ptr = fast_ptr.next.next
190+
if fast_ptr.next is None:
191+
return None
192+
if slow_ptr is fast_ptr:
193+
# we have a cycle
194+
break
195+
# if we get here, then there is a cycle.
196+
# advance one of fast or slow pointers
197+
# and a pointer that starts in the
198+
# beginning, by one until they match.
199+
# they will end at the beginning of
200+
# the cycle.
201+
p = ll.head
202+
while p is not slow_ptr:
203+
p = p.next
204+
slow_ptr = slow_ptr.next
205+
return p
206+
207+
208+
def loop_detection_const_space(ll: LinkedList) -> Optional[Node]:
209+
"""
210+
This function will determine if there is a
211+
cycle in the input linked list.
212+
A linked list is 'circular' when a node's
213+
next pointer points to an earlier node, so
214+
as to make a loop in the linked list.
215+
Runtime: O(n^2)
216+
Space Complexity: O(1)
217+
:param ll: an input linked list
218+
:return: the corrupt node or None
219+
"""
220+
# for the case of a single-node corrupt linked list
221+
if ll.head and ll.head.next is ll.head:
222+
return ll.head
223+
# this algorithm will traverse through the
224+
# linked list, and at each element, we will loop from
225+
# the start up to the current node, comparing
226+
# the next pointer of the current node with
227+
# each node leading up to the current node
228+
curr_node = ll.head
229+
while curr_node is not None:
230+
n = ll.head # n is a node
231+
# we will be traversing 'n' up to the current node
232+
# to see if a previous node happens to be the
233+
# 'next' of the current node.
234+
while n is not curr_node:
235+
if curr_node.next is n:
236+
# cycle found
237+
return n
238+
n = n.next
239+
curr_node = curr_node.next
240+
return None
241+
242+
243+
def loop_detection(ll: LinkedList) -> Optional[Node]:
244+
"""
245+
This function will determine if there is a
246+
cycle in the input linked list.
247+
A linked list is 'circular' when a node's
248+
next pointer points to an earlier node, so
249+
as to make a loop in the linked list.
250+
Runtime: O(n)
251+
Space Complexity: O(n)
252+
:param ll: an input linked list
253+
:return: the corrupt node or None
254+
"""
255+
nodes_seen = set()
256+
n = ll.head
257+
while n is not None:
258+
if n in nodes_seen:
259+
return n
260+
nodes_seen.add(n)
261+
n = n.next
262+
return None
263+
264+
265+
class CorruptLLStructure(NamedTuple):
266+
first_segment: LinkedList
267+
second_segment: LinkedList
268+
corrupt_node: Node
269+
270+
271+
class TestLoopDetection(unittest.TestCase):
272+
273+
def setUp(self):
274+
corrupt_structures = [
275+
CorruptLLStructure(
276+
LinkedList(1, 2),
277+
LinkedList(4, 5),
278+
Node(3)
279+
),
280+
CorruptLLStructure(
281+
LinkedList(1),
282+
LinkedList(3),
283+
Node(2)
284+
),
285+
CorruptLLStructure(
286+
LinkedList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
287+
LinkedList(13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23),
288+
Node(12)
289+
),
290+
CorruptLLStructure(
291+
LinkedList(1, 2, 3, 4, 5),
292+
LinkedList(7, 8, 9),
293+
Node(6)
294+
),
295+
CorruptLLStructure(
296+
LinkedList(),
297+
LinkedList(2, 3, 4, 5, 6, 7, 8, 9),
298+
Node(1)
299+
),
300+
CorruptLLStructure(
301+
LinkedList(1),
302+
LinkedList(3, 4, 5, 6, 7, 8, 9),
303+
Node(2)
304+
)
305+
]
306+
self.loop_detection_test_cases = []
307+
for s in corrupt_structures:
308+
s.first_segment.append_to_tail(s.corrupt_node)
309+
s.first_segment.append(s.second_segment)
310+
s.first_segment.tail.next = s.corrupt_node
311+
self.loop_detection_test_cases.append((s.first_segment, s.corrupt_node))
312+
313+
def test_loop_detection(self):
314+
for ll, corrupt_node in self.loop_detection_test_cases:
315+
self.assertEqual(loop_detection(ll), corrupt_node)
316+
self.assertEqual(loop_detection_const_space(ll), corrupt_node)
317+
self.assertEqual(loop_detection_linear_time_const_space(ll), corrupt_node)
318+
319+
def test_loop_detection_single_node_ll(self):
320+
ll = LinkedList()
321+
ll.append_to_tail(1)
322+
corrupt_node = ll.head
323+
ll.head.next = corrupt_node
324+
self.assertEqual(loop_detection(ll), corrupt_node)
325+
self.assertEqual(loop_detection_const_space(ll), corrupt_node)
326+
self.assertEqual(loop_detection_linear_time_const_space(ll), corrupt_node)
327+
328+
def test_loop_detection_empty_ll(self):
329+
ll = LinkedList()
330+
self.assertIsNone(loop_detection(ll))
331+
self.assertIsNone(loop_detection_const_space(ll))
332+
self.assertIsNone(loop_detection_linear_time_const_space(ll))
333+
334+
def test_loop_detection_non_corrupt_ll(self):
335+
for ll in [
336+
LinkedList(1, 2, 3, 4, 5),
337+
LinkedList(1),
338+
LinkedList()
339+
]:
340+
self.assertIsNone(loop_detection(ll))
341+
self.assertIsNone(loop_detection_const_space(ll))
342+
self.assertIsNone(loop_detection_linear_time_const_space(ll))
343+
344+
345+
if __name__ == '__main__':
346+
unittest.main()

0 commit comments

Comments
 (0)