Skip to main content

Command Palette

Search for a command to run...

๐Ÿ”— Linked Lists in the Age of AI

Because Not Every Data Structure Needs to Stand in a Straight Line

Updated
โ€ข29 min readโ€ขView as Markdown
๐Ÿ”— Linked Lists in the Age of AI
L

Hey there! ๐Ÿ‘‹ I'm Hardeep Jethwani (HJ), your resident cloud aficionado and code maestro, proudly navigating the ever-changing seas of AWS Cloud and Full Stack Development for ~5 glorious years and counting. โ˜๏ธ๐Ÿ’ป

Currently, I'm orchestrating the tech symphony as part of Team HSBC Bank, where I'm on a mission to enhance the banking experience through the magic of technology. ๐Ÿš€๐Ÿ’ผ

In my past life at Capgemini, I led exciting adventures like migrating critical applications to the cloud (18 and counting!). I had databases waltzing into the AWS Cloud, sprinkling a bit of containerization magic along the way. AWS managed services like RDS, Lambda, ECS, and friends? They were my trusty sidekicks. ๐ŸŽฉ๐Ÿ”ง

When not automating deployments with CI/CD finesse (think AWS CodePipeline, CodeBuild, and CodeDeploy), you might find me designing infrastructure like a digital architect using AWS CloudFormation. Security is my jam โ€“ I've got WAF, Security Groups, MFA, Cognito, and even a secret club in private subnets to keep things safe. ๐Ÿ”’๐Ÿ’‚โ€โ™‚๏ธ

On top of all that, I'm on a mission to reduce carbon footprints because, why not? HSBC's commitment to sustainability is my heart and soul. We're going for NET ZERO carbon footprint, and I'm leading the charge, one container at a time! ๐ŸŒ๐ŸŒฑ

And yes, the fun doesn't stop at work. In my past life at Tata Consultancy Services, I co-created a multi-tier Point of Sale application with a global footprint, touching the lives of billions. My automation tools were so efficient that even Father Time was left scratching his head. โณ๐Ÿ’ก

If you're in need of a cloud-savvy comedian or a code deployment magician, look no further. Let's chat about tech, swap automation tales, or share some coding humor over a virtual coffee. Oh, and don't worry; I promise not to write code in my sleep (well, most of the time). Cheers to cloud adventures! โ˜•๐Ÿš€

A beginner-friendly and humorous guide to linked lists, nodes, references, operations, time complexity, cycle detection, reversal, and practical AI use cases with Python.


๐Ÿ˜‚ Everyone Knows Only the Next Person

Imagine a queue outside a famous food stall.

The first person knows who is standing behind them. That person knows the next person, and so on.

But if you ask the first person:

โ€œWho is standing at position 927?โ€

They cannot instantly point to that person. They must ask:

โ€œWho is next?โ€

Then that person asks the same question, until the stall has closed and your program has finally reached node number 927.

Congratulations. You have understood a linked list. ๐Ÿ˜„

A linked list is a collection of elements called nodes. Each node normally stores:

  1. Some data

  2. A reference to another node

The first node is the head. The final node usually points to None, which is Pythonโ€™s way of saying:

โ€œBoss, there is nobody after me.โ€


๐Ÿค– Why Linked Lists Still Matter in AI

Modern AI is dominated by tensors, arrays, GPUs, embeddings, and matrices. Those structures benefit from compact, contiguous memory.

So are linked lists useless in AI?

Not at all.

Linked structures help us understand or build:

  • Dynamic queues

  • Agent execution traces

  • Undo and redo histories

  • Streaming pipelines

  • LRU caches

  • Graph adjacency lists

  • Sparse and irregular relationships

  • Workflow chains

  • Memory allocators

  • Event-processing systems

They also teach important ideas:

  • References

  • Object identity

  • Dynamic memory

  • Two-pointer algorithms

  • Time complexity

  • Data-structure trade-offs

However, remember:

Linked lists are not automatically faster just because insertion can be O(1).

For dense numerical AI operations, Python lists, NumPy arrays, and tensors are generally better because they support fast indexing, compact storage, and hardware-friendly access.

Linked lists are most useful when relationships and dynamic updates matter more than random access.


๐Ÿงฑ What Is a Node?

A node stores a value and a link to the next node.

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

Create two nodes:

