-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
241: Whiteboard Practice: Detecting a Cycle in a Linked List in C#
You've probably seen this question in a dozen interview prep lists. It's a classic for a reason: it tests whether you can think about pointers (or references, in our C# world) as moving parts rather than static data. But before we dive into the "correct" way to do this, we need to clear up a mental hurdle that trips up a lot of developers when they first see this on a whiteboard.
Thinking cycles always loop back to the head
A common mistake I see is the assumption that if a linked list has a cycle, that cycle must point back to the first node in the list. It's a natural assumption—you think of a circle. If you assume this, your code ends up looking like a simple loop that checks if (current == head).
Here is why that fails. Imagine a linked list that looks like the number "6". You start at the head, go through a few nodes, and then enter a loop that circles back to a node in the middle of the list. In this scenario, you will never hit the head node again, but you'll be trapped in that loop forever. Your program will hang, your CPU will spike, and your interviewer will stop you before you crash the environment.
// This is a "6-shaped" list, NOT a circle.
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next; // Points back to node 2, not node 1!
The Tortoise and the Hare approach
Now, you could solve this by throwing every node you visit into a HashSet<ListNode>. If you encounter a node that's already in the set, you've found your cycle. It works, and it's intuitive. But in a whiteboard setting, your interviewer is almost certainly going to ask: "Can you do this without using extra memory?"
This is where Floyd’s Cycle-Finding Algorithm comes in. I like to call it the "Tortoise and the Hare." Instead of tracking every node we've seen, we use two pointers moving at different speeds. One pointer (the tortoise) moves one step at a time. The other (the hare) moves two steps at a time.
Think about it like two runners on a track. If the track is a straight line, the fast runner just hits the finish line and the race is over. But if the track is a loop, the fast runner will eventually lap the slow runner. The moment the hare pointer equals the tortoise pointer, you have mathematical proof that a cycle exists.
Here is how we implement that in C#:
public class ListNode
{
public int Value;
public ListNode Next;
public ListNode(int val) => Value = val;
}
public class CycleDetector
{
public bool HasCycle(ListNode head)
{
if (head == null) return false;
ListNode slow = head;
ListNode fast = head;
// We check fast and fast.Next because the hare jumps by two.
// If either is null, we've hit the end of the list (no cycle).
while (fast != null && fast.Next != null)
{
slow = slow.Next; // Move 1 step
fast = fast.Next.Next; // Move 2 steps
if (slow == fast)
{
return true; // The hare lapped the tortoise!
}
}
return false; // We hit the end of the list
}
}
The beauty of this is the space complexity. We aren't creating a list or a set; we're just using two references regardless of whether the list has ten nodes or ten million. That's O(1) space. The time complexity is O(n) because in the worst case, the hare has to travel the length of the list plus the length of the loop once before catching the tortoise.
📋 Practical Task
Implement a Cycle-Detection Utility for a Custom Playlist Linked List
Imagine you are building a music player where songs are stored in a linked list. Some playlists are set to "Repeat All," which creates a cycle by pointing the last song back to some previous song in the list.
Create a class PlaylistManager with a method bool IsLooping(SongNode head). You must implement this using the two-pointer technique to ensure it uses constant space.
Test your implementation with these two scenarios:
- A linear playlist: Song A → Song B → Song C → null (Should return
false). - A looping playlist: Song A → Song B → Song C → Song B (Should return
true).
Starter Code:
public class SongNode
{
public string Title;
public SongNode Next;
public SongNode(string title) => Title = title;
}
public class PlaylistManager
{
public bool IsLooping(SongNode head)
{
// Your implementation here
}
}
There are no comments for now.