DSA Field Notes
Course map / 02 Linked Lists
NeetCode · DSA for Beginners · Lessons 5-7

Linked List Field Notes

Singly linked lists, doubly linked lists, and queues. Every diagram shows exactly which pointer moved. Read the notes, then close them and rebuild the code from memory.

0Why linked lists exist

Analogy

An array is a row of numbered lockers, all next to each other. A linked list is a treasure hunt: each clue is stored anywhere in the building, and each clue tells you where the next clue is. To reach clue 3, you must first read clue 1 and clue 2.

Both store items in order. The difference is where the items live in memory and how you find the next one.

ARRAY values sit side by side, so index math finds any item instantly red blue green yellow @100 @104 @108 @112 arr[2] = start + 2 × size → O(1) LINKED LIST nodes are scattered; each one stores the address of the next red @100 blue @340 green @212 ∅ next = @340 next = @212
Same order, different storage. The list order is red → blue → green, but the nodes sit at addresses 100, 340, 212. Order comes from the pointers, not from the position in memory. Contiguous means "side by side in memory". Arrays are contiguous; linked lists are not.
QuestionArrayLinked list
Get item at position iO(1)O(n)
Insert or delete in the middle (you already stand at that node)O(n) shiftingO(1)
Memory per itemvalue onlyvalue + pointer(s)
Grow without copyingNo (dynamic array copies)Yes

1Singly linked list

The node: two things in a box

A ListNode is an object with exactly two fields. val holds the data. next holds a reference (also called a pointer): the memory address of another node. In Python, "no node" is written None. NeetCode's videos say null; it is the same idea.

"red" val next another node, or None one ListNode object
The only thing to memorize. A node is data plus one arrow. Everything else in this topic is "change which box an arrow points at".
class ListNode:
    def __init__(self, val):
        self.val = val
        self.next = None     # a new node points nowhere

Building a chain by hand

Create three nodes, then set each next to the following node. Only the last node keeps next = None.

ListNode1 = ListNode("red")
ListNode2 = ListNode("blue")
ListNode3 = ListNode("green")

ListNode1.next = ListNode2    # red  → blue
ListNode2.next = ListNode3    # blue → green
ListNode3.next = None         # green is the end (already None, shown for clarity)
head tail red blue green ∅ ← next = None (end of list) ListNode1 ListNode2 ListNode3
Head and tail. head is a pointer to the first node; tail points to the last one. If the list has one node, head and tail point to the same node. If the list is empty, both are None.

Traversal: walk the chain

Traverse means "visit every node from first to last". You cannot jump. You start at the head and follow next until you hit None.

cur = head            # cur = "the node I am standing on"
while cur:            # stops when cur becomes None
    print(cur.val)
    cur = cur.next    # step forward
red blue green ∅ cur ① cur ② cur ③ cur ④ = None loop stops 4 checks of "while cur", 3 nodes printed → O(n)
The loop runs once per node, then one more check that fails. Time is O(n) where n is the number of nodes.
Loop turncur iswhile cur?printsthen cur = cur.next →
1red nodeTrueredblue node
2blue nodeTruebluegreen node
3green nodeTruegreenNone
4NoneFalse-exit loop
Keep in mind

cur.next is a node object, not a value. To read the next value write cur.next.val. Writing print(cur.next) prints something like <ListNode object at 0x1f3...>.

Append at the tail: 2 lines

Because we keep a tail pointer, adding to the end does not need a walk. Two pointer changes, O(1).

ListNode4 = ListNode("yellow")
tail.next = ListNode4     # step 1: old last node now points to the new node
tail = ListNode4          # step 2: move the tail label to the new node
red blue green ① yellow ∅ tail tail ② ① tail.next = ListNode4 ② tail = ListNode4 (no shifting, no copying)
Orange = what changed. Step ① draws the arrow from green to yellow. Step ② moves the tail label. Do them in this order: if you move tail first, you lose your handle on green and cannot set its next.

Delete a node: skip it

To delete a node you never "erase" it. You make the node before it point past it. Suppose we delete blue (the node after head):

head.next = head.next.next
# head.next      is blue
# head.next.next is green
# so: red now points to green; blue is skipped
head red head.next = head.next.next blue nobody points here anymore → garbage collected green ∅
One pointer change deletes a node. Blue still exists for a moment, but nothing points to it. Python's garbage collector (automatic memory cleanup) frees it. In C you would free it yourself. If blue were the tail, you must also move tail to red.
The star in O(1)*

Insert and delete are O(1) only if you already hold a reference to the node just before the spot. If you must walk from the head to find that spot, the walk costs O(n). NeetCode marks this with a star in the table. Interviewers ask about it.