first = Node("AI")
second = Node("Python")

first.next = second

Now:

first.next does not copy the second node. It stores a reference to it.

That connects directly to memory management: nodes are separate objects joined through references.


๐Ÿ“ฆ Building a Basic Singly Linked List

A singly linked list supports movement in one direction.

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None


class SinglyLinkedList:
    def __init__(self):
        self.head = None

Initially:

Core terms:

  • Head: first node

  • Tail: final node

  • Traversal: visiting nodes one by one

  • Insertion: adding a node

  • Deletion: removing a node

  • Search: finding a value

  • Cycle: a path that eventually returns to a previous node

A cycle is the linked-list version of:

โ€œWe are going in circles.โ€

Except the computer means it literally.


๐Ÿšถ Traversal

To display values, start at the head and follow next.

class SinglyLinkedList:
    def __init__(self):
        self.head = None

    def display(self):
        current = self.head

        while current is not None:
            print(current.value, end=" -> ")
            current = current.next

        print("None")

Example:

first = Node("AI")
second = Node("Python")
third = Node("RAG")

first.next = second
second.next = third

linked_list = SinglyLinkedList()
linked_list.head = first

linked_list.display()

Output:

AI -> Python -> RAG -> None

Complexity

Time:

O(n)

Extra space:

O(1)

Unlike arrays, linked lists do not provide direct indexed access. To reach node 500, we normally walk through nodes 0 to 499 first.


โšก Insert at the Beginning

class SinglyLinkedList:
    def __init__(self):
        self.head = None

    def prepend(self, value):
        new_node = Node(value)
        new_node.next = self.head
        self.head = new_node

Before:

After prepending "DSA":

Time:

O(1)

Only two references change.

This remains constant whether the list contains five nodes or five million.


๐Ÿ Insert at the End

Without a tail reference:

def append(self, value):
    new_node = Node(value)

    if self.head is None:
        self.head = new_node
        return

    current = self.head

    while current.next is not None:
        current = current.next

    current.next = new_node

Time:

O(n)

We must find the final node.

Improve it with a tail

class SinglyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        new_node = Node(value)

        if self.head is None:
            self.head = new_node
            self.tail = new_node
            return

        self.tail.next = new_node
        self.tail = new_node

Now append is:

O(1)

Maintaining one extra reference saves repeated traversal.


๐ŸŽฏ Insert After a Value

def insert_after(self, target, value):
    current = self.head

    while current is not None:
        if current.value == target:
            new_node = Node(value)
            new_node.next = current.next
            current.next = new_node

            if self.tail is current:
                self.tail = new_node

            return True

        current = current.next

    return False

Searching for the target:

O(n)

The actual pointer update after finding it:

O(1)

Overall:

O(n)

So when somebody says:

โ€œLinked-list insertion is always O(1),โ€

ask:

โ€œDo we already have the node, or are we first searching the whole list?โ€

The insertion is fast. Finding the location may not be.


๐Ÿ“ Insert at a Position

def insert_at(self, index, value):
    if index < 0:
        raise IndexError("Index cannot be negative")

    if index == 0:
        self.prepend(value)
        return

    current = self.head
    current_index = 0

    while (
        current is not None
        and current_index < index - 1
    ):
        current = current.next
        current_index += 1

    if current is None:
        raise IndexError("Index out of range")

    new_node = Node(value)
    new_node.next = current.next
    current.next = new_node

    if new_node.next is None:
        self.tail = new_node

Time:

O(n)

The list must be traversed to the requested position.


๐Ÿ”Ž Search for a Value

def contains(self, target):
    current = self.head

    while current is not None:
        if current.value == target:
            return True

        current = current.next

    return False

Best case:

O(1)

Worst and average case:

O(n)

Every node knows only the next node. Nobody has the complete attendance sheet.


๐ŸŽฒ Access by Index

def get(self, index):
    if index < 0:
        raise IndexError("Index cannot be negative")

    current = self.head
    current_index = 0

    while current is not None:
        if current_index == index:
            return current.value

        current = current.next
        current_index += 1

    raise IndexError("Index out of range")

Time:

O(n)

Array access is normally O(1). Linked-list indexed access is O(n).

This is one reason arrays dominate tensor and vector workloads. A GPU wants a predictable block of numbers, not a treasure hunt through Python objects.


