Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Linked Lists, Stacks & Queues  ›  Lesson

Linked Lists & the Linked Playlist

Linked Lists 25 minNodes joined by references: cheap splicing, no random access
You're building a piece ofTunebox — the player
This piece — splice_tracks(): Inserts and removes tracks without shifting everything after them.
Scenario A listener drags Verse in after Intro and deletes Outro from a long party mix. Tunebox's playlist editor rewires two arrows for each edit instead of shifting every track after them.
Your task
Build splice_tracks(tracks, edits). Build a linked list from tracks, then apply each edit in order: ["insert", after, title] splices title in straight after the first track named after (after = None inserts at the front), and ["remove", title] unlinks the first track with that title. Edits naming a track that isn't there are skipped. Return the final playlist as a list. Example: splice_tracks(["Intro", "Chorus", "Outro"], [["insert", "Intro", "Verse"], ["remove", "Outro"]]) → ["Intro", "Verse", "Chorus"].

Linked Lists & the Linked Playlist

Tunebox keeps a playlist in a Python list, and that works fine until someone drags a track into slot 2 of a 5,000-track party mix. To open that slot, Python shifts 4,998 tracks one place to the right. A linked list holds the same tracks as a chain of nodes, each pointing at the next, so splicing a track in means changing two arrows, however long the playlist is.

Why a list shifts on insert

A Python list is an array: its items sit side by side in one block of memory. That's what makes tracks[4000] instant, because Python can work out where slot 4000 is. It's also why inserting near the front is slow: every item after the gap has to move, so inserting at position 0 of n items moves all n.

python

A node: one value, one arrow

A linked list gives up the single block. Each track lives in its own small object, a node, which holds the value and a reference to the next node:

python

The whole list is just a reference to its first node, the head. No length is stored anywhere, and there is no index. There are only arrows.

Coming from Java/JS: next is an ordinary object reference, exactly like a Node next; field in Java or { val, next } in JS. None plays the part of null.

Every box in this lesson declares Node again so it runs on its own. The fastest way to build a chain is to keep putting a new node in front of the head:

What does this print?
class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

head = None
for title in ["A", "B", "C"]:
    head = Node(title, head)
print(head.val)

Walking the chain

With no index, the only way through is to follow the arrows. This loop is inside almost every linked-list function, and these two helpers convert between plain lists and chains for the rest of the course:

python

Walking also means no random access. To reach index i you take i hops, so reading tracks[4000] goes from O(1) on a list to O(n) on a chain.

What does this print?
class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

head = Node("A", Node("B", Node("C")))
cur, hops = head, 0
while cur.next:
    cur = cur.next
    hops += 1
print(cur.val, hops)

Splicing in O(1)

This is what you get in return. Once you hold a node, inserting after it or deleting the node after it changes one or two arrows, and nothing shifts:

python

The order of the two assignments matters. The new node has to take its arrow before the old arrow gets overwritten:

broken — fix it

This should splice Bridge in after Intro and print all four tracks. It prints Bridge over and over instead.

❓ Cross-question — "O(1)? You still had to find Intro first." True, and that walk is O(n). The splice is O(1) only when you already hold the node. A player usually does, since it holds the node that's playing, so "play this next" needs no walk. Searching by title costs O(n) to find plus O(1) to splice.

The dummy head

Deleting the first node is awkward. There's no node before it to rewire, so the head itself has to change, and every function ends up with an extra if. Put a dummy node in front of the head and every real node has a predecessor:

python

Reversing in place

Reversing a chain copies nothing. Walk it once and flip each arrow to point backwards. prev holds the part already reversed, cur is the node being flipped, and nxt saves the rest before the flip cuts it off. Step through it:

step through it
1class Node:
2 def __init__(self, val, next=None):
3 self.val = val
4 self.next = next
5 def __repr__(self):
6 return f"Node({self.val})"
7 
8head = Node(1, Node(2, Node(3)))
9prev, cur = None, head
10while cur:
11 nxt = cur.next # save the rest
12 cur.next = prev # flip one arrow
13 prev = cur # the reversed part grows
14 cur = nxt # move on
15print(prev.val, prev.next.val, prev.next.next.val)

It takes one pass, so O(n) time, and O(1) extra memory. Once the idea is clear, you can write the loop body as cur.next, prev, cur = prev, cur, cur.next.

Fast and slow pointers

Two walkers moving at different speeds can answer questions a single walk can't. Move slow one hop and fast two hops per step. When fast runs out of chain, slow is at the middle.

The same trick finds a loop. If a bug links the last track back into the middle of the playlist, a plain walk never ends. With two speeds, fast eventually laps slow and both land on the same node. This is Floyd's cycle detection:

python

❓ Cross-question — "Why not keep a set of the nodes I've visited?" That works too, but the set costs O(n) memory. Two pointers find the loop in O(n) time with O(1) memory, and that saving is the only reason the trick exists.

What it costs

operation Python list singly linked list
read index i O(1) O(n): walk i hops
insert / delete at the front O(n): everything shifts O(1)
insert / delete after a node you hold O(n) O(1)
append at the end O(1) amortized O(n), or O(1) if you keep a tail
find a value O(n) O(n)
memory per item one reference a whole object: value plus next

Idioms & real-world patterns

  • Convert only at the edges. Build the chain from a plain list, work on nodes, and turn it back into a list at the end. That's how every graded function in this course works.
  • Pick the loop condition deliberately. while cur: visits every node, while cur.next: stops on the tail, and while fast and fast.next: checks before a double hop.
  • Use a dummy whenever the head might change.
  • Where they live: Python's deque is a doubly linked list of small blocks, a Git commit points to its parent, and many hash tables chain their collisions.

❓ Cross-question — "So should Tunebox store titles in one?" Not a plain list of titles: a Python list is more compact and faster to loop over. Linked lists pay off when you hold node references and splice around them constantly, as an LRU cache does.

⚡ Advanced — doubly linked lists and LRU caches

Give each node a prev arrow as well, and a node can unlink itself in O(1), without anyone walking to its predecessor first. The cost is one more reference per node and two arrows to keep in step on every change. Real implementations also put a stand-in node (a sentinel) at both ends, so prev and next are never None:

python

That self-unlinking is what an LRU cache is built on. Tunebox keeps recently played tracks for offline listening and, when the cache is full, evicts the least recently used one. A dict from title to node finds a track in O(1). A doubly linked list in recency order moves that node to the fresh end, or drops the stale end, in O(1). Python ships the pairing as collections.OrderedDict, where move_to_end(key) and popitem(last=False) are those two moves, and functools.lru_cache uses it to remember a function's recent results.


🎯 Your turn

Build splice_tracks(tracks, edits), the linked playlist behind Tunebox's playlist editor. Build a chain from tracks, then apply each edit in order:

  • ["insert", after, title] splices title in straight after the first track named after. When after is None, the track goes at the front.
  • ["remove", title] unlinks the first track named title.

Skip any edit whose track isn't in the playlist, and return the final playlist as a list:

  • splice_tracks(["Intro", "Chorus", "Outro"], [["insert", "Intro", "Verse"], ["remove", "Outro"]])["Intro", "Verse", "Chorus"]
  • splice_tracks(["B", "C"], [["insert", None, "A"]])["A", "B", "C"]

Hint: put a dummy node in front of the chain. Inserting at the front then becomes inserting after the dummy, and removing the head needs no special case.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of the app is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upWalk to a track

A linked playlist has no index, so reaching a position means walking to it. Write track_at(tracks, i): build a linked list from tracks, walk i hops from the head, and return that node's title. Return None if i is negative or past the end. track_at(["A", "B", "C"], 2)"C".

linked-listnode
DrillPull every copy

A track was pulled from the catalogue. Write remove_all(tracks, title) that unlinks every node holding title from a linked list built from tracks, then returns what's left, in order. Use a dummy head so a match at the front isn't a special case. remove_all(["X", "A", "X", "X", "B"], "X")["A", "B"].

dummy-headpointer-rewire
BuildRotate the playlist

Write rotate_right(tracks, k): move the last k tracks to the front, keeping their order, by rewiring a linked list rather than slicing. k can be larger than the playlist, and rotating by the playlist's length changes nothing. rotate_right(["A", "B", "C", "D", "E"], 2)["D", "E", "A", "B", "C"].

pointer-rewirelinked-list
BossDrop the n-th from the end

Write drop_from_end(tracks, n): remove the n-th track counting from the end (n = 1 is the last track) and return what's left. Do it in one pass over a linked list, without counting its length first. n is always between 1 and the number of tracks. drop_from_end(["A", "B", "C", "D"], 2)["A", "B", "D"].

fast-slow-pointersdummy-head
CapstoneThe bookends mix

The bookends mix alternates between the two ends of a playlist: first, last, second, second-to-last, and so on. Write bookend_mix(tracks) that rearranges a linked list in place, without copying values into another list, and returns the result. bookend_mix([1, 2, 3, 4, 5])[1, 5, 2, 4, 3].

fast-slow-pointersreverse-listpointer-rewire
splice_tracks.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
splice_tracks(tracks, edits) → listThe playlist after every splice, in order.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.