Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Trees, Heaps & Tries  ›  Lesson

Binary Trees & the Genre Tree

Binary Trees 25 minNodes with two children, and the walks that visit them all
You're building a piece ofTunebox — discovery
This piece — genre_paths(): Walks the genre hierarchy from the top down to every sub-genre.
Scenario Tunebox's genre browser opens every branch of the genre tree. It needs each full path — Music > Rock > Punk — to label the shelves.
Your task
Build genre_paths(levels). The genre tree arrives as a level-order list with None for a missing child; build the nodes, walk down from the root, and return every root-to-leaf path joined with " > ", left to right. Example: genre_paths(["Music", "Rock", "Electronic", "Punk", "Metal", None, "House"]) → ["Music > Rock > Punk", "Music > Rock > Metal", "Music > Electronic > House"].

Binary Trees & the Genre Tree

Tunebox's genres aren't a flat list. Music splits into Rock and Electronic; Rock splits into Punk and Metal. A list can hold those names, but it can't say that Punk sits under Rock. A tree can — and once that shape is in memory, one short recursive function reaches every branch.

A node and its two children

A binary tree is made of nodes. Each holds a value and up to two children, left and right; a missing child is None.

python

The top node is the root. A node with no children is a leaf — Punk, Metal, House. Every child starts its own subtree: a smaller tree that follows exactly the same rules. That is why recursion fits trees so well.

❓ Cross-question — "Why not a dict of lists, {"Music": ["Rock", "Electronic"]}?" That's a fine general tree. A binary tree caps each node at two named slots, and the next lessons lean on that cap: a search tree keeps smaller keys left and bigger ones right, and a heap packs its nodes into a flat list by position.

Depth and height

Depth is a node's distance from the root: Music is depth 0, Punk depth 2. Height is how tall the tree is. We count nodes on the longest root-to-leaf path, so an empty tree has height 0 and a lone root height 1. (Some books count edges instead — one fewer. Check which one a problem means.)

Height is recursion in its purest form — a tree is one node taller than its taller subtree:

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

def height(node):
    if node is None:
        return 0
    return 1 + max(height(node.left), height(node.right))

jazz = Node("Jazz", Node("Bebop", None, Node("Hard Bop")), Node("Swing"))
print(height(jazz))

From a level-order list to a tree

Tunebox stores the genre tree as JSON, so it arrives level by level, left to right, with None for a missing child:

["Music", "Rock", "Electronic", "Punk", "Metal", None, "House"]

Read it with a queue. The first value is the root. After that, values come in pairs — the left and right child of each real node, in the order the nodes were made. A None has no children, so it never claims a pair of its own.

python

Every graded function in this section works this way: build the nodes from the list, work on real nodes, return plain values.

Depth-first: pre-, in- and post-order

A depth-first traversal dives all the way down one branch before trying the next. The three classic orders differ only in when the node itself is visited:

  • pre-order — node, left, right. Parents before children, like a table of contents.
  • in-order — left, node, right. In the next lesson's search trees, this is sorted order.
  • post-order — left, right, node. Children before parents: sizes, totals, deletes.

Step through a pre-order walk and watch the calls go down and come back up:

step through it
1class Node:
2 def __init__(self, val, left=None, right=None):
3 self.val, self.left, self.right = val, left, right
4 def __repr__(self):
5 return f"Node({self.val!r})"
6 
7def preorder(node, out):
8 if node is None:
9 return
10 out.append(node.val)
11 preorder(node.left, out)
12 preorder(node.right, out)
13 
14root = Node("Music", Node("Rock", Node("Punk")), Node("Jazz"))
15out = []
16preorder(root, out)
17print(out)

Now move the append to the end:

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

def postorder(node, out):
    if node:
        postorder(node.left, out)
        postorder(node.right, out)
        out.append(node.val)
    return out

root = Node(1, Node(2, Node(4), Node(5)), Node(3))
print(postorder(root, []))

Coming from Java/JS: the recursion is the same — if (node == null) return; becomes if node is None: return. Prefer is None to if not node once values can be falsy, like 0 or "".

