Interactive Python notes for linked-list intuition, node references, CRUD operations, dry runs, quizzes, and practice prompts.
| Feature | Array / List | Linked List |
|---|---|---|
| Access by index | O(1) β direct | O(n) β must traverse |
| Insert at head | O(n) β shift all | O(1) β rewire head |
| Insert at tail | O(1) amortised | O(n) (singly) / O(1) (with tail ptr) |
| Delete from head | O(n) | O(1) |
| Memory | Contiguous block | Scattered β each node anywhere |
| Dynamic resize | Expensive realloc | Free β add node anywhere |
A node is the basic unit of a linked list. It contains two things:
None if lasthead is None, the list is empty.next is always None (singly). Maintained explicitly for O(1) appends.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.
current = current.next means "move the label to the next node", not "move the node".# 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.
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
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
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
def display(self): current = self.head while current: print(current.data, end=" β ") current = current.next print("None")
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
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
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
prev pointer before bypassing. Losing track of prev means you can't reconnect the chain β nodes get orphaned in memory.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
| Operation | Time | Space | Notes |
|---|---|---|---|
| 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 node | O(n) | O(1) | O(n) to find, O(1) to link |
| Access by index | O(n) | O(1) | No random access β must traverse |
| Search by value | O(n) | O(1) | Linear scan |
| Update value | O(n) | O(1) | Find then mutate data field |
| Delete head | O(1) | O(1) | Move head pointer forward |
| Delete tail / middle | O(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 |
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
prev, current, nxt. Never lose nxt before flipping!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
prev.next = current.next β node(10).next = node(30). Node(20) is bypassed.
nxt = current.next before modifying current.next. One wrong assignment and you orphan half your list.if not self.head before any operation. Forgetting this causes AttributeError on empty lists.current.next is None, not when current is None.self.head. It doesn't move itself.