๐Ÿ—‘๏ธ Delete the Head

def delete_first(self):
    if self.head is None:
        return None

    removed_value = self.head.value
    self.head = self.head.next

    if self.head is None:
        self.tail = None

    return removed_value

Time:

O(1)

We simply move the head reference.


๐Ÿ—‘๏ธ Delete the Tail

In a singly linked list, the tail does not know the previous node.

def delete_last(self):
    if self.head is None:
        return None

    if self.head.next is None:
        removed_value = self.head.value
        self.head = None
        self.tail = None
        return removed_value

    current = self.head

    while current.next.next is not None:
        current = current.next

    removed_value = current.next.value
    current.next = None
    self.tail = current

    return removed_value

Time:

O(n)

Even with a tail reference, we must find the second-last node.

This is where a doubly linked list becomes useful.


โœ‚๏ธ Delete a Specific Value

def delete_value(self, target):
    if self.head is None:
        return False

    if self.head.value == target:
        self.delete_first()
        return True

    current = self.head

    while current.next is not None:
        if current.next.value == target:
            node_to_remove = current.next
            current.next = node_to_remove.next

            if node_to_remove is self.tail:
                self.tail = current

            return True

        current = current.next

    return False

Time:

O(n)

The pointer update is constant time, but the search may visit the whole list.


๐Ÿ“ Length of the List

Traverse every time

def length(self):
    count = 0
    current = self.head

    while current is not None:
        count += 1
        current = current.next

    return count

Time:

O(n)

Maintain a size attribute

class SinglyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

    def __len__(self):
        return self.size

Now length is:

O(1)

But every successful insertion and deletion must update size.

Forget one decrement and your linked list begins lying about its age.


๐Ÿ”„ Reverse a Linked List

Original:

Reversed:

Use three references:

def reverse(self):
    previous = None
    current = self.head

    self.tail = self.head

    while current is not None:
        next_node = current.next
        current.next = previous
        previous = current
        current = next_node

    self.head = previous

We must save next_node before changing current.next.

Otherwise we lose the rest of the list.

That is less โ€œreverse the listโ€ and more โ€œaccidentally delete the project roadmap.โ€

Time:

O(n)

Extra space:

O(1)

Recursive reversal

def reverse_recursive(self):
    def reverse_node(node):
        if node is None or node.next is None:
            return node

        new_head = reverse_node(node.next)

        node.next.next = node
        node.next = None

        return new_head

    self.tail = self.head
    self.head = reverse_node(self.head)

Time:

O(n)

Auxiliary space:

O(n)

because recursion creates call frames.

The iterative version is usually more memory-efficient.


๐ŸŽฏ Find the Middle Node

Use slow and fast pointers:

def find_middle(self):
    slow = self.head
    fast = self.head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next

    return None if slow is None else slow.value

The slow pointer moves one step.

The fast pointer moves two.

When fast reaches the end, slow is near the middle.

Time:

O(n)

Space:

O(1)

This two-pointer pattern is useful for:

  • Finding the middle

  • Cycle detection

  • Finding the kth node from the end

  • Palindrome checks

The fast pointer is basically an intern who runs ahead and gathers information.


๐Ÿ”™ Find the Kth Node from the End

def kth_from_end(self, k):
    if k <= 0:
        raise ValueError("k must be positive")

    first = self.head
    second = self.head

    for _ in range(k):
        if first is None:
            raise IndexError("k is larger than the list")

        first = first.next

    while first is not None:
        first = first.next
        second = second.next

    return second.value

Time:

O(n)

Space:

O(1)

The pointers maintain a gap of k nodes.


โ™ป๏ธ Detect a Cycle

A cyclic list may look like:

A normal traversal would never end.

Floydโ€™s cycle detection

def has_cycle(self):
    slow = self.head
    fast = self.head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next

        if slow is fast:
            return True

    return False

Time:

O(n)

Space:

O(1)

This is also called the tortoise-and-hare algorithm.

The hare runs faster, but the tortoise wins by having a better contract.


๐Ÿ“ Find the Start of a Cycle

def cycle_start(self):
    slow = self.head
    fast = self.head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next

        if slow is fast:
            break
    else:
        return None

    slow = self.head

    while slow is not fast:
        slow = slow.next
        fast = fast.next

    return slow

