Facebook Pixel

2674. Split a Circular Linked List 🔒

Problem Description

You are given a circular linked list containing positive integers. A circular linked list is like a regular linked list, except the last node points back to the first node, forming a circle.

Your task is to split this circular linked list into two separate circular linked lists:

  1. First circular linked list: Should contain the first half of the nodes, which is exactly ceil(list.length / 2) nodes. For example, if the original list has 5 nodes, the first list gets 3 nodes. If it has 6 nodes, the first list gets 3 nodes.

  2. Second circular linked list: Should contain all the remaining nodes.

Both resulting lists must:

  • Maintain the original order of nodes as they appeared in the input list
  • Be circular (the last node of each list should point back to its own first node)

The function should return an array of length 2, where:

  • The first element is the head of the first circular linked list
  • The second element is the head of the second circular linked list

For example, if you have a circular list 1 -> 2 -> 3 -> 4 -> 5 -> (back to 1), you would split it into:

  • First list: 1 -> 2 -> 3 -> (back to 1)
  • Second list: 4 -> 5 -> (back to 4)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to find the middle point of the circular linked list to know where to split it. Since we can't easily determine the length of a circular linked list without traversing it completely, we can use the classic "fast and slow pointer" technique.

Think of it like two runners on a circular track. If one runner moves twice as fast as the other, when the faster runner completes the circle, the slower runner will be at the halfway point. This is the core idea we'll use.

We set up two pointers:

  • Pointer a (slow) moves one step at a time
  • Pointer b (fast) moves two steps at a time

