Skip to content

Singly Linked List

A singly linked list is a linear data structure where elements are stored in nodes, and each node contains data and a reference (or link) to the next node in the sequence. It provides dynamic memory allocation and efficient insertion/deletion at the beginning.

Features

  • Dynamic size - grows and shrinks during runtime
  • Memory efficient - allocates memory as needed
  • Fast insertion/deletion at the beginning (O(1))
  • Sequential access - elements accessed by traversing from head
  • Generic type support for type safety

Time Complexities

Operation Time Complexity
Prepend O(1)
Append O(n)
Insert O(n)
Remove First O(1)
Remove Last O(n)
Remove by Value O(n)
Remove by Index O(n)
Search O(n)
Access by Index O(n)

Basic Usage

Creating and Initializing

from algo.data_structs.lists.singly_linked_list import SinglyLinkedList

# Create empty list
ll = SinglyLinkedList()

# Check if empty
print(ll.is_empty())  # True
print(len(ll))        # 0
print(ll.size())      # 0

Adding Elements

# Prepend elements (add to beginning)
ll.prepend(3)
ll.prepend(2)
ll.prepend(1)
print(list(ll))  # [1, 2, 3]

# Append elements (add to end)
ll.append(4)
ll.append(5)
print(list(ll))  # [1, 2, 3, 4, 5]

# Extend with multiple elements
ll.extend([6, 7, 8])
print(list(ll))  # [1, 2, 3, 4, 5, 6, 7, 8]

# Insert at specific position
ll.insert(0, 0)     # Insert at beginning
ll.insert(4, 3.5)   # Insert in middle
ll.insert(len(ll), 9)  # Insert at end
print(list(ll))     # [0, 1, 2, 3, 3.5, 4, 5, 6, 7, 8, 9]

Accessing Elements

# Get head and tail
print(ll.get_head())  # 0 (first element)
print(ll.get_tail())  # 9 (last element)

# Access by index
print(ll.get(0))   # 0 (first element)
print(ll.get(5))   # 4 (sixth element)

# Modify elements
ll.set(1, 10)
print(ll.get(1))   # 10

# Get list length
print(len(ll))     # 11
print(ll.size())   # 11

Removing Elements

# Remove first element
first = ll.remove_first()
print(first)       # 0
print(list(ll))    # [10, 2, 3, 3.5, 4, 5, 6, 7, 8, 9]

# Remove last element
last = ll.remove_last()
print(last)        # 9
print(list(ll))    # [10, 2, 3, 3.5, 4, 5, 6, 7, 8]

# Remove by value (first occurrence)
result = ll.remove(3.5)
print(result)      # True
print(list(ll))    # [10, 2, 3, 4, 5, 6, 7, 8]

# Remove by index
removed = ll.remove_at(0)
print(removed)     # 10
print(list(ll))    # [2, 3, 4, 5, 6, 7, 8]

# Clear all elements
ll.clear()
print(len(ll))     # 0

Search Operations

ll = SinglyLinkedList()
ll.extend([1, 2, 3, 2, 4, 2])

# Find index of element (first occurrence)
print(ll.find(2))      # 1
print(ll.find(4))      # 4
print(ll.find(99))     # -1 (not found)

# Check if element exists
print(2 in ll)         # True
print(99 in ll)        # False

# Count occurrences
print(ll.count(2))     # 3
print(ll.count(1))     # 1
print(ll.count(99))    # 0 (non-existent)

List Manipulation

ll = SinglyLinkedList()
ll.extend([1, 2, 3, 4, 5])

# Reverse the list
ll.reverse()
print(list(ll))  # [5, 4, 3, 2, 1]

# Copy the list
ll_copy = ll.copy()
print(list(ll_copy))  # [5, 4, 3, 2, 1]

# Lists are independent
ll_copy.append(6)
print(len(ll))        # 5 (original unchanged)
print(len(ll_copy))   # 6 (copy modified)

# Convert to Python list
python_list = ll.to_list()
print(python_list)    # [5, 4, 3, 2, 1]
print(type(python_list))  # <class 'list'>