After the first meeting, move one pointer to the head. Move both one step at a time. Their next meeting is the cycle start.

Time:

O(n)

Space:

O(1)

๐Ÿ”— Merge Two Sorted Linked Lists

def merge_sorted_lists(first, second):
    dummy = Node(0)
    current = dummy

    while first is not None and second is not None:
        if first.value <= second.value:
            current.next = first
            first = first.next
        else:
            current.next = second
            second = second.next

        current = current.next

    current.next = first if first is not None else second

    return dummy.next

For lists of sizes n and m:

Time:

O(n + m)

Extra space:

O(1)

when existing nodes are reused.

The dummy node handles edge cases and receives no credit in the final output. Classic office behaviour.


๐Ÿงฉ Find the Intersection of Two Lists

An intersection means the lists share the same node object:

def find_intersection(head_a, head_b):
    pointer_a = head_a
    pointer_b = head_b

    while pointer_a is not pointer_b:
        pointer_a = (
            head_b
            if pointer_a is None
            else pointer_a.next
        )

        pointer_b = (
            head_a
            if pointer_b is None
            else pointer_b.next
        )

    return pointer_a

Each pointer traverses both lists, equalizing the distance.

Time:

O(n + m)

Space:

O(1)

Use identity (is), not only value equality. Two nodes may contain "AI" without being the same node.


๐Ÿงฎ Remove Duplicates

Use a set for an unsorted list:

def remove_duplicates(self):
    seen = set()
    current = self.head
    previous = None

    while current is not None:
        if current.value in seen:
            previous.next = current.next

            if current is self.tail:
                self.tail = previous
        else:
            seen.add(current.value)
            previous = current

        current = current.next

Average time:

O(n)

Extra space:

O(n)

Without extra memory, compare each node with all later nodes:

Time:

O(nยฒ)

Space:

O(1)

Trade memory for speedโ€”the same negotiation behind caches, indexes, embeddings, and KV cache.


๐Ÿ” Palindrome Check

Example:

Approach:

  1. Find the middle

  2. Reverse the second half

  3. Compare both halves

def is_palindrome(self):
    if self.head is None or self.head.next is None:
        return True

    slow = self.head
    fast = self.head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next

    previous = None
    current = slow

    while current is not None:
        next_node = current.next
        current.next = previous
        previous = current
        current = next_node

    left = self.head
    right = previous

    while right is not None:
        if left.value != right.value:
            return False

        left = left.next
        right = right.next

    return True

Time:

O(n)

Space:

O(1)

Restore the second half afterward when preserving the original list matters.


โ†”๏ธ Doubly Linked Lists

A doubly linked node knows both neighbours:

class DoublyNode:
    def __init__(self, value):
        self.value = value
        self.next = None
        self.previous = None

Structure:

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

Append

def append(self, value):
    new_node = DoublyNode(value)

    if self.tail is None:
        self.head = new_node
        self.tail = new_node
    else:
        new_node.previous = self.tail
        self.tail.next = new_node
        self.tail = new_node

    self.size += 1

Time:

O(1)

Prepend

def prepend(self, value):
    new_node = DoublyNode(value)

    if self.head is None:
        self.head = new_node
        self.tail = new_node
    else:
        new_node.next = self.head
        self.head.previous = new_node
        self.head = new_node

    self.size += 1

Time:

O(1)

Delete a Known Node

def delete_node(self, node):
    if node.previous is None:
        self.head = node.next
    else:
        node.previous.next = node.next

    if node.next is None:
        self.tail = node.previous
    else:
        node.next.previous = node.previous

    self.size -= 1

If the node reference is already known:

O(1)

Trade-off

A doubly linked list uses more memory because each node stores another reference.

You gain backward traversal and faster deletion of known nodes, but you also gain more bookkeeping and more chances to connect the previous pointer to something it should have emotionally moved on from.


๐Ÿ”„ Circular Linked Lists

In a circular linked list, the final node points back to the head:

Use cases:

  • Round-robin scheduling

  • Repeating playlists

  • Turn rotation

  • Cyclic task assignment

  • Circular buffers

Traversal must stop when we reach the head again:

def display_circular(head):
    if head is None:
        return

    current = head

    while True:
        print(current.value, end=" -> ")
        current = current.next

        if current is head:
            break

    print("(back to head)")