Since the list is circular, we need to stop when the fast pointer is about to complete the circle or has completed it. We check if b.next == list (fast pointer's next is back to start) or b.next.next == list (fast pointer can take one more step before returning to start).

When we stop, the slow pointer a will be pointing to approximately the middle of the list. If the fast pointer stopped at b.next == list, it means we have an even number of nodes. If it stopped at b.next.next == list, we have an odd number of nodes, and we need to advance b one more step to reach the actual end.

Once we've identified the splitting point, we need to:

  1. Save the reference to where the second list will start (a.next)
  2. Connect the end of the second list (b) back to its beginning to make it circular
  3. Connect the end of the first list (a) back to its beginning to make it circular

This way, we transform one circular list into two separate circular lists without creating new nodes or losing any connections.

Learn more about Linked List and Two Pointers patterns.

Solution Approach

The solution uses the fast and slow pointer technique to find the midpoint of the circular linked list.

Step 1: Initialize Two Pointers

a = b = list

Both pointers a (slow) and b (fast) start at the head of the circular linked list.

Step 2: Move Pointers to Find the Midpoint

while b.next != list and b.next.next != list:
    a = a.next
    b = b.next.next
  • Pointer a moves one step forward each iteration
  • Pointer b moves two steps forward each iteration
  • The loop continues until b is close to completing the circle
  • We check two conditions: b.next != list (one step away from start) and b.next.next != list (two steps away from start)

Step 3: Adjust for Odd Length

if b.next != list:
    b = b.next

If we exited the loop because b.next.next == list (but b.next != list), it means we have an odd number of nodes. We need to move b one more step forward to reach the actual last node of the list.

Step 4: Split the Circular List

list2 = a.next  # Save the start of the second list
b.next = list2  # Make the second list circular
a.next = list   # Make the first list circular
  • list2 = a.next: This saves the reference to the first node of what will become the second circular list
  • b.next = list2: The last node of the original list (b) now points to the start of the second list, making it circular
  • a.next = list: The midpoint node (a) now points back to the original head, making the first half circular

Step 5: Return Both Lists

return [list, list2]

Return an array containing the heads of both circular linked lists.

The algorithm runs in O(n) time where n is the number of nodes, as we traverse the list once. The space complexity is O(1) as we only use a constant amount of extra variables.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with a circular linked list containing 5 nodes: 1 -> 2 -> 3 -> 4 -> 5 -> (back to 1)

Initial Setup:

  • list points to node 1
  • a = b = list (both pointers start at node 1)

Iteration 1:

  • Check: b.next != list? (node 2 ≠ node 1) ✓ and b.next.next != list? (node 3 ≠ node 1) ✓
  • Move: a = a.next (a moves to node 2)
  • Move: b = b.next.next (b moves to node 3)

Iteration 2:

  • Check: b.next != list? (node 4 ≠ node 1) ✓ and b.next.next != list? (node 5 ≠ node 1) ✓
  • Move: a = a.next (a moves to node 3)
  • Move: b = b.next.next (b moves to node 5)

Iteration 3:

  • Check: b.next != list? (node 1 = node 1) ✗
  • Loop exits

Adjust for Odd Length:

  • Since b.next == list (node 5 points to node 1), no adjustment needed
  • a is at node 3 (middle point)
  • b is at node 5 (last node)

Split the Lists:

  1. list2 = a.next (list2 = node 4)
  2. b.next = list2 (node 5 now points to node 4 instead of node 1)
  3. a.next = list (node 3 now points to node 1 instead of node 4)

Result:

  • First circular list: 1 -> 2 -> 3 -> (back to 1) with 3 nodes (ceil(5/2))
  • Second circular list: 4 -> 5 -> (back to 4) with 2 nodes
  • Return: [node 1, node 4]

The original connections 3 -> 4 and 5 -> 1 were replaced with 3 -> 1 and 5 -> 4, effectively splitting one circular list into two separate circular lists while maintaining the original order.

Solution Implementation

1# Definition for singly-linked list.
2# class ListNode:
3#     def __init__(self, val=0, next=None):
4#         self.val = val
5#         self.next = next
6
7from typing import Optional, List
8
9class Solution:
10    def splitCircularLinkedList(
11        self, list: Optional[ListNode]
12    ) -> List[Optional[ListNode]]:
13        """
14        Split a circular linked list into two halves.
15        If the list has odd number of nodes, the first half gets the extra node.
16      
17        Args:
18            list: Head of the circular linked list
19          
20        Returns:
21            A list containing heads of two circular linked lists
22        """
23        # Initialize two pointers for finding the middle
24        # slow_ptr moves one step at a time, fast_ptr moves two steps
25        slow_ptr = fast_ptr = list
26      
27        # Find the middle of the circular linked list using Floyd's algorithm
28        # Stop when fast_ptr reaches the end or one node before the end
29        while fast_ptr.next != list and fast_ptr.next.next != list:
30            slow_ptr = slow_ptr.next
31            fast_ptr = fast_ptr.next.next
32      
33        # If the list has even number of nodes, move fast_ptr one more step
34        # to reach the last node
35        if fast_ptr.next != list:
36            fast_ptr = fast_ptr.next
37      
38        # Second half starts from the node after slow_ptr (middle node)
39        second_half_head = slow_ptr.next
40      
41        # Make the second half circular by connecting last node to second_half_head
42        fast_ptr.next = second_half_head
43      
44        # Make the first half circular by connecting middle node to original head
45        slow_ptr.next = list
46      
47        # Return both circular linked lists
48        return [list, second_half_head]
49
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 *     int val;
5 *     ListNode next;
6 *     ListNode() {}
7 *     ListNode(int val) { this.val = val; }
8 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12    /**
13     * Splits a circular linked list into two halves.
14     * If the list has odd number of nodes, the first half gets the extra node.
15     * 
16     * @param list The head of the circular linked list to split
17     * @return An array containing the heads of the two resulting circular linked lists
18     */
19    public ListNode[] splitCircularLinkedList(ListNode list) {
20        // Initialize slow and fast pointers for finding the middle
21        ListNode slowPointer = list;
22        ListNode fastPointer = list;
23      
24        // Use Floyd's cycle detection algorithm to find the middle
25        // Fast pointer moves twice as fast as slow pointer
26        // Stop when fast pointer is about to complete the circle
27        while (fastPointer.next != list && fastPointer.next.next != list) {
28            slowPointer = slowPointer.next;
29            fastPointer = fastPointer.next.next;
30        }
31      
32        // If the list has even number of nodes, move fast pointer one more step
33        // to reach the last node of the list
34        if (fastPointer.next != list) {
35            fastPointer = fastPointer.next;
36        }
37      
38        // Split the circular list into two halves
39        // slowPointer is now at the last node of the first half
40        ListNode secondHalfHead = slowPointer.next;
41      
42        // Make the second half circular by connecting its tail to its head
43        fastPointer.next = secondHalfHead;
44      
45        // Make the first half circular by connecting its tail to its head
46        slowPointer.next = list;
47      
48        // Return both circular linked lists
49        return new ListNode[] {list, secondHalfHead};
50    }
51}
52
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 *     int val;
5 *     ListNode *next;
6 *     ListNode() : val(0), next(nullptr) {}
7 *     ListNode(int x) : val(x), next(nullptr) {}
8 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13    /**
14     * Splits a circular linked list into two halves.
15     * Uses the slow-fast pointer technique to find the midpoint.
16     * 
17     * @param list Head of the circular linked list
18     * @return Vector containing heads of the two split circular lists
19     */
20    vector<ListNode*> splitCircularLinkedList(ListNode* list) {
21        // Initialize slow and fast pointers
22        ListNode* slowPtr = list;
23        ListNode* fastPtr = list;
24      
25        // Find the midpoint using slow-fast pointer technique
26        // Slow pointer moves one step, fast pointer moves two steps
27        // Loop continues until fast pointer is about to complete the circle
28        while (fastPtr->next != list && fastPtr->next->next != list) {
29            slowPtr = slowPtr->next;
30            fastPtr = fastPtr->next->next;
31        }
32      
33        // If the list has even number of nodes, move fast pointer one more step
34        // to reach the last node of the original list
35        if (fastPtr->next != list) {
36            fastPtr = fastPtr->next;
37        }
38      
39        // Split the circular list into two halves
40        // Second half starts from the node after slow pointer
41        ListNode* secondHalfHead = slowPtr->next;
42      
43        // Make the second half circular by connecting its tail to its head
44        fastPtr->next = secondHalfHead;
45      
46        // Make the first half circular by connecting its tail to its head
47        slowPtr->next = list;
48      
49        // Return both circular halves
50        return {list, secondHalfHead};
51    }
52};
53
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 *     val: number
5 *     next: ListNode | null
6 *     constructor(val?: number, next?: ListNode | null) {
7 *         this.val = (val===undefined ? 0 : val)
8 *         this.next = (next===undefined ? null : next)
9 *     }
10 * }
11 */
12
13/**
14 * Splits a circular linked list into two equal halves.
15 * If the list has odd number of nodes, the first half gets the extra node.
16 * 
17 * @param list - The head of the circular linked list to split
18 * @returns An array containing the heads of the two resulting circular linked lists
19 */
20function splitCircularLinkedList(list: ListNode | null): Array<ListNode | null> {
21    // Handle edge case: empty list
22    if (!list) {
23        return [null, null];
24    }
25  
26    // Initialize two pointers for finding the middle of the list
27    // slowPointer moves one step at a time
28    let slowPointer: ListNode = list;
29    // fastPointer moves two steps at a time
30    let fastPointer: ListNode = list;
31  
32    // Find the middle node using the two-pointer technique
33    // Stop when fastPointer reaches the end or is about to complete the circle
34    while (fastPointer.next !== list && fastPointer.next.next !== list) {
35        slowPointer = slowPointer.next!;
36        fastPointer = fastPointer.next!.next!;
37    }
38  
39    // If the list has even number of nodes, move fastPointer one more step
40    if (fastPointer.next !== list) {
41        fastPointer = fastPointer.next!;
42    }
43  
44    // Split the circular list into two halves
45    // secondListHead will be the head of the second half
46    const secondListHead: ListNode = slowPointer.next!;
47  
48    // Make the second half circular by connecting its tail to its head
49    fastPointer.next = secondListHead;
50  
51    // Make the first half circular by connecting its tail to its head
52    slowPointer.next = list;
53  
54    // Return both circular linked lists
55    return [list, secondListHead];
56}
57

