Skip to content

Commit b0cd89c

Browse files
authored
Merge pull request #159 from BrianLusina/feat/data-structures-linked-list-remove-cycle
feat(data-structures, linked-lists, remove cycle): utilities to detect and remove cycles
2 parents bdca77c + ecae2ac commit b0cd89c

File tree

2 files changed

+170
-97
lines changed

2 files changed

+170
-97
lines changed

datastructures/linked_lists/__init__.py

Lines changed: 15 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
from abc import ABCMeta, abstractmethod
33

44
from datastructures.linked_lists.exceptions import EmptyLinkedList
5+
from datastructures.linked_lists.linked_list_utils import (
6+
has_cycle,
7+
detect_node_with_cycle,
8+
cycle_length,
9+
remove_cycle,
10+
)
511

612
T = TypeVar("T")
713

@@ -379,14 +385,7 @@ def has_cycle(self):
379385
:return: True if there is a cycle, False otherwise
380386
:rtype: bool
381387
"""
382-
fast_pointer = slow_pointer = self.head
383-
while fast_pointer and slow_pointer and fast_pointer.next:
384-
fast_pointer = fast_pointer.next.next
385-
slow_pointer = slow_pointer.next
386-
387-
if slow_pointer == fast_pointer:
388-
return True
389-
return False
388+
return has_cycle(self.head)
390389

391390
def cycle_length(self) -> int:
392391
"""
@@ -395,105 +394,25 @@ def cycle_length(self) -> int:
395394
Returns:
396395
int: length of the cycle or number of nodes in the cycle
397396
"""
398-
if not self.head:
399-
return 0
400-
slow_pointer = fast_pointer = self.head
401-
402-
while fast_pointer and fast_pointer.next:
403-
slow_pointer = slow_pointer.next
404-
fast_pointer = fast_pointer.next.next
405-
406-
# Cycle detected
407-
if slow_pointer is fast_pointer:
408-
length = 1
409-
# Move slow pointer by one step to start counting
410-
slow_pointer = slow_pointer.next
411-
412-
# Continue moving the slow pointer until it meets the fast pointer again
413-
while slow_pointer != fast_pointer:
414-
length += 1
415-
slow_pointer = slow_pointer.next
416-
417-
return length
418-
419-
return 0
397+
return cycle_length(self.head)
420398

421399
def detect_node_with_cycle(self) -> Optional[Node]:
422400
"""
423401
Detects the node with a cycle and returns it
424402
"""
425-
if not self.has_cycle():
426-
return None
427-
else:
428-
slow_pointer = fast_pointer = self.head
429-
430-
while fast_pointer and slow_pointer and fast_pointer.next:
431-
fast_pointer = fast_pointer.next.next
432-
slow_pointer = slow_pointer.next
433-
434-
if slow_pointer == fast_pointer:
435-
break
436-
else:
437-
return None
438-
439-
while self.head != slow_pointer:
440-
slow_pointer = slow_pointer.next
441-
self.head = self.head.next
442-
return self.head
403+
return detect_node_with_cycle(self.head)
443404

444-
def remove_cycle(self):
405+
def remove_cycle(self) -> Optional[Node]:
445406
"""
446407
Removes cycle if there exists. This will use the same concept as has_cycle method to check if there is a loop
447408
and remove the cycle
448-
if one is found.
449-
1) Detect Loop using Floyd’s Cycle detection algo and get the pointer to a loop node.
450-
2) Count the number of nodes in loop. Let the count be k.
451-
3) Fix one pointer to the head and another to kth node from head.
452-
4) Move both pointers at the same pace, they will meet at loop starting node.
453-
5) Get pointer to the last node of loop and make next of it as NULL.
454-
:return: True if the cycle has been removed, False otherwise
455-
:rtype: bool
409+
Returns:
410+
Node: head node with cycle removed
456411
"""
457-
fast_pointer = slow_pointer = self.head
458-
459-
while fast_pointer and slow_pointer and fast_pointer.next:
460-
fast_pointer = fast_pointer.next.next
461-
slow_pointer = slow_pointer.next
462-
463-
if slow_pointer == fast_pointer:
464-
pointer_1 = pointer_2 = slow_pointer
465-
466-
# Count the number of nodes in loop
467-
k = 1
468-
while pointer_1.next != pointer_2:
469-
pointer_1 = pointer_1.next
470-
k += 1
471-
472-
# Fix one pointer to head
473-
pointer_1 = self.head
474-
475-
# And the other pointer to k nodes after head
476-
pointer_2 = self.head
477-
for _ in range(k):
478-
pointer_2 = pointer_2.next
479-
480-
# Move both pointers at the same place
481-
# they will meet at loop starting node
482-
while pointer_2 != pointer_1:
483-
pointer_1 = pointer_1.next
484-
pointer_2 = pointer_2.next
485-
486-
# Get pointer to the last node
487-
pointer_2 = pointer_2.next
488-
while pointer_2.next != pointer_1:
489-
pointer_2 = pointer_2.next
490-
491-
# Set the next node of the loop ending node
492-
# to fix the loop
493-
pointer_2.next = None
494-
return True
412+
if not self.head or not self.head.next:
413+
return self.head
495414

496-
return False
415+
return remove_cycle(self.head)
497416

498417
@abstractmethod
499418
def alternate_split(self):

datastructures/linked_lists/linked_list_utils.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Optional, Callable, Any
22
from datastructures.linked_lists import Node
33

44

@@ -19,3 +19,157 @@ def find_middle_node(head: Optional[Node]) -> Optional[Node]:
1919
fast_pointer = fast_pointer.next.next
2020

2121
return slow_pointer
22+
23+
24+
def has_cycle(
25+
head: Optional[Node], func: Optional[Callable[[Node, Node], Any]] = None
26+
) -> bool:
27+
f"""
28+
Checks if a given linked list has a cycle if a head node is provided. A cycle is when a linked list node can be
29+
reached again after traversing the entire linked list
30+
Args:
31+
head(Node): head node of a linked list
32+
func(Callable): An optional callable function that if passed in will be executed
33+
Returns:
34+
bool: True if there is a cycle, False otherwise
35+
"""
36+
if not head or not head.next:
37+
return False
38+
39+
fast_pointer, slow_pointer = head, head
40+
while fast_pointer and fast_pointer.next:
41+
slow_pointer = slow_pointer.next
42+
fast_pointer = fast_pointer.next.next
43+
44+
if slow_pointer is fast_pointer:
45+
# do something after detecting cycle passing in the two nodes
46+
if func:
47+
func(slow_pointer, fast_pointer)
48+
return True
49+
return False
50+
51+
52+
def cycle_length(head: Optional[Node]) -> int:
53+
"""
54+
Determines the length of the cycle in a linked list if it has one. The length of the cycle is the number
55+
of nodes that are 'caught' in the cycle
56+
Args:
57+
head(Node): head node of linked list
58+
Returns:
59+
int: length of the cycle or number of nodes in the cycle
60+
"""
61+
if not head:
62+
return 0
63+
slow_pointer = fast_pointer = head
64+
65+
while fast_pointer and fast_pointer.next:
66+
slow_pointer = slow_pointer.next
67+
fast_pointer = fast_pointer.next.next
68+
69+
# Cycle detected
70+
if slow_pointer is fast_pointer:
71+
length = 1
72+
# Move slow-pointer by one step to start counting
73+
slow_pointer = slow_pointer.next
74+
75+
# Continue moving the slow pointer until it meets the fast pointer again
76+
while slow_pointer is not fast_pointer:
77+
length += 1
78+
slow_pointer = slow_pointer.next
79+
80+
return length
81+
return 0
82+
83+
84+
def detect_node_with_cycle(head: Optional[Node]) -> Optional[Node]:
85+
"""
86+
Detects a node with a cycle in a Linked List and returns it. The node with a cycle is the entry point of the loop
87+
Args:
88+
head(Node): head node of a linked list
89+
Returns:
90+
Node: node with a cycle if the linked list has a cycle
91+
"""
92+
slow_pointer = fast_pointer = head
93+
94+
while fast_pointer and slow_pointer and fast_pointer.next:
95+
fast_pointer = fast_pointer.next.next
96+
slow_pointer = slow_pointer.next
97+
98+
if slow_pointer == fast_pointer:
99+
break
100+
else:
101+
return None
102+
103+
current = head
104+
while current is not slow_pointer:
105+
slow_pointer = slow_pointer.next
106+
current = current.next
107+
return current
108+
109+
110+
def remove_cycle(head: Optional[Node]) -> Optional[Node]:
111+
"""
112+
Removes cycle from a linked list given the head node
113+
Args:
114+
head(Node): head node of a linked list
115+
Returns:
116+
Node: head node without the cycle
117+
"""
118+
# This is the entry point of the cycle
119+
node_with_cycle = detect_node_with_cycle(head)
120+
121+
# If there is no node with a cycle, return the head node as is
122+
if not node_with_cycle:
123+
return head
124+
125+
# Now, with the node with the cycle, we set a current pointer that will move a node at a time, until its next pointer
126+
# points back to the fixed pointer
127+
current = node_with_cycle
128+
fixed_pointer = node_with_cycle
129+
130+
# Move the pointer on this current until the next pointer reaches the fixed pointer
131+
while current.next is not fixed_pointer:
132+
current = current.next
133+
134+
# Remove the cycle by setting the next to None
135+
current.next = None
136+
137+
return head
138+
139+
def remove_nth_from_end(head: Optional[Node], n: int) -> Optional[Node]:
140+
"""
141+
Removes the nth node from a linked list from the head given the head of the linked list and the position from the
142+
end of the linked list to remove.
143+
Args:
144+
head(Node): head node of linkedlist
145+
n(int): the position of the last node from the tail in the linked list to remove
146+
Returns:
147+
Node: head node of modified linked list
148+
"""
149+
if not head:
150+
return head
151+
152+
# Initialize two pointers, both starting at the head node
153+
fast = slow = head
154+
155+
# Move the fast pointer until it reaches position n in the linked list
156+
for _ in range(n):
157+
fast = fast.next
158+
159+
# If there is no node at this pointers position, then we have reached the end of the linked list and we return the
160+
# next node from the head. This means, we are removing the head node
161+
if not fast:
162+
return head.next
163+
164+
# Move the fast pointer, until it reaches the end of the linked list and until the slow pointer reaches n nodes from
165+
# the end of the linked list
166+
while fast.next:
167+
fast = fast.next
168+
slow = slow.next
169+
170+
# Set the next pointer of the node at the slow pointer's position to the next node's next pointer, removing the node in the middle
171+
slow.next = slow.next.next
172+
173+
# Return the modified head node of the linked list with the node removed
174+
return head
175+

0 commit comments

Comments
 (0)