A missing stop condition produces an infinite loop.

The data structure is not broken. It is obeying your instructions with dangerous enthusiasm.


๐Ÿ†š Arrays vs Linked Lists

Operation Dynamic Array / Python List Singly Linked List
Access by index O(1) O(n)
Search O(n) O(n)
Insert at beginning O(n) O(1)
Insert at end Amortized O(1) O(1) with tail
Delete beginning O(n) O(1)
Delete end Usually O(1) O(n)
Insert after known node Not the same model O(1)
Memory locality Good Poor
Extra link memory Low One or more references per node

Why arrays are often faster in practice

Big O is not the complete story.

Arrays provide:

  • Better CPU cache locality

  • Compact memory

  • Fast indexing

  • Fewer object allocations

  • Vectorization

  • GPU compatibility

Linked lists require:

  • One node object per element

  • Extra references

  • Pointer chasing

  • Poor memory locality

  • Allocation overhead

In Python, each node is a full object. For millions of small values, a linked list can use dramatically more memory than an array.

So even when both structures have O(n) traversal, the array may be much faster in practice.


๐Ÿ’พ Space Complexity and __slots__

A singly linked list with n nodes requires:

O(n)

total space.

Each node stores:

Data + Next reference

A doubly linked node stores:

Previous reference + Data + Next reference

A normal Python class often has an instance dictionary. When creating many nodes, __slots__ can reduce overhead:

class Node:
    __slots__ = ("value", "next")

    def __init__(self, value):
        self.value = value
        self.next = None

Use __slots__ when:

  • Creating many lightweight objects

  • Dynamic attributes are unnecessary

  • Memory consumption matters

It does not transform a Python linked list into a NumPy array, but it can make each node less expensive.


๐Ÿค– Where Linked Structures Appear in AI

Linked lists are not used for every AI workload.

Storing model weights as Python nodes would make your GPU cry before inference begins.

But linked structures appear in several useful places.


๐Ÿง  1. Agent Execution Traces

An AI agent may perform:

A step can reference the next step:

class AgentStep:
    def __init__(self, action, result=None):
        self.action = action
        self.result = result
        self.next = None

This can support:

  • Workflow inspection

  • Dynamic insertion of steps

  • Replaying execution

  • Failure analysis

  • Explainability traces

In many Python applications, a built-in list is simpler. A linked structure becomes useful when steps are inserted, removed, or reconnected dynamically.


โช 2. Undo and Redo Histories

An AI editor may support:

  • Generate

  • Rewrite

  • Undo

  • Redo

  • Compare versions

A doubly linked history allows movement both ways:

class Version:
    def __init__(self, content):
        self.content = content
        self.previous = None
        self.next = None

This resembles browser navigation.


๐Ÿ—ƒ๏ธ 3. LRU Cache Internals

An LRU cache removes the least recently used entry.

A common design combines:

  • A dictionary for O(1) key lookup

  • A doubly linked list for O(1) order updates

When an entry is used:

  1. Find it in the dictionary

  2. Move its node to the front

  3. Remove the tail when capacity is exceeded

This pattern is relevant for caching:

  • Embeddings

  • Prompt results

  • Retrieved documents

  • Model responses

  • Feature computations

A linked list alone cannot give fast lookup by key. The performance comes from combining data structures.

Good architecture is often two ordinary structures cooperating instead of one magical structure doing everything.


๐ŸŒ 4. Graph Adjacency Lists

AI systems use graphs for:

  • Knowledge graphs

  • Recommendations

  • Social connections

  • Agent workflows

  • Search and planning

Historically, adjacency lists could be implemented using linked lists.

In Python, ordinary lists are often used:

graph = {
    "AI": ["ML", "NLP"],
    "ML": ["Deep Learning"],
    "NLP": ["LLM"]
}

The conceptual structure is dynamic and relationship-based even if the implementation uses arrays.


๐ŸŒŠ 5. Processing Pipelines

A dynamic pipeline may look like:

class Handler:
    def __init__(self):
        self.next_handler = None

    def set_next(self, handler):
        self.next_handler = handler
        return handler

    def handle(self, data):
        if self.next_handler is not None:
            return self.next_handler.handle(data)

        return data

This resembles the chain of responsibility pattern.