Breadth-first: level order with a deque

Level order visits the root, then every node at depth 1, then depth 2 — the same queue trick build used. Take a node from the front, add its children to the back:

python

❓ Cross-question — "Why a deque, not list.pop(0)?" pop(0) shifts every remaining item left — O(n) per pop, O(n²) for the whole walk. deque.popleft() is O(1). You won't notice on seven genres; you will on a million-node tree.

Root-to-leaf paths

The genre browser needs every full path, like Music > Rock > Punk. Walk down carrying the trail so far, and record it when you reach a leaf. The easy mistake is sharing one trail list between every call and never taking anything back off it:

broken — fix it

This should print both paths, but the second comes out as 'Music > Rock > Jazz' — Rock is still on the trail. Fix it.

There are two cures: trail.pop() after the recursive calls (that's backtracking), or pass trail + [node.val] down so each call owns a copy.

Operation Time Extra space
Build from a level-order list O(n) O(n) nodes
Pre-, in-, post-order (recursive) O(n) O(h) call stack
Level order with a deque O(n) O(w), the widest level
Height O(n) O(h)
All root-to-leaf paths O(n · h) O(n · h) for the strings

h is the height: about log₂ n when the tree is bushy, but n when it's one long chain.


Idioms & real-world patterns

Depth-first without recursion

Swap the queue for a stack and you get pre-order. Push the right child first, so the left one is popped first:

python

Trees are everywhere once you look for them: folders on disk, the HTML DOM, and the syntax tree Python builds from your own code, which the ast module hands you.

⚡ Advanced — when recursion runs out of stack

CPython stops recursing at about 1000 frames. A balanced tree of a million nodes is only ~20 levels deep, but a degenerate one — each node with a single child — is as deep as it is long:

python

For input you don't control, use the explicit stack or deque versions.

⚡ Advanced — a traversal as a generator

yield from turns a traversal into a lazy stream you can stop early:

python

🎯 Your turn

Write genre_paths(levels). levels is the genre tree as a level-order list with None for a missing child. Return every root-to-leaf path joined with " > ", left to right:

  • genre_paths(["Music", "Rock", "Electronic", "Punk", "Metal", None, "House"])["Music > Rock > Punk", "Music > Rock > Metal", "Music > Electronic > House"]
  • genre_paths([])[]

Hint — the starter's build(levels) makes the nodes. Recurse with the trail so far, and record it when a node has no children.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox 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-upHow deep does the genre tree go?

Write tree_height(levels) returning the height of the tree — the number of nodes on the longest root-to-leaf path. The tree is a level-order list with None for a missing child; an empty tree has height 0. build(levels) is provided in the starter.

tree-heightbase-case
DrillPre-, in- and post-order in one walk

Write three_orders(levels) returning a dict {"pre": [...], "in": [...], "post": [...]} — the values in pre-order, in-order and post-order. The tree is a level-order list with None for a missing child.

preorderinorderpostorder
BuildThe genre browser, one row per level

Write genres_by_level(levels) returning a list of rows: row 0 holds the root, row 1 every genre at depth 1, and so on, each row left to right. The tree is a level-order list with None for a missing child; an empty tree gives [].

level-orderdeque
BossThe closest shared parent genre

Write common_genre(levels, a, b) returning the name of the lowest genre that has both a and b in its subtree. A genre counts as being in its own subtree, so the answer for "Rock" and "Punk" can be "Rock". Names are unique. If either name is not in the tree, return None.

tree-recursiontree
CapstoneMirror the genre tree

Write mirror_levels(levels) that mirrors the tree — every node's left and right children swap, all the way down — and returns it as a level-order list again: root first, then each real node's left and right child in turn, None for a missing child, with trailing Nones removed. [1, 2, 3, None, 4][1, 3, 2, None, None, 4]. An empty tree gives [].

recursionlevel-order
genre_paths.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
genre_paths(levels) → listEvery path from the top genre down to a sub-genre with nothing beneath it.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.