# Iteration
for item in ll:
    print(item)  # 5, 4, 3, 2, 1

Working with Different Types

Strings

str_ll = SinglyLinkedList()
str_ll.extend(['apple', 'banana', 'cherry'])

print(str_ll.find('banana'))   # 1
print(str_ll.count('apple'))   # 1
print('cherry' in str_ll)      # True

Mixed Types

mixed_ll = SinglyLinkedList()
mixed_ll.extend([1, 'hello', 3.14, True])

print(len(mixed_ll))    # 4
print(mixed_ll.get(1))  # 'hello'
print(mixed_ll.get(3))  # True

Custom Objects

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return isinstance(other, Person) and self.name == other.name

people = SinglyLinkedList()
alice = Person("Alice", 30)
bob = Person("Bob", 25)

people.extend([alice, bob])
print(people.count(alice))    # 1
print(people.find(bob))       # 1
print(alice in people)        # True

Performance Characteristics

Insertion Performance

  • Prepend: O(1) - Always fast as it only modifies the head
  • Append: O(n) - Must traverse to the end
  • Insert at arbitrary position: O(n) - Must traverse to position
ll = SinglyLinkedList()

# Fast prepend operations
for i in range(1000):
    ll.prepend(i)  # O(1) each

# Slower append operations  
for i in range(100):
    ll.append(i)   # O(n) each

Memory Efficiency

Singly linked lists use memory efficiently by allocating nodes only as needed:

# Memory grows with elements
ll = SinglyLinkedList()
for i in range(10):
    ll.append(i)  # Each append allocates one new node

# Memory freed when elements removed
ll.clear()  # All nodes can be garbage collected

Error Handling

ll = SinglyLinkedList()

# Index errors for empty list
try:
    ll.get(0)  # IndexError: list index out of range
except IndexError:
    print("Cannot access empty list")

try:
    ll.remove_first()  # IndexError: remove from empty list
except IndexError:
    print("Cannot remove from empty list")

# Index errors for invalid indices
ll.extend([1, 2, 3])
try:
    ll.get(5)  # IndexError: list index out of range
except IndexError:
    print("Index out of range")

try:
    ll.insert(-1, 5)  # IndexError: negative index not allowed
except IndexError:
    print("Invalid insert index")

# Value error for non-existent elements
try:
    ll.remove(99)  # Returns False (doesn't raise error)
    print("Element not found")
except:
    pass

String Representation

ll = SinglyLinkedList()
ll.extend([1, 2, 3])

print(str(ll))   # SinglyLinkedList([1 -> 2 -> 3 -> None])
print(repr(ll))  # SinglyLinkedList(size=3, head=ListNode(1))

# Empty list representation
empty_ll = SinglyLinkedList()
print(str(empty_ll))  # SinglyLinkedList([])

Common Patterns

Building from Iterables

# From list
ll = SinglyLinkedList()
ll.extend([1, 2, 3, 4, 5])

# From tuple
ll2 = SinglyLinkedList()
ll2.extend((1, 2, 3))

# From range
ll3 = SinglyLinkedList()
ll3.extend(range(10))

# From another linked list
ll4 = SinglyLinkedList()
ll4.extend(ll)  # Copy elements from ll

Stack-like Usage (LIFO)

stack = SinglyLinkedList()

# Push elements (prepend for O(1) performance)
stack.prepend(1)
stack.prepend(2)
stack.prepend(3)
print(list(stack))  # [3, 2, 1]

# Pop elements
while not stack.is_empty():
    item = stack.remove_first()
    print(item)  # 3, 2, 1

Queue-like Usage (FIFO) - Less Efficient

# Note: This is inefficient for queues due to O(n) remove_last
queue = SinglyLinkedList()

# Enqueue (append to end)
queue.append(1)
queue.append(2)
queue.append(3)

# Dequeue (remove from front)
while not queue.is_empty():
    item = queue.remove_first()
    print(item)  # 1, 2, 3

Filtering Elements

ll = SinglyLinkedList()
ll.extend(range(10))