Pipeline stages can be inserted, removed, or replaced without rewriting the entire flow.


๐Ÿงฌ 6. Sparse and Irregular Structures

Linked relationships help when:

  • Each item has a different number of neighbours

  • Connections change often

  • Traversal follows relationships

  • Nodes are inserted dynamically

Examples:

  • Parse structures

  • Symbolic AI systems

  • Rule engines

  • Search trees

  • Knowledge representations

Production implementations may use arrays, maps, compressed sparse formats, or graph libraries for better performance.

The linked-list concept remains useful even when the exact physical structure differs.


๐ŸŽฎ 7. AI Serving and Memory Blocks

Production inference systems may manage KV-cache pages, free blocks, or request queues through linked free structures or similar low-level techniques.

They may track:

  • Available blocks

  • Occupied blocks

  • Eviction order

  • Request ownership

  • Reusable memory regions

The Python developer sees:

model.generate(...)

The serving engine sees:

โ€œWhich memory block belongs to which request, and who forgot to release it?โ€


โš ๏ธ When a Linked List Is a Bad Choice

Avoid it when you need:

Fast random access

value = data[500_000]

Arrays win.

Dense numerical operations

Matrix multiplication, convolution, and attention require tensors or array-like layouts.

GPU computation

GPUs prefer large contiguous memory blocks and predictable access.

Compact storage

A Python object per value adds substantial overhead.

Simple append-only collections

Pythonโ€™s built-in list may be easier and faster.

Queue operations in Python

collections.deque is usually a better production choice than writing your own linked queue.

Do not use a linked list because the data โ€œfeels connected.โ€

Everything in a grocery list is connected by your hunger. That does not require custom nodes.


๐Ÿ› ๏ธ Complete Singly Linked List Implementation

class Node:
    __slots__ = ("value", "next")

    def __init__(self, value):
        self.value = value
        self.next = None


class SinglyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

    def __len__(self):
        return self.size

    def is_empty(self):
        return self.head is None

    def prepend(self, value):
        new_node = Node(value)
        new_node.next = self.head
        self.head = new_node

        if self.tail is None:
            self.tail = new_node

        self.size += 1

    def append(self, value):
        new_node = Node(value)

        if self.tail is None:
            self.head = new_node
            self.tail = new_node
        else:
            self.tail.next = new_node
            self.tail = new_node

        self.size += 1

    def insert_at(self, index, value):
        if index < 0 or index > self.size:
            raise IndexError("Index out of range")

        if index == 0:
            self.prepend(value)
            return

        if index == self.size:
            self.append(value)
            return

        current = self.head

        for _ in range(index - 1):
            current = current.next

        new_node = Node(value)
        new_node.next = current.next
        current.next = new_node
        self.size += 1

    def get(self, index):
        if index < 0 or index >= self.size:
            raise IndexError("Index out of range")

        current = self.head

        for _ in range(index):
            current = current.next

        return current.value

    def contains(self, target):
        current = self.head

        while current is not None:
            if current.value == target:
                return True

            current = current.next

        return False

    def delete_first(self):
        if self.head is None:
            return None

        removed_value = self.head.value
        self.head = self.head.next
        self.size -= 1

        if self.head is None:
            self.tail = None

        return removed_value

    def delete_last(self):
        if self.head is None:
            return None

        if self.head is self.tail:
            removed_value = self.head.value
            self.head = None
            self.tail = None
            self.size = 0
            return removed_value

        current = self.head

        while current.next is not self.tail:
            current = current.next

        removed_value = self.tail.value
        current.next = None
        self.tail = current
        self.size -= 1

        return removed_value

    def delete_value(self, target):
        if self.head is None:
            return False

        if self.head.value == target:
            self.delete_first()
            return True

        current = self.head

        while current.next is not None:
            if current.next.value == target:
                node_to_remove = current.next
                current.next = node_to_remove.next

                if node_to_remove is self.tail:
                    self.tail = current

                self.size -= 1
                return True

            current = current.next

        return False

    def reverse(self):
        previous = None
        current = self.head
        self.tail = self.head

        while current is not None:
            next_node = current.next
            current.next = previous
            previous = current
            current = next_node

        self.head = previous

    def find_middle(self):
        if self.head is None:
            return None

        slow = self.head
        fast = self.head

        while fast is not None and fast.next is not None:
            slow = slow.next
            fast = fast.next.next

        return slow.value

    def has_cycle(self):
        slow = self.head
        fast = self.head

        while fast is not None and fast.next is not None:
            slow = slow.next
            fast = fast.next.next

            if slow is fast:
                return True

        return False

    def clear(self):
        self.head = None
        self.tail = None
        self.size = 0

    def __iter__(self):
        current = self.head

        while current is not None:
            yield current.value
            current = current.next

    def __str__(self):
        if self.head is None:
            return "None"

        return " -> ".join(
            str(value)
            for value in self
        ) + " -> None"