Circular list: the infinite loop trap

If the last node's next points back to an earlier node, the list has no end. while cur: never sees None and runs forever.

red blue green green.next = red → no None → while loop never ends
Cycles are detected with two pointers (slow and fast), which you covered earlier as Floyd's algorithm. The pattern is in section 4.

Full class with head and tail

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

    def append(self, val):
        node = ListNode(val)
        if self.tail:                # list has nodes
            self.tail.next = node
            self.tail = node
        else:                        # list is empty: head and tail are the same node
            self.head = self.tail = node

    def delete_after(self, node):
        # removes node.next (the node AFTER the one you hold)
        if node is None or node.next is None:
            return
        removed = node.next
        node.next = removed.next
        if removed is self.tail:      # deleted the last node? move tail back
            self.tail = node

    def to_list(self):
        out, cur = [], self.head
        while cur:
            out.append(cur.val)
            cur = cur.next
        return out

2Doubly linked list

Analogy

A train. Each car is hooked to the car in front and the car behind. You can walk from the engine to the back, or from the back to the engine. The engine has nothing in front (prev = None); the last car has nothing behind (next = None).

Same as a singly linked list, plus one extra pointer per node: prev, which points to the previous node. Cost: a little more memory and one more pointer to update on every change. Benefit: you can walk backwards, and you can delete a node when you only hold that node (no need for the node before it).

head tail ∅ 1 2 3 ∅ prev val next bold arrows = next (→) faded arrows = prev (←)
Three fields per node. prev = None tells you "I am the head". next = None tells you "I am the tail".
class DNode:
    def __init__(self, val):
        self.val = val
        self.prev = None
        self.next = None

Insert at end: 3 lines, in order

ListNode4 = DNode(4)
tail.next = ListNode4      # ① old tail → new node
ListNode4.prev = tail      # ② new node ← old tail
tail = tail.next           # ③ move the tail label
3 ① ② 4 ∅ tail tail ③ two arrows must be drawn, one in each direction, before tail moves
Every link in a doubly linked list is two arrows. Forget prev and walking backwards will skip the new node.

Delete at end

Here prev pays off. In a singly linked list you cannot delete the tail in O(1) because you do not know the node before it. In a doubly linked list, tail.prev gives it to you instantly.

ListNode2 = tail.prev      # ① grab the node before the tail
ListNode2.next = None       # ② cut the forward arrow
tail = ListNode2           # ③ move the tail label back
# optional: old_tail.prev = None  (helps garbage collection, not required)
2 ∅ ② next = None 3 ∅ unreachable → freed tail ③ tail ① ListNode2 = tail.prev is what makes this O(1)
O(1) insert and delete at both ends. This is why a doubly linked list can act as a stack or a queue.

Front operations (the animations in the lesson)

Same idea, mirrored. Insert at front: new node's next is the old head, old head's prev is the new node, then move head. Delete at front: grab head.next, set its prev to None, move head.

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

    def insert_end(self, val):
        node = DNode(val)
        if self.tail:
            self.tail.next = node
            node.prev = self.tail
            self.tail = node
        else:
            self.head = self.tail = node

    def insert_front(self, val):
        node = DNode(val)
        if self.head:
            node.next = self.head
            self.head.prev = node
            self.head = node
        else:
            self.head = self.tail = node

    def delete_end(self):
        if not self.tail:
            return None
        val = self.tail.val
        if self.tail is self.head:        # only one node → list becomes empty
            self.head = self.tail = None
        else:
            before = self.tail.prev
            before.next = None
            self.tail = before
        return val

    def delete_front(self):
        if not self.head:
            return None
        val = self.head.val
        if self.head is self.tail:
            self.head = self.tail = None
        else:
            after = self.head.next
            after.prev = None
            self.head = after
        return val
Keep in mind

Every method has three cases: empty list, one node, many nodes. Most bugs live in the first two. When you write a linked list method, test those two first.

3Queue

Analogy

A line at the bank. New people join at the back. The teller serves the person at the front. First in, first out. That rule has a name: FIFO.

A queue is not a new structure; it is a linked list with two rules. Add only at the back (enqueue). Remove only from the front (dequeue). NeetCode names the two ends left (front) and right (back). Compare with a stack, which is LIFO: last in, first out.

left (front) right (back) A B C ∅ dequeue returns A D enqueue goes after C served in order: A, B, C, D — first in, first out
Two pointers, two doors. left is the head (exit door), right is the tail (entry door). Both operations touch only the ends, so both are O(1).

The full queue, with the empty-list cases handled

