DSA · Visual lesson · Trees & Graphs
Each idea is shown four ways: in plain words, as an everyday picture, as a live diagram you can click and play, and as real Python code. Press Play, click the circles, and watch what happens.
01
A graph is just dots (called nodes or vertices) joined by lines (called edges). That's it. Cities and roads, people and friendships, web pages and links.
A tree is a special graph with two rules: everything is connected, and there are no loops. Because of that, there is exactly one route between any two nodes, and a tree with n nodes always has n − 1 edges.
Folders on your laptop form a tree: each folder lives inside exactly one parent. Add a shortcut from one folder to another and you've made a loop. Now it's a general graph.
# A tree: each node lists its children
tree = {"A": ["B", "C"], "B": ["D", "E"], "C": ["F"]}
# A graph: each node lists its neighbours.
# The extra E-F link creates a loop (a cycle)
graph = {"A": ["B", "C"], "B": ["A", "D", "E"],
"C": ["A", "F"], "D": ["B"],
"E": ["B", "F"], "F": ["C", "E"]}02
Trees borrow words from family trees. Click any node on the right and its relatives light up.
A company org chart. The CEO is the root, managers are parents, people with no reports are leaves, and "depth" is how many bosses sit above you.
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def height(node):
if node is None:
return -1 # so a leaf has height 0
return 1 + max(height(node.left), height(node.right))03
A traversal means visiting every node once, in some order. The four classic orders differ only in when you write down the current node compared with its children.
04
A BST is a binary tree with one promise: everything on the left is smaller, everything on the right is bigger. That promise lets you throw away half the tree at every step when searching.
With 1,000,000 numbers in a balanced BST you need only about 20 comparisons to find one. But if you insert numbers already sorted, the tree becomes a long chain and search gets slow. Try the "sorted input" button to see why balanced trees (AVL, Red-Black) exist.
The number-guessing game. "Is it 50?" "Lower." "25?" "Higher." Each answer cuts the possibilities in half.
def insert(node, val):
if node is None:
return Node(val)
if val < node.val:
node.left = insert(node.left, val)
else:
node.right = insert(node.right, val)
return node
def search(node, val):
while node and node.val != val:
node = node.left if val < node.val else node.right
return node # None means "not found"05
A computer can't see a picture, so we write the graph down. Two common ways:
Edges can also carry a weight: a distance, a cost, a time. Tick "weighted" to see the same graph with numbers on it.
An adjacency list is your phone's contact list: each person, and who they know. A matrix is a giant mileage chart in a road atlas, with a box for every pair of cities.
# Adjacency list
graph = {"A": ["B", "C"], "B": ["A", "C", "D"], ...}
# Weighted adjacency list
graph = {"A": [("B", 4), ("C", 2)], ...}
# Adjacency matrix (rows/cols = A..G)
M = [[0,1,1,0,0,0,0],
[1,0,1,1,0,0,0], ...]
connected = M[0][1] == 1 # is A linked to B?06
Two ways to explore every node reachable from a start point. The only real difference is the waiting line you use.
07
When edges have weights, fewest hops is not the same as cheapest route. A → C → B costs 2 + 1 = 3, cheaper than the direct A → B road of 4.
Dijkstra's algorithm keeps a best-known distance for every node. It repeatedly picks the closest node it hasn't finalised yet, locks it in, and checks whether going through it gives any neighbour a shorter route. Each improvement is called relaxing an edge.
It works only when no weight is negative. The blue numbers above nodes are the current best distances.
Spilling water at the start city. It spreads along roads at a steady speed, and each city gets wet at the exact moment of its shortest distance. The order cities get wet is the order Dijkstra finalises them.
import heapq
def dijkstra(graph, start):
dist = {n: float("inf") for n in graph}
dist[start] = 0
heap = [(0, start)] # (distance, node)
while heap:
d, node = heapq.heappop(heap) # closest unfinished node
if d > dist[node]:
continue # old, stale entry
for nb, w in graph[node]:
if d + w < dist[nb]: # found a shorter route
dist[nb] = d + w
heapq.heappush(heap, (dist[nb], nb))
return dist08
V is the number of nodes, E the number of edges, n the number of items in a tree, h its height.
| Tool | Plain idea | Time | Reach for it when… |
|---|---|---|---|
| Preorder | Parent first, then children | O(n) | Copying a tree, printing a folder structure |
| Inorder | Left, me, right | O(n) | Getting a BST's values in sorted order |
| Postorder | Children first, then parent | O(n) | Deleting a tree, totalling folder sizes |
| Level order | Row by row with a queue | O(n) | Printing by level, nearest-first answers |
| BST search | Go left if smaller, right if bigger | O(h): log n balanced, n worst | Fast lookup while keeping data sorted |
| Adjacency list | Each node keeps its neighbours | Space O(V + E) | Most real graphs (few links per node) |
| Adjacency matrix | V × V grid of yes/no | Space O(V²) | Dense graphs, instant "is A linked to B?" |
| BFS | Spread in rings using a queue | O(V + E) | Fewest hops, shortest path when all edges cost the same |
| DFS | Go deep, back up, try next | O(V + E) | Mazes, finding cycles, connected parts, task ordering |
| Dijkstra | Always finalise the closest node | O((V + E) log V) | Cheapest route with non-negative weights (maps, networks) |