Given the head
of a singly linked list, return the middle node of the linked list. If there are two middle nodes, return the second middle node.
For example:
Input: head = [1, 2, 3, 4, 5]
Output: [3, 4, 5]
Input: head = [1, 2, 3, 4, 5, 6]
Output: [4, 5, 6]
Explain two different approaches to solve this problem, and detail the time and space complexity. Be sure to explain any edge cases.
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force method for finding the middle of a linked list is like figuring out which item is in the middle of a train without knowing how many train cars there are. We will walk through the entire train step by step.
Here's how the algorithm would work step-by-step:
class ListNode:
def __init__(self, value=0, next_node=None):
self.value = value
self.next_node = next_node
def find_middle_brute_force(head):
list_length = 0
current_node = head
# Iterate through the list to get the length
while current_node:
list_length += 1
current_node = current_node.next_node
middle_index = list_length // 2
current_node = head
current_index = 0
# Traverse the list until the middle is found
while current_index < middle_index:
current_node = current_node.next_node
current_index += 1
return current_node
We can find the middle of a linked list without knowing its total size beforehand by using two 'pointers' that move at different speeds. This cleverly allows us to reach the middle in a single pass through the list.
Here's how the algorithm would work step-by-step:
def middle_node(head):
slow_pointer = head
fast_pointer = head
# Traverse list with slow_pointer moving 1 step and fast_pointer moving 2 steps
while fast_pointer and fast_pointer.next:
slow_pointer = slow_pointer.next
fast_pointer = fast_pointer.next.next
#When fast_pointer reaches the end, slow_pointer will be at the middle
return slow_pointer
class ListNode:
def __init__(self, value=0, next_node=None):
self.value = value
self.next = next_node
# Example Usage (Not part of the solution, for testing purposes only)
if __name__ == '__main__':
# Create a sample linked list: 1 -> 2 -> 3 -> 4 -> 5
head_node = ListNode(1)
head_node.next = ListNode(2)
head_node.next.next = ListNode(3)
head_node.next.next.next = ListNode(4)
head_node.next.next.next.next = ListNode(5)
middle = middle_node(head_node)
print(middle.value) # Output: 3
# Create another list: 1 -> 2 -> 3 -> 4 -> 5 -> 6
head_node = ListNode(1)
head_node.next = ListNode(2)
head_node.next.next = ListNode(3)
head_node.next.next.next = ListNode(4)
head_node.next.next.next.next = ListNode(5)
head_node.next.next.next.next.next = ListNode(6)
middle = middle_node(head_node)
print(middle.value) # Output: 4
Case | How to Handle |
---|---|
Null or empty linked list (head is null) | Return null immediately as there is no middle node in an empty list. |
Single-node linked list | Return the head node itself, as it's the middle node. |
Two-node linked list | Return the second node, fulfilling the requirement to return the 'second' middle node in case of even length. |
Large linked list (potential performance concerns) | The fast and slow pointer approach has O(n) time complexity and constant space complexity, scaling efficiently. |
Linked list with duplicate values | The values within the nodes are irrelevant to finding the middle node using the fast/slow pointer approach. |
Linked list with a very long length near integer limit for storing length | The fast and slow pointer approach avoids storing the length, preventing overflow issues. |
Linked list with negative, zero, and positive values | Node values do not affect the algorithm, so it works fine with any value. |
Memory limitations prevent creating a large array to store node values. | The fast/slow pointer method uses constant space, avoiding memory limitations. |