Back to DSA Python report

Linked Lists & Nodes

Interactive Python notes for linked-list intuition, node references, CRUD operations, dry runs, quizzes, and practice prompts.

Linked Lists in Python β€” Interactive Notes

πŸ”

Intuition

β–Ά

Real-life analogy

πŸš‚
Train cars: each car (node) holds cargo (data) and is connected to the next car (pointer). You must walk car-by-car from the engine (head) to reach any car. You can't jump directly to car #5.
πŸ”—
Chain links: each link holds data and knows only the next link. Removing a link? Just re-connect the neighbours.
πŸ“Œ
Scavenger hunt: the first clue (head) tells you where to find the next. Each clue leads to the next, until a clue says "done!" (None).

Linked List vs Array β€” when to choose

FeatureArray / ListLinked List
Access by indexO(1) β€” directO(n) β€” must traverse
Insert at headO(n) β€” shift allO(1) β€” rewire head
Insert at tailO(1) amortisedO(n) (singly) / O(1) (with tail ptr)
Delete from headO(n)O(1)
MemoryContiguous blockScattered β€” each node anywhere
Dynamic resizeExpensive reallocFree β€” add node anywhere
Use linked lists when inserts/deletes at the front are frequent, or when size is highly dynamic (e.g., LRU cache, undo history, queue/deque).
πŸ“˜

Definitions

β–Ά

Node

A node is the basic unit of a linked list. It contains two things:

  • data β€” the value stored (int, string, object, anything)
  • next β€” a reference (pointer) to the next node, or None if last
head
10
next β†’
β†’
20
next β†’
β†’
tail
30
next β†’
β†’
None

Types of Linked Lists

Singly Each node points to only the next node. Traversal: one direction only.
A
β†’
β†’
B
β†’
β†’
C
β†’
β†’
None
Doubly Each node has prev + next pointers. Can traverse both ways.
None ← [A] ⇄ [B] ⇄ [C] β†’ None
Circular Tail's next points back to head. No None terminator.
[A] β†’ [B] β†’ [C] β†’ back to [A]

Head & Tail

  • head β€” reference to the first node. Entry point to the list. If head is None, the list is empty.
  • tail β€” reference to the last node. Its next is always None (singly). Maintained explicitly for O(1) appends.
🧠

Core Concepts

β–Ά

Dynamic memory & references

Python manages memory automatically. A node object lives in the heap. The variable holding it is just a reference (pointer) β€” a label that says "look over here". Reassigning a reference does not move the node, it just changes where the label points.

Key mental model: current = current.next means "move the label to the next node", not "move the node".

Traversal pattern (the cursor loop)

# canonical traversal β€” O(n)
current = self.head          # start at head
while current is not None:
    print(current.data)      # do something
    current = current.next   # advance cursor

Think of current as a flashlight scanning the chain. When it hits None, you've reached the end.

βš™οΈ

CRUD Operations β€” Interactive Step-by-Step

β–Ά
Create
Read
Update
Delete

Create β€” Insert at head O(1)

def prepend(self, data):
    new_node = Node(data)        # 1. create node
    new_node.next = self.head    # 2. point new β†’ old head
    self.head = new_node         # 3. head = new node
Press "Next step" to walk through prepend(5) on list [10β†’20β†’30]
0 / 4

Create β€” Append at tail O(n) / O(1) with tail ptr

def append(self, data):
    new_node = Node(data)
    if not self.head:
        self.head = new_node; return
    current = self.head
    while current.next:        # walk to last node
        current = current.next
    current.next = new_node   # attach at end

Create β€” Insert after given node O(n) to find, O(1) to insert

def insert_after(self, target_data, data):
    current = self.head
    while current and current.data != target_data:
        current = current.next
    if current:
        new_node = Node(data)
        new_node.next = current.next  # new β†’ current's next
        current.next = new_node       # current β†’ new

Read β€” Traverse (print all) O(n)

def display(self):
    current = self.head
    while current:
        print(current.data, end=" β†’ ")
        current = current.next
    print("None")

Read β€” Search by value O(n)

def search(self, target):
    current = self.head
    position = 0
    while current:
        if current.data == target:
            return position    # found!
        current = current.next
        position += 1
    return -1               # not found

Update β€” Modify node value O(n)

def update(self, old_val, new_val):
    current = self.head
    while current:
        if current.data == old_val:
            current.data = new_val  # in-place update
            return True
        current = current.next
    return False                # not found
πŸ’‘
Update never rewires pointers β€” only the data field changes. Pointers stay identical. This means update is "safe" β€” no risk of losing nodes.

Delete β€” Three cases

Case 1: Delete head β€” just move head forward
Case 2: Delete tail β€” traverse to second-last, set its next = None
Case 3: Delete middle β€” prev.next = current.next (bypass)
def delete(self, target):
    if not self.head: return          # empty list
    if self.head.data == target:      # case 1: head
        self.head = self.head.next
        return
    prev, current = None, self.head
    while current:
        if current.data == target:
            prev.next = current.next  # bypass node
            return
        prev = current
        current = current.next
Press "Next step" to walk through delete(20) on [10β†’20β†’30]
0 / 5
Never lose the prev pointer before bypassing. Losing track of prev means you can't reconnect the chain β€” nodes get orphaned in memory.
🐍

Full Python Implementation

β–Ά
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None     # pointer to next node