Usage:

topics = SinglyLinkedList()

topics.append("Big O")
topics.append("Classes & Objects")
topics.append("Memory Management")
topics.append("Linked Lists")

print(topics)
print(len(topics))
print(topics.find_middle())

topics.reverse()

print(topics)

๐Ÿ“Š Time Complexity Cheat Sheet

Singly Linked List

Operation Without Tail/Size With Tail/Size
Access head O(1) O(1)
Access tail O(n) O(1)
Access by index O(n) O(n)
Search O(n) O(n)
Prepend O(1) O(1)
Append O(n) O(1)
Delete head O(1) O(1)
Delete tail O(n) O(n)
Insert after known node O(1) O(1)
Delete after known node O(1) O(1)
Get length O(n) O(1)
Reverse O(n) O(n)
Find middle O(n) O(n)
Detect cycle O(n) O(n)

Doubly Linked List

Operation Complexity
Access by index O(n)
Search O(n)
Prepend O(1)
Append with tail O(1)
Delete head O(1)
Delete tail O(1)
Delete known node O(1)
Move to next or previous O(1) per step
Full traversal O(n)

Important Reminder

O(1) insertion usually assumes that the insertion node or position is already known.

If we first search for the position, the complete operation may still be:

O(n)

๐ŸŽค Common Interview Questions

Why is insertion at the head O(1)?

Because we change only a fixed number of references:

new_node.next = head
head = new_node

Why is indexed access O(n)?

Nodes are not addressed through direct offsets. We must follow links from the head.

Is appending always O(1)?

Only when a tail reference is maintained. Without it, appending requires traversal.

Why is deleting the tail of a singly linked list O(n)?

We need the previous node, but the list stores only forward links.

Why can a doubly linked list delete a known node in O(1)?

The node directly references both neighbours.

What is the disadvantage of a doubly linked list?

Extra memory and more complex reference maintenance.

How do you detect a cycle without extra memory?

Use Floydโ€™s slow-and-fast pointer algorithm.

What is the difference between == and is for nodes?

== may compare values. is checks whether two references identify the exact same object.

Why are linked lists less cache-friendly?

Nodes may be scattered across memory, forcing the processor to chase references rather than scanning a compact block.

Are linked lists frequently used directly in Python?

The concepts are common, but built-ins such as list, deque, dictionaries, and specialized libraries are usually more practical.

When should a linked list be preferred?

When frequent insertion or deletion around already-known nodes matters more than fast random access and compact memory.


๐Ÿšจ Common Mistakes

1. Losing the Rest of the List

Wrong reversal logic:

current.next = previous
current = current.next

After changing current.next, the original next node is lost.

Correct:

next_node = current.next
current.next = previous
current = next_node

2. Forgetting the Empty-List Case

Always handle:

if self.head is None:

3. Forgetting to Update the Tail

Deleting or appending around the end may require changing self.tail.

4. Incorrect Size Tracking

Every successful insertion and deletion should update size exactly once.

5. Infinite Traversal Because of a Cycle

A normal display method may never terminate if the list contains a loop.

6. Comparing Values Instead of Node Identity

Two nodes can both store "AI" and still be different objects.

7. Accidentally Sharing Nodes Across Lists

Two lists may unintentionally share the same tail. Updating one structure can affect the other.

8. Assuming Theoretical Complexity Guarantees Speed

Poor cache locality and Python-object overhead may make linked lists slower than built-in arrays even when Big O looks attractive.