Time and Space Complexity

Time Complexity: O(n), where n is the length of the linked list.

The algorithm uses a two-pointer approach (slow and fast pointers) to find the middle of the circular linked list. The fast pointer b moves twice as fast as the slow pointer a. The while loop continues until the fast pointer reaches the end of the list (when b.next == list or b.next.next == list). In the worst case, the fast pointer traverses the entire list once, which takes O(n) time. After the loop, there are only constant-time operations to split the list and adjust the pointers.

Space Complexity: O(1)

The algorithm only uses a fixed amount of extra space regardless of the input size. It creates two pointer variables a and b to traverse the list, and one additional variable list2 to mark the head of the second half. No additional data structures like arrays, hash tables, or recursive calls are used, so the space complexity remains constant.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not Handling Edge Cases for Small Lists

The Problem: The code doesn't handle edge cases where the circular linked list has only 1 or 2 nodes. For a single-node circular list, the algorithm would fail because we're trying to split something that cannot be split into two separate lists. The while loop condition fast_ptr.next != list and fast_ptr.next.next != list would immediately be false for a 1-node list, and the subsequent operations would create invalid references.

Solution: Add explicit checks for these edge cases at the beginning of the function:

def splitCircularLinkedList(self, list: Optional[ListNode]) -> List[Optional[ListNode]]:
    # Handle edge case: single node
    if list.next == list:
        # Cannot split a single node into two lists
        # Could either return [list, None] or raise an exception
        return [list, None]
  
    # Handle edge case: two nodes
    if list.next.next == list:
        second_node = list.next
        list.next = list  # First list: single node pointing to itself
        second_node.next = second_node  # Second list: single node pointing to itself
        return [list, second_node]
  
    # Continue with the original algorithm for 3+ nodes...