class Queue:
    def __init__(self):
        self.left = None     # front: dequeue here
        self.right = None    # back:  enqueue here

    def enqueue(self, val):
        newNode = ListNode(val)
        if self.right:                   # queue is non-empty
            self.right.next = newNode
            self.right = self.right.next
        else:                            # queue is empty
            self.left = self.right = newNode

    def dequeue(self):
        if not self.left:                # queue is empty: nothing to return
            return None
        val = self.left.val
        self.left = self.left.next
        if not self.left:                # we just removed the last node
            self.right = None            # right must not point at a dead node
        return val
The line people forget

if not self.left: self.right = None. After removing the last node, right still points to it. The next enqueue would then attach to a node that is not in the queue, and left stays None. The queue silently breaks.

In real Python code

Do not build a queue from scratch in an interview unless asked. Use collections.deque. A deque ("deck", double-ended queue) supports O(1) add and remove at both ends.

from collections import deque

q = deque()
q.append(1)        # enqueue at the back
q.append(2)
q.popleft()        # dequeue from the front → 1
q.appendleft(0)    # also allowed: add at front
q.pop()            # also allowed: remove from back

# NEVER do this for a queue:
lst = [1, 2, 3]
lst.pop(0)         # O(n): every element shifts left

Where you will use a queue next: breadth-first search (BFS) on trees and graphs. BFS is "visit everything 1 step away, then everything 2 steps away". A queue keeps that order for you. Trees are the next big topic in the course.

4Three moves that solve most linked list problems

The lessons teach the structure. Interviews test these three moves on top of it. Learn them as muscle memory.

Move 1: Reverse with prev / cur / nxt

Walk the list once. At each node, save where you are going, flip the arrow to point backwards, then move both markers forward.

def reverse_list(head):
    prev, cur = None, head
    while cur:
        nxt = cur.next      # 1. save next  (or you lose the rest of the list)
        cur.next = prev     # 2. flip the arrow
        prev = cur          # 3. move prev forward
        cur = nxt           # 4. move cur forward
    return prev             # prev is the new head
prev cur nxt (saved) 1 ∅ already flipped 2 cur.next = prev 3 not yet touched 4 ∅
The dashed arrow is why you save nxt first. The moment you flip cur.next, node 3 and everything after it are unreachable from cur. nxt is your only way there.

Move 2: Dummy node

Deleting the head is a special case because nothing points to it. Fix: put a fake node in front so every real node has a node before it. Return dummy.next at the end.

def remove_value(head, target):
    dummy = ListNode(0)
    dummy.next = head
    cur = dummy                        # cur always stands BEFORE the node we inspect
    while cur.next:
        if cur.next.val == target:
            cur.next = cur.next.next   # skip it (same trick as section 1)
        else:
            cur = cur.next
    return dummy.next                  # the real head, even if the old head was removed

Move 3: Slow and fast pointers

Two walkers. Slow moves 1 step, fast moves 2. When fast hits the end, slow is at the middle. If the list has a cycle, fast laps slow and they meet.

def middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False
1 2 3 4 5 slow (2 steps) fast (4 steps) fast.next is None → stop → slow is the middle. Even length? slow lands on the second middle.
Guard with while fast and fast.next. fast.next.next crashes if fast.next is None. This guard is the most common bug in this pattern.

5Big-O summary

Say these out loud in interviews. The star always means: "if I already hold a reference to the node at that spot".

OperationSingly LLDoubly LLQueue (LL)Python list
Access by indexO(n)O(n)-O(1)
Search by valueO(n)O(n)-O(n)
Insert at end (have tail)O(1)O(1)O(1) enqueueO(1)*
Delete at endO(n)O(1)-O(1)
Insert / delete at frontO(1)O(1)O(1) dequeueO(n)
Insert / delete in middleO(1)*O(1)*-O(n)
Walk backwardsnoyes-yes

Why singly delete-at-end is O(n): you need the node before the tail, and a singly list gives you no way to go back. You must walk from the head. Python list's O(1)* at end is "amortized": occasionally it copies to a bigger array.

6Mistakes to watch for

Losing the rest of the list

You overwrite cur.next before saving it. Everything after cur is gone. Always nxt = cur.next first.

Reading a node as a value

cur.next is an object. Compare or print cur.next.val. You made this exact mistake earlier; it is normal.

Calling .next on None

AttributeError: 'NoneType' object has no attribute 'next'. Guard every step: while cur and cur.next, while fast and fast.next.

Forgetting to move tail (or right)