# Remove all even numbers
i = 0
while i < len(ll):
    if ll.get(i) % 2 == 0:
        ll.remove_at(i)
    else:
        i += 1

print(list(ll))  # [1, 3, 5, 7, 9]

Merging Lists

ll1 = SinglyLinkedList()
ll1.extend([1, 3, 5])

ll2 = SinglyLinkedList()
ll2.extend([2, 4, 6])

# Merge ll2 into ll1
ll1.extend(ll2)
print(list(ll1))  # [1, 3, 5, 2, 4, 6]

Finding and Replacing

ll = SinglyLinkedList()
ll.extend([1, 2, 3, 2, 4])

# Replace all occurrences of 2 with 20
for i in range(len(ll)):
    if ll.get(i) == 2:
        ll.set(i, 20)

print(list(ll))  # [1, 20, 3, 20, 4]

Use Cases

When to Use Singly Linked Lists

Good for:

  • Frequent insertion/deletion at the beginning
  • Unknown or highly variable data size
  • Memory-constrained environments
  • Implementing other data structures (stacks, queues)
  • Sequential access patterns

Examples:

# Undo/Redo functionality (stack-like)
undo_stack = SinglyLinkedList()
undo_stack.prepend("action1")
undo_stack.prepend("action2")
last_action = undo_stack.remove_first()

# Browser history (recent first)
history = SinglyLinkedList()
history.prepend("google.com")
history.prepend("github.com")

# Dynamic playlist
playlist = SinglyLinkedList()
playlist.append("song1.mp3")
playlist.append("song2.mp3")
playlist.insert(1, "favorite_song.mp3")

When NOT to Use Singly Linked Lists

Avoid for:

  • Frequent random access by index
  • Frequent insertion/deletion at the end
  • Memory access patterns requiring cache efficiency
  • When you need bidirectional traversal

Better alternatives:

# For random access, use dynamic array instead
from algo.data_structs.arrays.dynamic_array import DynamicArray
arr = DynamicArray()  # Better for arr[index] operations

# For queue operations, use deque instead
from collections import deque
queue = deque()  # Better for queue operations

Node Structure

The underlying ListNode class provides the building blocks:

from algo.data_structs.lists.singly_linked_list import ListNode

# Create individual nodes
node1 = ListNode(1)
node2 = ListNode(2, node1)  # Points to node1

print(node1.data)    # 1
print(node1.next)    # None
print(node2.data)    # 2
print(node2.next)    # node1

# String representation
print(str(node1))    # "1"
print(repr(node1))   # "ListNode(1)"

Performance Comparison

Operation Singly Linked List Dynamic Array Python List
Prepend O(1) O(n) O(n)
Append O(n) O(1) amortized O(1) amortized
Insert Middle O(n) O(n) O(n)
Remove First O(1) O(n) O(n)
Remove Last O(n) O(1) O(1)
Access by Index O(n) O(1) O(1)
Search O(n) O(n) O(n)
Memory Overhead Low Medium Low

API Reference

Singly Linked List Implementation

A singly linked list is a linear data structure where each element (node) contains data and a reference (link) to the next node in the sequence. Unlike arrays, linked lists don't store elements in contiguous memory locations.