Pitfall 2: Incorrect Pointer Movement for Even-Length Lists

The Problem: The condition if fast_ptr.next != list: is meant to adjust for odd-length lists, but the logic can be confusing. When the list has an even number of nodes, fast_ptr.next.next == list causes the loop to exit, and fast_ptr is already at the second-to-last node. The subsequent if statement moves it to the last node. However, this logic is not immediately clear and can lead to off-by-one errors if modified incorrectly.

Solution: Make the intent clearer by counting nodes explicitly or adding comments to clarify the pointer positions:

# After the while loop:
# - For even-length lists: fast_ptr is at second-to-last node
# - For odd-length lists: fast_ptr is at third-to-last node

# Move fast_ptr to the actual last node
if fast_ptr.next.next == list:  # Even number of nodes
    fast_ptr = fast_ptr.next

Pitfall 3: Assuming Non-Null Input

The Problem: The code assumes that the input list is never None. If a None value is passed, the code will throw an AttributeError when trying to access list.next.

Solution: Add a null check at the beginning:

def splitCircularLinkedList(self, list: Optional[ListNode]) -> List[Optional[ListNode]]:
    if not list:
        return [None, None]
  
    # Continue with the rest of the implementation...

Pitfall 4: Not Verifying Circular Structure

The Problem: The code assumes the input is actually circular. If a regular (non-circular) linked list is passed by mistake, the algorithm will enter an infinite loop because fast_ptr.next will never equal list.

Solution: Either document this assumption clearly or add a validation step using Floyd's cycle detection:

def splitCircularLinkedList(self, list: Optional[ListNode]) -> List[Optional[ListNode]]:
    # Verify the list is actually circular (optional safety check)
    if not self.isCircular(list):
        raise ValueError("Input list is not circular")
  
    # Continue with splitting logic...

def isCircular(self, head: Optional[ListNode]) -> bool:
    if not head:
        return False
  
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More