After append, tail must point at the new node. After removing the last node, tail (or the queue's right) must be updated or set to None.

Skipping the empty and one-node cases

First line of every method: what if the list is empty? Second: what if it has one node? Write those branches before the general case.

Wrong order of pointer updates

In a doubly linked list, draw the arrow to the new node before moving head or tail. Move the label last.

Using list.pop(0) as a queue

O(n) every time. Use collections.deque and popleft().

Mixing the node class and the list class

val / next / prev belong to the node. head / tail / left / right belong to the list or queue. Do not put self.val inside Queue.__init__.

Interview one-liners

  • "A linked list trades O(1) random access for O(1) insert and delete at a known position."
  • "Nodes are not contiguous in memory, so there is no index math; I have to traverse."
  • "Insert and delete are O(1) only if I already hold the node before the position; otherwise the walk is O(n)."
  • "A doubly linked list costs one extra pointer per node and buys O(1) delete-at-tail and backward traversal."
  • "A queue is FIFO; with a linked list both enqueue and dequeue are O(1). In Python I use collections.deque."
  • "For head-edge cases I use a dummy node. For middle or cycle questions I use slow and fast pointers."

7Practice, in this order

Notes do not make you good at this. Solving does. Do these on LeetCode with no notes open, then check against the templates above.

#ProblemMove it trainsLevel
206Reverse Linked Listprev / cur / nxtEasy
21Merge Two Sorted Listsdummy node + walk two listsEasy
203Remove Linked List Elementsdummy node + skipEasy
876Middle of the Linked Listslow / fastEasy
141Linked List Cycleslow / fast (Floyd)Easy
707Design Linked Listfull doubly list, all edge casesMedium
1472Design Browser Historydoubly list, prev and nextMedium
19Remove Nth Node From Enddummy + two pointers with a gapMedium
2Add Two Numbersdummy + build a new listMedium
1700Number of Students Unable to Eat Lunchqueue simulation with dequeEasy
225Implement Stack using Queuesqueue rotationEasy
143Reorder Listmiddle + reverse + merge (all three moves)Medium

Your next step today: LeetCode 206. Write it from memory, run it, then read the reverse diagram again and compare.

8Self-check

Answer out loud before opening. If you cannot answer in one breath, reread that section.

Q1What are the two fields of a singly linked list node, and what does each hold?

val holds the data. next holds a reference (address) to the next node, or None if this is the last node.

Q2Why is accessing the 100th node O(n) in a linked list but O(1) in an array?

Array elements are contiguous, so the address of index i is start + i × size: one multiplication. Linked list nodes are scattered; the only way to reach node 100 is to follow 99 next pointers.

Q3Write the two lines that append node to a singly linked list that has a tail. Why does the order matter?

tail.next = node then tail = node. If you move tail first, you no longer have a reference to the old last node, so you cannot connect it to the new one.

Q4What does head.next = head.next.next do?

It deletes the second node. head now points at what used to be the third node. The second node becomes unreachable and is garbage collected.

Q5Explain the star in "Insert O(1)*".

The insert itself is one or two pointer changes, O(1). But that assumes you already hold the node just before the insertion point. If you have to walk to it from the head, the walk is O(n).

Q6Deleting the tail: why O(n) in a singly list but O(1) in a doubly list?

To delete the tail you must set the previous node's next to None. A singly list cannot go backwards, so you walk from the head to find that node. A doubly list gives it to you instantly as tail.prev.

Q7Insert node X at the end of a doubly linked list. Write the three lines in the correct order.

tail.next = X, X.prev = tail, tail = X. (The first two can swap; tail = X must be last.)

Q8In dequeue, why do we set self.right = None when self.left becomes None?

We just removed the last node. right still points at it. If we leave it, the next enqueue sees a non-empty queue and attaches after a node that is no longer in the queue, while left stays None. The queue is corrupted.

Q9What happens when you run while cur: on a circular linked list?

Infinite loop. No node has next = None, so cur never becomes None. Detect cycles with slow and fast pointers.

Q10In the reverse-list loop, which line must come first and why?

nxt = cur.next. The next line, cur.next = prev, overwrites the only pointer to the rest of the list. Without saving it, everything after cur is lost.

Q11Which Python tool should you use for a queue, and which method removes from the front?

collections.deque, and popleft(). list.pop(0) is O(n) because every element shifts.

Q12Trace it: list is 1 → 2 → 3 → 4. Run middle(head). What are slow and fast after each loop turn, and what is returned?

Start: slow=1, fast=1. Turn 1: slow=2, fast=3. Turn 2: slow=3, fast=None (4.next). Loop stops because fast is None. Returns node 3, the second middle of an even-length list.