class LinkedList:
    def __init__(self):
        self.head = None     # empty list

    # ── CREATE ───────────────────────────────
    def prepend(self, data):          # O(1)
        node = Node(data)
        node.next = self.head
        self.head = node

    def append(self, data):           # O(n)
        node = Node(data)
        if not self.head:
            self.head = node; return
        cur = self.head
        while cur.next: cur = cur.next
        cur.next = node

    # ── READ ─────────────────────────────────
    def display(self):                # O(n)
        cur, res = self.head, []
        while cur:
            res.append(str(cur.data))
            cur = cur.next
        print(" β†’ ".join(res) + " β†’ None")

    def search(self, target):         # O(n)
        cur, i = self.head, 0
        while cur:
            if cur.data == target: return i
            cur, i = cur.next, i + 1
        return -1

    # ── UPDATE ───────────────────────────────
    def update(self, old, new):       # O(n)
        cur = self.head
        while cur:
            if cur.data == old:
                cur.data = new; return True
            cur = cur.next
        return False

    # ── DELETE ───────────────────────────────
    def delete(self, target):         # O(n)
        if not self.head: return
        if self.head.data == target:
            self.head = self.head.next; return
        prev, cur = None, self.head
        while cur:
            if cur.data == target:
                prev.next = cur.next; return
            prev, cur = cur, cur.next

# ── USAGE ────────────────────────────────────
ll = LinkedList()
ll.append(10); ll.append(20); ll.append(30)
ll.prepend(5)
ll.display()           # 5 β†’ 10 β†’ 20 β†’ 30 β†’ None
print(ll.search(20))   # 2
ll.update(20, 99)
ll.delete(10)
ll.display()           # 5 β†’ 99 β†’ 30 β†’ None
⚑

Complexity Table

β–Ά
OperationTimeSpaceNotes
Prepend (insert head)O(1)O(1)Just rewire head pointer
Append (insert tail)O(n)O(1)O(1) if tail pointer maintained
Insert after nodeO(n)O(1)O(n) to find, O(1) to link
Access by indexO(n)O(1)No random access β€” must traverse
Search by valueO(n)O(1)Linear scan
Update valueO(n)O(1)Find then mutate data field
Delete headO(1)O(1)Move head pointer forward
Delete tail / middleO(n)O(1)Must find predecessor
Reverse (iterative)O(n)O(1)Three-pointer technique
Detect cycle (Floyd's)O(n)O(1)Fast + slow pointer
🧩

Patterns & Use Cases

β–Ά

Reverse a linked list (iterative) O(n) time, O(1) space

def reverse(self):
    prev, current = None, self.head
    while current:
        nxt = current.next      # save next
        current.next = prev     # flip pointer
        prev = current          # advance prev
        current = nxt           # advance current
    self.head = prev            # new head
Before: [1]β†’[2]β†’[3]β†’None  |  After: [3]β†’[2]β†’[1]β†’None
Three variables: prev, current, nxt. Never lose nxt before flipping!

Detect a cycle β€” Floyd's algorithm O(n) time, O(1) space

def has_cycle(self):
    slow = fast = self.head
    while fast and fast.next:
        slow = slow.next         # 1 step
        fast = fast.next.next    # 2 steps
        if slow == fast:
            return True          # cycle!
    return False
πŸƒ
Imagine two runners on a circular track. The faster one eventually laps the slower one β€” they meet. If there's no cycle, the fast runner just falls off the end (hits None).

Common use cases

  • Undo/Redo β€” each action is a node; undo = move head back
  • LRU Cache β€” doubly linked list + hash map
  • Browser history β€” doubly LL for back/forward
  • Queue / Deque β€” O(1) enqueue at tail, O(1) dequeue at head
  • Polynomial arithmetic β€” each term is a node
πŸ’»

Dry Runs

β–Ά

Dry run: delete(20) on [10β†’20β†’30]

1
Is head None? No. Is head.data (10) == 20? No. Continue.
head/prev=None / cur
10
β†’
β†’
20
β†’
β†’
30
β†’
β†’
None
2
prev = node(10), current = node(20). Is current.data (20) == 20? YES!
prev
10
β†’
β†’
current (target)
20
β†’
β†’
30
β†’
β†’
None
3
Execute: prev.next = current.next β†’ node(10).next = node(30). Node(20) is bypassed.
head
10
β†’
β†’β†’
30
β†’
β†’
None
   
20
βœ•
4
Result: [10 β†’ 30 β†’ None]. Node 20 has no references, Python GC will reclaim it.
🧠

Concept Checks

β–Ά
πŸ§ͺ

Practice Problems

β–Ά
πŸš€

Interview Tips

β–Ά

Common mistakes

Losing the reference: Always save nxt = current.next before modifying current.next. One wrong assignment and you orphan half your list.
Null/None handling: Always check if not self.head before any operation. Forgetting this causes AttributeError on empty lists.
Off-by-one in traversal: To reach the last node, stop when current.next is None, not when current is None.
Forgetting to update head: When deleting head or reversing, explicitly reassign self.head. It doesn't move itself.

Interview best practices

  • Draw the list before writing code β€” interviewers love visual thinkers
  • Always ask: singly or doubly? size constraints? what if empty?
  • Use two-pointer technique for cycle detection, finding middle, nth-from-end
  • For in-place reversal, articulate the three-pointer setup clearly
  • Mention time + space complexity unprompted β€” shows awareness
  • Edge cases to always mention: empty list, single node, duplicate values, cycle