โœ… Questions to Ask Before Choosing a Linked List

  • Do I need fast random access?

  • Will I traverse nearly every item frequently?

  • Do I already know the node where insertion occurs?

  • Are insertions and deletions more common than indexing?

  • Is extra node memory acceptable?

  • Does CPU cache locality matter?

  • Will the data be processed on a GPU?

  • Would collections.deque solve the problem more simply?

  • Do I need backward traversal?

  • Can cycles occur?

  • Who owns the nodes?

  • Will millions of Python objects create excessive overhead?

  • Should the logical links be represented using arrays or indexes instead?

  • Would a dictionary plus doubly linked list be more suitable?

The best data structure is not the one with the coolest diagram.

It is the one matching the actual workload.


๐Ÿ“Œ Linked List Quick Cheat Sheet

Node

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

Prepend

new_node.next = head
head = new_node

Time:

O(1)

Append with Tail

tail.next = new_node
tail = new_node

Time:

O(1)
while current is not None:
    if current.value == target:
        return True

    current = current.next

Time:

O(n)

Reverse

previous = None
current = head

while current is not None:
    next_node = current.next
    current.next = previous
    previous = current
    current = next_node

head = previous

Time:

O(n)

Space:

O(1)

Find the Middle

slow = head
fast = head

while fast and fast.next:
    slow = slow.next
    fast = fast.next.next

Cycle Detection

if slow is fast:
    cycle_exists = True

Singly Linked List

Data + Next

Doubly Linked List

Previous + Data + Next

Circular Linked List

Tail.next = Head

Golden Rule

Linked lists provide flexible relationships, not magical performance.


๐ŸŽฏ Final Thoughts

A linked list begins with a simple picture:

But it teaches several deep computer-science ideas:

  • References

  • Object identity

  • Dynamic memory

  • Traversal

  • Time complexity

  • Two-pointer algorithms

  • Cycles

  • Data-structure trade-offs

  • Cache locality

  • Composite architecture

In the age of AI, linked lists are not the default structure for tensors, embeddings, or model weights.

Arrays and specialized numerical structures dominate those workloads.

But linked ideas still appear in:

  • Agent workflows

  • Version histories

  • LRU caches

  • Graph representations

  • Event pipelines

  • Memory managers

  • Dynamic execution chains

  • AI-serving infrastructure

Most importantly, linked lists teach us to ask:

What operations must this system perform most frequently?

If the answer is:

โ€œJump directly to item 900,000,โ€

choose an array.

If the answer is:

โ€œInsert and remove items around known locations while preserving relationships,โ€

a linked structure may be exactly right.

So the next time someone says:

โ€œLinked-list insertion is always O(1),โ€

politely ask:

โ€œDo we already have the node, or are we travelling through the entire list like a courier without Google Maps?โ€ ๐Ÿ˜„

Good data-structure decisions are not made by memorizing one complexity value.

They are made by understanding the complete operation, the memory layout, and the real workload.

In AI engineering, that difference can determine whether a system is elegant and scalableโ€”or simply a chain of objects pointing toward production chaos. ๐Ÿค–๐Ÿ”—๐Ÿš€


๐ŸŒ Letโ€™s Connect

Whether you are learning data structures, building AI systems, preparing for interviews, or trying to understand why your linked list entered an infinite loop, I am always happy to connect.

Let us continue building AI systems that are not only intelligent, but also supported by strong computer-science foundations.

LinkedIn: Connect with me for AI, AWS, cloud engineering, Python, and software-development content.
hardeepjethwani@LinkedIn

TopMate: Looking for guidance, brainstorming, or a technical discussion over a virtual coffee?
hardeepjethwani@TopMate

Instagram: Follow my learning journey, technology experiments, and behind-the-scenes builder content.
hardeepjethwani@Instagram

X: Join the conversation around AI, cloud, Python, data structures, and developer humour.
hardeepjethwani@X

Want to support my AI, cloud, and educational adventures? Feel free to buy me a virtual coffee.

After all, coffee is basically a circular linked list:

There is no None at the end. โ˜•๐Ÿ˜‚


#LinkedList #DataStructures #DSA #Python #ArtificialIntelligence #AIEngineering #Algorithms #TimeComplexity #SpaceComplexity #SinglyLinkedList #DoublyLinkedList #PythonProgramming #MachineLearning #GenerativeAI #SoftwareEngineering #ComputerScience #Coding #Programming #TechEducation #DeveloperCommunity #BeginnerFriendly #InterviewPreparation