Linked Lists
Why Linked Lists Exist
Dynamic sizing without pre-allocating memory, efficient insertion/deletion anywhere. They solve the fixed-size problem of static arrays while allowing insertion/deletion at arbitrary positions.
When to Use Linked Lists
Use when:
- Frequent insertions/deletions at arbitrary positions
- Unknown final size and memory efficiency matters
- Implementing other data structures (stacks, queues)
- Memory is fragmented and you can’t allocate large contiguous blocks
Don’t use when:
- Need random access by index
- Cache performance is critical (arrays have better locality)
- Memory overhead is a concern (extra pointers per node)
- Working with small, fixed-size datasets
Modern reality: Most languages have dynamic arrays (C# List<T>
) that are usually better. Linked lists are primarily educational or for very specific use cases.
Time Complexity
Operation | Singly Linked | Doubly Linked | Array |
---|---|---|---|
Access by index | O(n) | O(n) | O(1) |
Search | O(n) | O(n) | O(n) |
Insert at beginning | O(1) | O(1) | O(n) |
Insert at end | O(n) or O(1)* | O(1) | O(1) amortized |
Insert at position | O(n) | O(n) | O(n) |
Delete at beginning | O(1) | O(1) | O(n) |
Delete at end | O(n) | O(1) | O(1) |
*O(1) if you maintain a tail pointer
Node Implementation
Node Implementation
public class Node<T>
{
public T Value { get; set; }
public Node<T> LinkNode { get; set; }
public Node(T value, Node<T> linkNode = null)
{
Value = value;
LinkNode = linkNode;
}
public void SetLinkNode(Node<T> linkNode) => LinkNode = linkNode;
public Node<T> GetLinkNode() => LinkNode;
public T GetValue() => Value;
}
Singly Linked List
C# Implementation
public class LinkedList<T> where T : IEquatable<T>
{
private Node<T> headNode;
public LinkedList(T value = default(T))
{
if (!EqualityComparer<T>.Default.Equals(value, default(T)))
{
headNode = new Node<T>(value);
}
}
public Node<T> GetHeadNode() => headNode;
public void InsertBeginning(T newValue)
{
var newNode = new Node<T>(newValue);
newNode.SetLinkNode(headNode);
headNode = newNode;
}
public string StringifyList()
{
var result = new StringBuilder();
var currentNode = headNode;
while (currentNode != null)
{
result.Append($"{currentNode.GetValue()} -> ");
currentNode = currentNode.GetLinkNode();
}
return result.Append("null").ToString();
}
public bool RemoveNode(T valueToRemove)
{
if (headNode == null) return false;
// Removing head node
if (headNode.GetValue().Equals(valueToRemove))
{
headNode = headNode.GetLinkNode();
return true;
}
// Traverse to find node to remove
var currentNode = headNode;
while (currentNode.GetLinkNode() != null)
{
if (currentNode.GetLinkNode().GetValue().Equals(valueToRemove))
{
currentNode.SetLinkNode(currentNode.GetLinkNode().GetLinkNode());
return true;
}
currentNode = currentNode.GetLinkNode();
}
return false; // Value not found
}
public Node<T> FindNode(T value)
{
var currentNode = headNode;
while (currentNode != null)
{
if (currentNode.GetValue().Equals(value))
return currentNode;
currentNode = currentNode.GetLinkNode();
}
return null;
}
}
// Example usage
var ll = new LinkedList<string>();
ll.InsertBeginning("C");
ll.InsertBeginning("B");
ll.InsertBeginning("A");
Console.WriteLine(ll.StringifyList()); // A -> B -> C -> null
ll.RemoveNode("B");
Console.WriteLine(ll.StringifyList()); // A -> C -> null
Doubly Linked List
When to use doubly linked:
- Need bidirectional traversal
- Implementing undo/redo functionality
- Browser history navigation
- Music playlist with previous/next
Don’t use when:
- Memory is constrained (50% more pointers)
- Only need forward traversal
- Working with simple, small datasets
C# Implementation
public class DoublyLinkedNode<T>
{
public T Value { get; set; }
public DoublyLinkedNode<T> NextNode { get; set; }
public DoublyLinkedNode<T> PrevNode { get; set; }
public DoublyLinkedNode(T value, DoublyLinkedNode<T> nextNode = null, DoublyLinkedNode<T> prevNode = null)
{
Value = value;
NextNode = nextNode;
PrevNode = prevNode;
}
}
public class DoublyLinkedList<T> where T : IEquatable<T>
{
private DoublyLinkedNode<T> headNode;
private DoublyLinkedNode<T> tailNode;
public void AddToHead(T newValue)
{
var newHead = new DoublyLinkedNode<T>(newValue);
var currentHead = headNode;
if (currentHead != null)
{
currentHead.PrevNode = newHead;
newHead.NextNode = currentHead;
}
headNode = newHead;
if (tailNode == null)
{
tailNode = newHead;
}
}
public void AddToTail(T newValue)
{
var newTail = new DoublyLinkedNode<T>(newValue);
var currentTail = tailNode;
if (currentTail != null)
{
currentTail.NextNode = newTail;
newTail.PrevNode = currentTail;
}
tailNode = newTail;
if (headNode == null)
{
headNode = newTail;
}
}
public T RemoveHead()
{
var removedHead = headNode;
if (removedHead == null)
{
return default(T);
}
headNode = removedHead.NextNode;
if (headNode != null)
{
headNode.PrevNode = null;
}
else
{
tailNode = null; // List is now empty
}
return removedHead.Value;
}
public T RemoveTail()
{
var removedTail = tailNode;
if (removedTail == null)
{
return default(T);
}
tailNode = removedTail.PrevNode;
if (tailNode != null)
{
tailNode.NextNode = null;
}
else
{
headNode = null; // List is now empty
}
return removedTail.Value;
}
public T RemoveByValue(T valueToRemove)
{
if (headNode == null)
return default(T);
// Check if it's the head or tail first
if (headNode.Value.Equals(valueToRemove))
return RemoveHead();
if (tailNode.Value.Equals(valueToRemove))
return RemoveTail();
// Search in the middle
var currentNode = headNode.NextNode;
while (currentNode != null)
{
if (currentNode.Value.Equals(valueToRemove))
{
// Update surrounding nodes
var prevNode = currentNode.PrevNode;
var nextNode = currentNode.NextNode;
prevNode.NextNode = nextNode;
nextNode.PrevNode = prevNode;
return currentNode.Value;
}
currentNode = currentNode.NextNode;
}
return default(T); // Value not found
}
public string StringifyList()
{
var result = new StringBuilder();
var currentNode = headNode;
while (currentNode != null)
{
result.Append($"{currentNode.Value} <-> ");
currentNode = currentNode.NextNode;
}
return result.Append("null").ToString();
}
}
Common Interview Problems
1. Reverse a Linked List
public static class LinkedListProblems
{
public static Node<T> ReverseLinkedList<T>(Node<T> head) where T : IEquatable<T>
{
Node<T> prev = null;
Node<T> current = head;
while (current != null)
{
Node<T> nextNode = current.GetLinkNode();
current.SetLinkNode(prev);
prev = current;
current = nextNode;
}
return prev; // prev is the new head
}
}
2. Detect Cycle (Floyd’s Cycle Detection)
public static bool HasCycle<T>(Node<T> head) where T : IEquatable<T>
{
if (head == null) return false;
Node<T> slow = head;
Node<T> fast = head;
while (fast != null && fast.GetLinkNode() != null)
{
slow = slow.GetLinkNode();
fast = fast.GetLinkNode().GetLinkNode();
if (slow == fast)
return true;
}
return false;
}
3. Find Middle Node
public static Node<T> FindMiddle<T>(Node<T> head) where T : IEquatable<T>
{
if (head == null) return null;
Node<T> slow = head;
Node<T> fast = head;
// Move fast pointer 2 steps and slow pointer 1 step
while (fast != null && fast.GetLinkNode() != null)
{
slow = slow.GetLinkNode();
fast = fast.GetLinkNode().GetLinkNode();
}
return slow; // slow is at the middle
}
Modern Usage Notes
C#: Use LinkedList<T>
from System.Collections.Generic or List<T>
Interview focus: Understand pointer manipulation, edge cases (empty list, single node), and common algorithms like cycle detection and reversal.
Production reality: Arrays/dynamic arrays usually win due to cache locality and simpler memory management. Linked lists are primarily educational or for very specific use cases where insertion/deletion patterns heavily favor them.
Found this guide helpful? Share it with your team:
Share on LinkedIn