Types of Binary Trees: Full, Complete, Perfect and Balanced
8 min readBytePatterns
Binary tree types explained: full vs complete vs perfect vs balanced vs degenerate, how to test each one, the height bounds, and why heaps are complete.
A binary tree is simple to define: every node has at most two children, a left one and a right one. The vocabulary built on top of it is where people stumble. "Full", "complete", "perfect" and "balanced" sound like synonyms; they are not, and interview questions depend on the difference: a heap must be complete, a search tree is fast only when balanced, and "check whether this tree is complete" is a question in its own right. This article defines each type, tests for it, and checks the tests against every small tree shape.
The problem it solves
The shape of a tree decides what you can do with it:
- Height decides speed. Searching, inserting and deleting in a binary search tree cost
O(h), wherehis the height. A balanced tree keepshnearlog₂ n; a degenerate one lets it reachn - 1. - Completeness decides storage. A complete tree fits in an array with no gaps, which is exactly how binary heaps are stored.
- Fullness and perfection give counting formulas for proofs and quick sanity checks.
The intuition
Start from the lesson's definition. Each node has two child slots, left and right; either may be empty (None in Python), and the two are positions, not an unordered pair, so swapping them makes a different tree. With that in place, the five named shapes are:
- Full (also called proper or strict): every node has zero or two children, never one. In any full tree, leaves outnumber internal nodes by exactly one.
- Complete: every level is full except possibly the last, and the last level is filled from the left with no gaps. Number the nodes level by level from 0, and a complete tree with
nnodes uses exactly the numbers0ton - 1; nodeihas its children at2i + 1and2i + 2. - Perfect: every internal node has two children and every leaf sits on the same level. A perfect tree of height
hhas exactly2^(h+1) - 1nodes. - Balanced (height-balanced): at every node, the heights of the two subtrees differ by at most one. This is the property that keeps height at
O(log n), and the one AVL trees maintain. - Degenerate: every internal node has one child. It is a linked list wearing a tree's clothes, with height
n - 1.
The types nest: every perfect tree is full, complete and balanced, and every complete tree is balanced, but neither a full nor a balanced tree need be complete. Textbooks disagree on names, and some older ones say "complete" for what is called perfect here, so in an interview, state your definition before you code.
Watch it run
The animation uses the lesson's tree, 1 with children 2 and 3, and 4 and 5 under 2. A binary node has exactly two child slots, left and right, never three. Node 2 fills both of its slots, 4 on the left and 5 on the right. Node 3 fills neither: both slots exist and both hold None. Swap 4 and 5 and this is a different tree; the two slots are positions, not an unordered pair. Put them back, and counting the tree is one line of recursion, where an empty slot contributes 0. The count returns in postorder: each real node returns 1 + left + right, 3 returns 1 + 0 + 0 = 1 because both its children are empty, and five nodes, each visited once, give count(root) = 5. That tree is full, complete and balanced, but not perfect: 3's slots are empty while 2's are not.
Binary Trees
Step 1 of 11
A binary node has exactly two child slots: left and right. Never three.
The same interactive animation as the lesson — step through it with the controls.
The code
One test per type. The completeness test walks level by level and fails if a real node ever appears after an empty slot; balance is checked in one post-order pass that returns None as soon as any subtree is unbalanced:
from collections import deque
class Node:
def __init__(self, v, l=None, r=None):
self.val, self.left, self.right = v, l, r
def count(n):
return 0 if n is None else 1 + count(n.left) + count(n.right)
def height(n):
return -1 if n is None else 1 + max(height(n.left), height(n.right))
def is_full(n):
if n is None:
return True
if (n.left is None) != (n.right is None): # exactly one child
return False
return is_full(n.left) and is_full(n.right)
def is_complete(root):
q, seen_gap = deque([root]), False
while q:
node = q.popleft()
if node is None:
seen_gap = True # every slot after this must be empty
elif seen_gap:
return False
else:
q.extend([node.left, node.right])
return True
def is_perfect(root):
return count(root) == 2 ** (height(root) + 1) - 1
def is_balanced(root):
def check(n): # height, or None if unbalanced below
if n is None:
return -1
l, r = check(n.left), check(n.right)
if l is None or r is None or abs(l - r) > 1:
return None
return 1 + max(l, r)
return check(root) is not None
def kinds(root):
return [name for name, test in [("full", is_full), ("complete", is_complete),
("perfect", is_perfect), ("balanced", is_balanced)] if test(root)]
lesson = Node(1, Node(2, Node(4), Node(5)), Node(3))
print(count(lesson), height(lesson), kinds(lesson))
# 5 2 ['full', 'complete', 'balanced']
print(kinds(Node(1, Node(2, Node(4), Node(5)), Node(3, Node(6), Node(7)))))
# ['full', 'complete', 'perfect', 'balanced']
print(kinds(Node(1, Node(2, None, Node(4)), Node(3)))) # ['balanced']
print(kinds(Node(1, Node(2, Node(3, Node(4)))))) # [] degenerate: a chain
Every shape with n nodes, generated by choosing how many nodes go left. The shape counts are the Catalan numbers, and very few shapes are complete: exactly one per n.
def shapes(n):
"""Every binary tree shape with n nodes (values are not part of the shape)."""
if n == 0:
return [None]
return [Node(0, l, r) for k in range(n)
for l in shapes(k) for r in shapes(n - 1 - k)]
for n in range(1, 8):
all_n = shapes(n)
tally = [sum(1 for t in all_n if test(t))
for test in (is_full, is_complete, is_perfect, is_balanced)]
print(n, len(all_n), tally)
# 1 1 [1, 1, 1, 1]
# 2 2 [0, 1, 0, 2]
# 3 5 [1, 1, 1, 1]
# 4 14 [0, 1, 0, 4]
# 5 42 [2, 1, 0, 6]
# 6 132 [0, 1, 0, 4]
# 7 429 [5, 1, 1, 17]
Checked against brute-force definitions on every shape up to 9 nodes (6,917 trees) and 2,000 seeded random trees of up to 60 nodes: completeness against heap numbering (the largest index must be n - 1), fullness against the leaf count, perfection against "all leaves at one depth, no single children", balance against a slow check that recomputes heights at every node, plus the implications above:
import math
import random
def nodes_with_index(root):
out, stack = [], [(root, 0, 0)]
while stack:
n, i, d = stack.pop()
if n:
out.append((n, i, d))
stack += [(n.left, 2 * i + 1, d + 1), (n.right, 2 * i + 2, d + 1)]
return out
def brute(root):
nodes = nodes_with_index(root)
kids = [(n.left is not None) + (n.right is not None) for n, _, _ in nodes]
leaf_depths = {d for (n, _, d), k in zip(nodes, kids) if k == 0}
return {
"full": kids.count(0) == kids.count(2) + 1 and 1 not in kids,
"complete": max(i for _, i, _ in nodes) == len(nodes) - 1,
"perfect": len(leaf_depths) == 1 and 1 not in kids,
"balanced": all(abs(height(n.left) - height(n.right)) <= 1 for n, _, _ in nodes),
}
def random_tree(rng, n):
root = Node(0)
for _ in range(n - 1):
node = root
while True: # random walk down to an empty slot
side = "left" if rng.random() < 0.5 else "right"
if getattr(node, side) is None:
setattr(node, side, Node(0))
break
node = getattr(node, side)
return root
def complete_tree(n):
nodes = [Node(0) for _ in range(n)]
for i in range(n):
if 2 * i + 1 < n: nodes[i].left = nodes[2 * i + 1]
if 2 * i + 2 < n: nodes[i].right = nodes[2 * i + 2]
return nodes[0]
rng = random.Random(31)
trees = [t for n in range(1, 10) for t in shapes(n)]
trees += [random_tree(rng, rng.randint(1, 60)) for _ in range(1_500)]
trees += [complete_tree(rng.randint(1, 60)) for _ in range(500)]
ok = True
for t in trees:
b, n, h = brute(t), count(t), height(t)
ok &= b == {k: k in kinds(t) for k in b}
ok &= math.floor(math.log2(n)) <= h <= n - 1 # the height bounds
ok &= (not b["perfect"] or b["full"] and b["complete"]) # perfect => full, complete
ok &= (not b["complete"] or b["balanced"]) # complete => balanced
print(len(trees), ok) # 8917 True
The complexity
- Every test is
O(n)time. The completeness test usesO(w)extra space for the widest level; the recursive ones useO(h)stack. - Height is between
⌊log₂ n⌋andn - 1, and that range is the whole story of balanced versus degenerate trees. - Naive balance checking, recomputing heights at every node, is
O(n²)on a chain, which is why the one-pass version returns heights and a failure flag together. The balanced binary tree article walks through it.
Where it goes wrong
- Mixing up full and complete. A full tree can have a long right spine with gaps on the last level; a complete tree can have a node with one child, as the lesson's tree with
6added under3on the left would show. - Checking only the root for balance. Balance is a property of every node, not just the root.
- Stopping the completeness scan at the first
None. The test must keep draining the queue; a real node after a gap is exactly the failure case. - Assuming
2^(h+1) - 1for any tree. That is the maximum for heighth, reached only by perfect trees.
When it shows up in interviews
As direct questions, "check completeness of a binary tree" and "balanced binary tree", and as the unstated assumption behind heaps (complete, stored in an array), binary search trees (fast only when balanced) and level-order traversal, which is the completeness test's engine.
How to say it in an interview
"A binary tree gives each node a left and a right slot. Full means no node has exactly one child; complete means every level is filled except the last, which fills from the left, so it maps onto an array with no gaps, which is why heaps are complete; perfect means complete and full with every leaf on one level, 2 to the h plus 1 minus 1 nodes; balanced means subtree heights differ by at most one at every node, which keeps height logarithmic. To check completeness I'd do a BFS that enqueues empty slots too and fails if a real node appears after the first empty one, O(n) time."