Key Features:

  • Dynamic size allocation
  • Efficient insertion/deletion at the beginning
  • Sequential access to elements
  • No memory waste (allocates exactly what's needed)

Time Complexities:

  • Access: O(n) - must traverse from head
  • Search: O(n) - linear search required
  • Insertion: O(1) at head, O(n) at arbitrary position
  • Deletion: O(1) at head, O(n) at arbitrary position
  • Append: O(n) - must traverse to end

Space Complexity: O(n)

ListNode(data, next_node=None)

Bases: Generic[T]

A node in a singly linked list.

Each node contains data and a reference to the next node.

Initialize a new list node.

Parameters:

Name Type Description Default
data T

The data to store in this node

required
next_node Optional[ListNode[T]]

Reference to the next node (default: None)

None

__repr__()

Return detailed string representation for debugging.

__str__()

Return string representation of the node.

SinglyLinkedList()

Bases: Generic[T]

A singly linked list implementation with comprehensive operations.

This implementation maintains a reference to the head node and tracks the size for efficient length operations.

Initialize an empty singly linked list.

__contains__(value)

Check if a value exists in the list.

__iter__()

Return an iterator over the list elements.

__len__()

Return the number of elements in the list.

__repr__()

Return detailed string representation for debugging.

__str__()

Return string representation of the list.

append(data)

Add an element to the end of the list.

Time Complexity: O(n) - must traverse to the end

Parameters:

Name Type Description Default
data T

Element to add

required

clear()

Remove all elements from the list.

Time Complexity: O(1)

copy()

Create a shallow copy of the list.

Time Complexity: O(n)

Returns:

Type Description
SinglyLinkedList[T]

A new SinglyLinkedList with the same elements

count(data)

Count the number of occurrences of the specified value.

Time Complexity: O(n)

Parameters:

Name Type Description Default
data T

Value to count

required

Returns:

Type Description
int

Number of occurrences

extend(iterable)

Extend the list by appending all elements from the iterable.

Time Complexity: O(k * n) where k is the number of elements to add and n is the current size (due to append being O(n))

Parameters:

Name Type Description Default
iterable

Iterable containing elements to add

required

find(data)

Find the index of the first occurrence of the specified value.

Time Complexity: O(n)

Parameters:

Name Type Description Default
data T

Value to search for

required

Returns:

Type Description
int

Index of the first occurrence, or -1 if not found

get(index)

Get element at the specified index.

Time Complexity: O(n)

Parameters:

Name Type Description Default
index int

Index of element to retrieve

required

Returns:

Type Description
T

Element at the specified index

Raises:

Type Description
IndexError

If index is out of bounds

get_head()

Get the first element without removing it.

Time Complexity: O(1)

Returns:

Type Description
Optional[T]

The first element, or None if list is empty

get_tail()

Get the last element without removing it.

Time Complexity: O(n)

Returns:

Type Description
Optional[T]

The last element, or None if list is empty

insert(index, data)

Insert an element at the specified index.

Time Complexity: O(n) - may need to traverse to the position

Parameters:

Name Type Description Default
index int

Index where to insert (0-based)

required
data T

Element to insert

required

Raises:

Type Description
IndexError

If index is out of valid range

is_empty()

Check if the list is empty.

Returns:

Type Description
bool

True if the list is empty, False otherwise

prepend(data)

Add an element to the beginning of the list.

Time Complexity: O(1)

Parameters:

Name Type Description Default
data T

Element to add

required

remove(data)

Remove the first occurrence of the specified value.

Time Complexity: O(n)

Parameters:

Name Type Description Default
data T

Value to remove

required

Returns:

Type Description
bool

True if element was found and removed, False otherwise

remove_at(index)

Remove and return element at the specified index.

Time Complexity: O(n)

Parameters:

Name Type Description Default
index int

Index of element to remove

required

Returns:

Type Description
T

The removed element

Raises:

Type Description
IndexError

If index is out of bounds

remove_first()

Remove and return the first element.

Time Complexity: O(1)

Returns:

Type Description
T

The removed element

Raises:

Type Description
IndexError

If the list is empty

remove_last()

Remove and return the last element.

Time Complexity: O(n) - must traverse to find the second-to-last node

Returns:

Type Description
T

The removed element

Raises:

Type Description
IndexError

If the list is empty

reverse()

Reverse the list in place.

Time Complexity: O(n) Space Complexity: O(1)

set(index, data)

Set element at the specified index.

Time Complexity: O(n)

Parameters:

Name Type Description Default
index int

Index where to set the value

required
data T

New value to set

required

Raises:

Type Description
IndexError

If index is out of bounds

size()

Get the number of elements in the list.

Returns:

Type Description
int

The number of elements

to_list()

Convert the linked list to a Python list.

Time Complexity: O(n)

Returns:

Type Description
list[T]

A Python list containing all elements