// STUHUB · C++ DATA STRUCTURES
Tree Terminology and Binary Tree Shapes: Depth, Height, Full vs Complete
Every tree term on one worked example — root, leaf, degree, siblings, internal node, path length, ancestor, subtree — plus the level-versus-depth-versus-height distinction, the edge-count versus node-count height clash that makes two correct answers differ by one, the difference between full, perfect, complete, proper, balanced and skewed trees, and the counting formulas (edges = n - 1, 2^d nodes at depth d, 2^(h+1) - 1 nodes at height h, minimum height floor(log2 n)). With three compiled C++ programs that measure height, depth, node and leaf counts and test the shape properties.
Introduction
Trees are where a data-structures course stops being about one thing after another and starts being about shape. The vocabulary arrives all at once — root, leaf, degree, sibling, ancestor, level, depth, height, full, complete, balanced, skewed — and most of it is easy. The trouble is that three or four of those words are defined slightly differently by different sources, and the differences are all worth exactly one.
The worst offender is height. One definition counts edges, so a single node has height 0. The other counts levels, so a single node has height 1. Both are in wide use, both show up in the same set of notes, and a recursive height() function written under one convention returns the other convention's answer. Two people can do identical, correct work and write down answers that differ by one. That is not a footnote; it is the single most productive source of lost marks in this topic, so it gets its own section below, along with a rule for writing answers that survive either convention.
This page is the vocabulary and the arithmetic: what every term means on one worked example, how level, depth and height relate, what separates a full tree from a complete one from a perfect one, and the handful of counting formulas (edges = n - 1, 2^d nodes at depth d, 2^(h+1) - 1 nodes in a tree of height h) that turn shape questions into one line of arithmetic. Three C++ programs run through it; all three compile clean under g++ -std=c++17 -Wall -Wextra, and every number quoted in the prose is a number one of them printed.
One tree, every term on it
A tree is a set of nodes connected by edges in a hierarchy: one node at the top, every other node hanging below exactly one parent, no cycles. That is the whole structure. It is the right shape whenever your data has ancestors and descendants rather than a sequence — file systems, org charts, XML and JSON documents, expression trees, decision trees.
Here is the tree the rest of this section refers to. It is deliberately not binary: the general terms apply to any tree, and using a three-child root keeps you from quietly assuming things that are only true of binary trees.
(A) <- level 1 | depth 0 | root
/ | \
(B) (C) (D) <- level 2 | depth 1
/ \ \
(E) (F) (G) <- level 3 | depth 2
\
(H) <- level 4 | depth 3Eight nodes, seven edges. That is not a coincidence: a tree with n nodes has exactly n - 1 edges, because every node except the root is reached by exactly one edge from its parent. Count the edges of a drawn tree, add one, and you have the node count — a free consistency check on any tree you have just drawn yourself.
| Term | What it means | In the tree above |
|---|---|---|
| Root | The single top node, the only one with no parent | A |
| Node | One element of the tree, holding a value | A through H, eight of them |
| Edge (branch) | A link joining a parent to a child | A-B, D-G, G-H, seven in all |
| Parent | The node directly above and connected | the parent of H is G |
| Child | A node directly below and connected | the children of A are B, C, D |
| Siblings | Nodes sharing the same parent | B, C, D are siblings; so are E and F; G has none |
| Leaf | A node with no children | E, F, C, H |
| Internal node | A node with at least one child | A, B, D, G |
| Subtree | A node together with all of its descendants | the subtree at D is {D, G, H} |
| Path | The sequence of nodes walked along edges from one node to another | A -> D -> G -> H |
| Path length | The number of edges on that path, one fewer than the number of nodes | the path A to H has length 3 |
| Ancestor | Every node on the path up from a node to the root | the ancestors of H are G, D, A |
| Descendant | Every other node in a node's subtree | the descendants of D are G and H |
| Degree of a node | How many children it has | deg(A) = 3, deg(B) = 2, deg(G) = 1, deg(C) = 0 |
| Degree of the tree | The largest degree of any node in it | 3, from A |
| Level | Position counting down from the root | H is on level 4 |
| Depth of a node | Edges from the root down to it, root = 0 | depth(G) = 2, depth(H) = 3 |
| Height of a node | Edges from it down to its deepest leaf, leaf = 0 | height(B) = 1, height(D) = 2, height(C) = 0 |
| Height of the tree | The height of the root, i.e. the longest root-to-leaf path in edges | 3, along A-D-G-H |
Two entries in that table are the ones people get wrong under time pressure. Leaf is defined by having no children, not by being on the bottom row — C is a leaf even though it sits two levels above H. And degree counts children only, which is not what degree means in graph theory, where it counts every incident edge. Bring the graph definition to a tree question and every internal node's answer is one too high.
Level, depth, height: two of them count down, one counts up
These three are the same idea measured from different ends, and mixing them up is the second-most-common way to lose a mark here.
| Measured from | Root value | Leaf value | Direction | |
|---|---|---|---|---|
| Level | the top | 1 (commonly; sometimes 0) | varies | downward |
| Depth | the top | 0 | varies | downward |
| Height | the bottom | the tree's height | 0 | upward |
Depth looks down, height looks up. Depth of a node asks how far did I come from the root; height of a node asks how far can I still fall. They agree at the root — the root's depth is 0 and the root's height is the tree's height — and they are opposites at a leaf: a deep leaf has large depth and height 0.
The depth of the tree (as opposed to a node) is the depth of its deepest leaf, which is the same number as the tree's height. So "depth of the tree" and "height of the tree" are two names for one quantity, while "depth of a node" and "height of a node" are genuinely different. Read which one the question asked for.
Level versus depth is a numbering choice, not a concept. Many sources put the root on level 1; many others put it on level 0, in which case level and depth are the same thing. Where the root is level 1, the relationship is simply:
depth(x) = level(x) - 1This matters when you plug into a formula. The maximum number of nodes on level L with a level-1 root is 2^(L-1); the maximum number at depth d is 2^d. Same fact, two numberings, and using the level number in the depth formula doubles your answer. When a question numbers the levels on the diagram, use its numbering; when a question says "depth" or "height starts from 0", you are counting edges.
The height clash: edges or nodes, and why your code disagrees with your notes
Now the big one. Two definitions of height are in circulation:
- Height in edges. "The length of the longest path, measured in edges, from the root to a leaf." A leaf has height 0. A single-node tree has height 0. An empty tree is usually given height -1.
- Height in nodes (levels). Count the nodes on that longest path instead. A leaf has height 1. A single-node tree has height 1. An empty tree has height 0.
On any non-empty tree, the node count is exactly one more than the edge count. That is the entire disagreement, and it is why two people who both understand trees perfectly will submit answers that differ by one.
What makes this genuinely dangerous rather than merely annoying is that the standard recursive implementation uses the node convention while the standard written definition uses the edge convention — and they usually appear on the same page. This function:
int height(TreeNode* node) {
if (node == nullptr) return 0;
return 1 + max(height(node->left), height(node->right));
}returns 1 for a single node and 3 for a tree drawn with three rows, because its base case gives the empty tree 0. It is counting levels. If you want the edge count from the same recursion, the base case has to be -1 instead — then the leaf case comes out 1 + max(-1, -1) = 0, which is what the written definition demands.
The program below runs both on one tree so you can see the offset directly. Note the -1 base case in heightInEdges; that single character is the whole difference between the two functions.
How to answer so that neither convention can cost you. State the units. Write height = 2 (edges); 3 if counting levels rather than a bare 2. It takes four seconds, it is not hedging — it is the same information expressed unambiguously — and it is correct under either marking scheme. If the question says "height starts from 0", or defines height as a path length, it means edges. If the question hands you a height() function and asks what it returns, trace the code, not the definition.
// The same tree, two height conventions, answers one apart.
#include <algorithm>
#include <iostream>
struct Node {
char data;
Node* left = nullptr;
Node* right = nullptr;
explicit Node(char d) : data(d) {}
};
// Edges: "the longest path, measured in edges, from the root to a leaf".
// A single node has no path at all, so its height is 0, and the empty tree
// must return -1 for the recursion to produce that.
int heightInEdges(const Node* n) {
if (n == nullptr) return -1;
return 1 + std::max(heightInEdges(n->left), heightInEdges(n->right));
}
// Nodes: count the levels instead. Empty tree 0, single node 1.
// This is the version most lecture slides print.
int heightInNodes(const Node* n) {
if (n == nullptr) return 0;
return 1 + std::max(heightInNodes(n->left), heightInNodes(n->right));
}
int main() {
Node* root = new Node('A');
root->left = new Node('B');
root->right = new Node('C');
root->left->left = new Node('D');
root->left->right = new Node('F');
root->right->left = new Node('E');
root->right->right = new Node('G');
Node solo('X');
std::cout << "A(B(D,F), C(E,G)) edges=" << heightInEdges(root)
<< " nodes=" << heightInNodes(root) << "\n"; // edges=2 nodes=3
std::cout << "single node edges=" << heightInEdges(&solo)
<< " nodes=" << heightInNodes(&solo) << "\n"; // edges=0 nodes=1
std::cout << "empty tree edges=" << heightInEdges(nullptr)
<< " nodes=" << heightInNodes(nullptr) << "\n"; // edges=-1 nodes=0
delete root->left->left; delete root->left->right;
delete root->right->left; delete root->right->right;
delete root->left; delete root->right; delete root;
}Binary trees, and why left and right are not interchangeable
A binary tree is a tree in which every node has at most two children, and — this is the part that carries weight — the two children are distinguished: one is the left child, one is the right child. The definition is recursive: a binary tree is either empty, or a node with a left subtree and a right subtree, each of which is itself a binary tree. That recursion is why almost every tree function you will write is three lines: handle the empty case, recurse left, recurse right.
Because the two slots are named, these are two different binary trees, not one tree drawn twice:
A A
/ \
B BAs unordered trees they are identical — a root with one child. As binary trees they are distinct, they store differently in an array, they print differently under in-order traversal, and in a binary search tree they mean opposite things about B's value. Any answer that treats "has one child" as a complete description of a node is missing half the information.
Two consequences worth keeping:
- A node in a binary tree has degree 0, 1 or 2. A binary tree's tree degree is therefore at most 2.
- Reconstructing a binary tree from traversals needs the in-order sequence. Pre-order plus post-order cannot tell an only-left-child apart from an only-right-child, because it is exactly this left/right distinction that they both fail to record.
Full, perfect, complete: the three shapes, and the word that means two things
This is the vocabulary that shows up in multiple-choice questions, and one of the words is genuinely ambiguous across sources. Learn all three shapes and label them by property rather than by name, and the ambiguity stops mattering.
Perfect. Every level is completely filled, right down to the bottom. All leaves sit at the same depth. A perfect tree of height h (edges) has exactly 2^(h+1) - 1 nodes — 1, 3, 7, 15, 31 — and no other node count is possible.
Perfect: h = 2 (edges), n = 7 = 2^3 - 1
(A)
/ \
(B) (C)
/ \ / \
(D) (E) (F) (G)Complete. Every level is filled except possibly the last, and the last level is filled from the left with no gaps. The test is mechanical: number the nodes level by level, left to right, starting at 1; the tree is complete exactly when the numbers run 1, 2, 3, ... with nothing skipped. That is the same numbering used to store a tree in an array (left(i) = 2i, right(i) = 2i + 1), which is why completeness is the property heaps care about — a complete tree stores in a flat array with no holes.
Complete but not perfect: n = 6
(A)
/ \
(B) (C)
/ \ /
(D) (E) (F) last level fills left to right, no gapProper (also called strictly binary). Every node has either 0 or 2 children — no node with a single child. Says nothing about depth: a proper tree can be wildly lopsided as long as no node has exactly one child.
Proper but neither perfect nor complete:
(A)
/ \
(B) (C)
/ \
(D) (E)
/ \
(F) (G)Now the ambiguity. The word full is used for both of these. Some sources define a full binary tree as one where every level is filled — that is perfect, and those sources are the ones that write "a full binary tree of height h has exactly 2^(h+1) - 1 nodes". Other sources define full as every node having 0 or 2 children — that is proper. Both usages are common; you cannot settle it by being clever. What you can do is notice which definition your question is using, usually from a formula printed nearby: if a node-count formula like 2^(h+1) - 1 appears next to the word, it means perfect.
The containments are worth knowing cold:
- Perfect implies complete, and perfect implies proper.
- Complete does not imply proper: the complete tree above has a node with exactly one child if you add one more node on the left of the last level.
- Proper does not imply complete: the proper tree above has a gap under
B. - Complete does not imply perfect: the six-node tree above is the counterexample.
Balanced, skewed, degenerate: the shapes that decide your complexity
The previous section was about fullness. This one is about lopsidedness, and it is the part that actually affects running time.
Balanced. For every node, the heights of its left and right subtrees differ by at most 1. A balanced tree of n nodes has height O(log n), so any operation that walks one root-to-leaf path — a BST search, insert or delete — is O(log n).
Skewed / degenerate. Every node has at most one child, so the tree is a chain. A right-skewed tree leans entirely right, a left-skewed one entirely left, and both have height n - 1:
right-skewed left-skewed
(A) (D)
\ /
(B) (C)
\ /
(C) (B)
\ /
(D) (A)A degenerate tree is a linked list wearing tree pointers. Every operation that was going to be O(log n) is now O(n), and you are paying two pointers per node for the privilege. This is not a hypothetical: inserting already-sorted data into an ordinary binary search tree produces exactly this shape. Insert 1, 2, 3, 4, 5 in order and every value goes right, forever. The worst case of an unbalanced BST is not adversarial input — it is tidy input, which is why self-balancing trees (AVL, red-black) exist.
The same contrast shows up in array storage. Storing a tree by the level-order numbering needs an array of length about n + 1 when the tree is complete, but 2^n when it is skewed, because the numbering leaves an exponentially growing hole beside the chain. Array storage is excellent for complete trees and catastrophic for skewed ones; linked storage costs the same either way.
The counting formulas
Everything below assumes height in edges, root at depth 0 — the convention the formulas are written for. If you are working in levels, convert first; do not put a level number into a depth formula.
| Quantity | Formula | Check |
|---|---|---|
Edges in any tree of n nodes | n - 1 | 8 nodes, 7 edges |
Max nodes at depth d | 2^d | d = 0 gives 1, d = 2 gives 4 |
Max nodes on level L (root = level 1) | 2^(L-1) | level 1 gives 1, level 3 gives 4 |
Max nodes in a binary tree of height h | 2^(h+1) - 1 | h = 4 gives 31 |
Min nodes in a binary tree of height h | h + 1 | h = 2 gives 3, a chain of three |
Min height for n nodes | floor(log2(n)), equivalently ceil(log2(n+1)) - 1 | n = 23 gives 4 |
Max height for n nodes | n - 1 | n = 7 gives 6, a chain of seven |
Three of these deserve a sentence of why, because a remembered formula you cannot re-derive is a formula you will misremember at the wrong moment.
2^d at depth d. Depth 0 holds the root alone. Each node has at most two children, so each level can at most double the one above it: 1, 2, 4, 8. Summing that geometric series from depth 0 to depth h gives 2^(h+1) - 1, which is where the max-nodes formula comes from — it is not a separate fact to memorise.
h + 1 minimum. To reach height h you need h edges below the root, and the cheapest way to buy h edges is a chain of h + 1 nodes. Any extra node is optional.
The two min-height expressions are the same number. floor(log2 n) and ceil(log2(n+1)) - 1 agree for every n >= 1; the second is the more obviously derived form (find the smallest h with 2^(h+1) - 1 >= n), the first is the one people quote. The program below prints the tables and brute-force checks that the two expressions match for the first hundred thousand values of n, which is more reassurance than the algebra gives most people at 11pm.
Worked examples, straight off the table: max nodes when h = 4 is 2^5 - 1 = 31; min nodes when h = 2 is 3; min height for n = 23 is 4 (since h = 3 holds only 15 nodes and h = 4 holds up to 31); max height for n = 7 is 6.
// The counting formulas, and a check that the two min-height expressions agree.
#include <cmath>
#include <iostream>
int maxNodesAtDepth(int d) { return 1 << d; } // 2^d
int maxNodesOfHeight(int h) { return (1 << (h + 1)) - 1; } // 2^(h+1) - 1
int minNodesOfHeight(int h) { return h + 1; } // one chain
// Both of these are "the height of the shallowest binary tree holding n nodes",
// measured in edges. They agree for every n >= 1.
int minHeightCeil(int n) {
int h = 0;
while (maxNodesOfHeight(h) < n) ++h; // smallest h with 2^(h+1)-1 >= n
return h;
}
int minHeightFloor(int n) {
return static_cast<int>(std::floor(std::log2(static_cast<double>(n))));
}
int main() {
std::cout << "d : max nodes at that depth\n";
for (int d = 0; d <= 4; ++d)
std::cout << " " << d << " -> " << maxNodesAtDepth(d) << "\n";
std::cout << "h : min nodes .. max nodes\n";
for (int h = 0; h <= 4; ++h)
std::cout << " " << h << " -> " << minNodesOfHeight(h)
<< " .. " << maxNodesOfHeight(h) << "\n";
std::cout << "n : min height, max height\n";
for (int n : {1, 2, 3, 7, 8, 23, 100}) {
std::cout << " " << n << " -> " << minHeightCeil(n)
<< ", " << (n - 1) << "\n";
}
bool agree = true;
for (int n = 1; n <= 100000; ++n)
if (minHeightCeil(n) != minHeightFloor(n)) { agree = false; break; }
std::cout << "ceil(log2(n+1))-1 == floor(log2 n) for n in [1,100000]? "
<< (agree ? "yes" : "no") << "\n";
}Measuring a tree in C++
One program that computes everything discussed above: both heights, the depth of a named node, node and leaf counts, and the three shape predicates. Read it as the executable form of the definitions — each function is short precisely because each definition is recursive.
Points worth pausing on:
heightEdgesreturns-1onnullptrso a leaf comes out 0;heightNodesreturns0so a leaf comes out 1. Same recursion, different base case.depthOfsearches the whole tree, because in a general binary tree there is no way to know which branch holds the key. In a BST you would compare and descend one side, making itO(h)instead ofO(n).isPerfectdoes not walk the shape at all. It measures the height, computes2^(h+1) - 1, and compares against the node count — the formula is the test.isCompleteuses the array numbering: give the root index 1 and each child2i/2i + 1, then a tree ofnnodes is complete exactly when no index exceedsn. A gap anywhere pushes some later index past the count.isProperchecks that a node's two child pointers are either both null or both non-null;(a == nullptr) != (b == nullptr)is the compact way to say "exactly one of them is null".
The program runs the three trees drawn earlier and prints this:
perfect tree A(B(D,F), C(E,G))
nodes = 7
edges = 6
height (edges) = 2
height (nodes) = 3
leaves = 4
proper (0 or 2) = yes
perfect = yes
complete = yes
complete-not-perfect A(B(D,E), C(F,_))
nodes = 6
edges = 5
height (edges) = 2
height (nodes) = 3
leaves = 3
proper (0 or 2) = no
perfect = no
complete = yes
right-skewed A-B-C-D
nodes = 4
edges = 3
height (edges) = 3
height (nodes) = 4
leaves = 1
proper (0 or 2) = no
perfect = no
complete = no
depths in the perfect tree: A=0 B=1 G=2
height of node B (edges) = 1
height of leaf D (edges) = 0Notice the skewed tree: four nodes and height 3, versus the perfect tree's seven nodes and height 2. More nodes, shorter tree. That gap is the whole argument for balancing.
// Measuring a binary tree: height, depth, counts, and shape predicates.
#include <algorithm>
#include <iostream>
#include <queue>
#include <string>
#include <utility>
struct Node {
char data;
Node* left = nullptr;
Node* right = nullptr;
explicit Node(char d) : data(d) {}
};
// Height in EDGES. Empty tree is -1 so that a single node comes out as 0,
// which is what "longest root-to-leaf path, measured in edges" requires.
int heightEdges(const Node* n) {
if (n == nullptr) return -1;
return 1 + std::max(heightEdges(n->left), heightEdges(n->right));
}
// Height in NODES (levels). This is the shape most textbooks print, and it is
// exactly heightEdges + 1 on any non-empty tree.
int heightNodes(const Node* n) {
if (n == nullptr) return 0;
return 1 + std::max(heightNodes(n->left), heightNodes(n->right));
}
// Depth in edges from the root, or -1 if the key is not in the tree.
int depthOf(const Node* n, char key, int d = 0) {
if (n == nullptr) return -1;
if (n->data == key) return d;
int left = depthOf(n->left, key, d + 1);
if (left != -1) return left;
return depthOf(n->right, key, d + 1);
}
int countNodes(const Node* n) {
if (n == nullptr) return 0;
return 1 + countNodes(n->left) + countNodes(n->right);
}
int countLeaves(const Node* n) {
if (n == nullptr) return 0;
if (n->left == nullptr && n->right == nullptr) return 1;
return countLeaves(n->left) + countLeaves(n->right);
}
// Proper / strictly binary: every node has either 0 or 2 children.
bool isProper(const Node* n) {
if (n == nullptr) return true;
if ((n->left == nullptr) != (n->right == nullptr)) return false;
return isProper(n->left) && isProper(n->right);
}
// Perfect: every level completely filled. Equivalent to n == 2^(h+1) - 1.
bool isPerfect(const Node* n) {
int h = heightEdges(n);
int expected = (1 << (h + 1)) - 1; // h == -1 gives expected == 0
return countNodes(n) == expected;
}
// Complete: number the nodes level by level from 1. The tree is complete
// exactly when no index exceeds the node count -- i.e. the array layout has
// no holes.
bool isComplete(const Node* root) {
if (root == nullptr) return true;
int n = countNodes(root);
std::queue<std::pair<const Node*, int>> q;
q.push({root, 1});
while (!q.empty()) {
auto [node, index] = q.front();
q.pop();
if (index > n) return false;
if (node->left) q.push({node->left, 2 * index});
if (node->right) q.push({node->right, 2 * index + 1});
}
return true;
}
void report(const std::string& name, const Node* root) {
int n = countNodes(root);
std::cout << name << "\n"
<< " nodes = " << n << "\n"
<< " edges = " << (n - 1) << "\n"
<< " height (edges) = " << heightEdges(root) << "\n"
<< " height (nodes) = " << heightNodes(root) << "\n"
<< " leaves = " << countLeaves(root) << "\n"
<< " proper (0 or 2) = " << (isProper(root) ? "yes" : "no") << "\n"
<< " perfect = " << (isPerfect(root) ? "yes" : "no") << "\n"
<< " complete = " << (isComplete(root) ? "yes" : "no") << "\n";
}
void destroy(Node* n) {
if (n == nullptr) return;
destroy(n->left);
destroy(n->right);
delete n;
}
int main() {
// A
// / \
// B C
// / \ / \
// D F E G
Node* perfect = new Node('A');
perfect->left = new Node('B');
perfect->right = new Node('C');
perfect->left->left = new Node('D');
perfect->left->right = new Node('F');
perfect->right->left = new Node('E');
perfect->right->right = new Node('G');
// A
// / \
// B C
// / \ /
// D E F
Node* complete = new Node('A');
complete->left = new Node('B');
complete->right = new Node('C');
complete->left->left = new Node('D');
complete->left->right = new Node('E');
complete->right->left = new Node('F');
// A
// \
// B
// \
// C
// \
// D
Node* skewed = new Node('A');
skewed->right = new Node('B');
skewed->right->right = new Node('C');
skewed->right->right->right = new Node('D');
report("perfect tree A(B(D,F), C(E,G))", perfect);
report("complete-not-perfect A(B(D,E), C(F,_))", complete);
report("right-skewed A-B-C-D", skewed);
std::cout << "\ndepths in the perfect tree: "
<< "A=" << depthOf(perfect, 'A')
<< " B=" << depthOf(perfect, 'B')
<< " G=" << depthOf(perfect, 'G') << "\n";
std::cout << "height of node B (edges) = " << heightEdges(perfect->left) << "\n";
std::cout << "height of leaf D (edges) = " << heightEdges(perfect->left->left) << "\n";
destroy(perfect);
destroy(complete);
destroy(skewed);
}The mistakes that actually cost marks
- Answering a height question without saying which unit. The commonest single loss in this topic. Write
2 (edges), 3 (levels)and move on. - Writing
height()withreturn 0on null and then quoting the edge definition. The code counts levels; the definition counts edges. Pick the base case that matches the answer you are claiming:-1for edges,0for levels. - Putting a level number into a depth formula. With the root on level 1, level
Lholds up to2^(L-1)nodes, not2^L. Convert to depth first if you are unsure:depth = level - 1. - Using the graph-theory degree. In a tree, degree counts children only. A node with a parent and two children has degree 2, not 3.
- Confusing degree of a node with degree of the tree. The tree's degree is the maximum over all nodes — one number for the whole tree.
- Calling a node a leaf because it is on the bottom row, or refusing to call one a leaf because it is not. A leaf is a node with no children, at any depth.
- Swapping full and complete. Complete means every level filled except possibly the last, which fills from the left. If a source's "full" comes with the formula
2^(h+1) - 1, that source means perfect. - Assuming complete implies proper, or proper implies complete. Neither holds. Only perfect implies both.
- Treating left and right as interchangeable. A left-only child and a right-only child are different trees, and in a BST they are contradictory claims about the value.
- Forgetting that the tree's depth and the tree's height are the same number while a node's depth and height are not. Read whether the question asks about the tree or about a node.
- Skipping the null base case in a recursive tree function.
if (node == nullptr) return ...;is the first line of essentially every tree function you will write. Without it you dereference null on the first leaf. - Believing sorted input is the easy case for a BST. It is the worst case: sorted insertion order produces a fully skewed tree and turns every
O(log n)operation intoO(n).
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Read the terms off a tree. Given this tree:
(P)
/ | \
(Q) (R) (S)
/ \ |
(T) (U) (V)
|
(W)Give: the root; every leaf; every internal node; the siblings of R; the ancestors of W; the descendants of S; deg(P), deg(Q), deg(S), deg(T); the degree of the tree; depth(U) and depth(W); height(Q), height(S) and height(R); the height of the tree; the number of edges; and the length of the path from P to W.
Then answer: is R a leaf, and how many nodes would the tree have if you were told only that it has 9 edges?
Solution
Root: P — the only node with no parent.
Leaves (no children): T, U, R, W. R is a leaf despite sitting on the second row; leafhood is about children, not about depth.
Internal nodes (at least one child): P, Q, S, V.
Siblings of R: Q and S — the other children of P.
Ancestors of W: V, S, P, reading upward to the root.
Descendants of S: V and W — everything in S's subtree except S itself.
Degrees (children only): deg(P) = 3, deg(Q) = 2, deg(S) = 1, deg(T) = 0. Degree of the tree = the maximum = 3, from P.
Depths (edges from the root, root = 0): depth(U) = 2, depth(W) = 3.
Heights (edges down to the deepest leaf, leaf = 0): height(Q) = 1 (down to T or U), height(S) = 2 (down through V to W), height(R) = 0 (R is a leaf).
Height of the tree = height of the root = 3 edges (4 levels), along P-S-V-W. This is also the tree's depth: deepest leaf W is at depth 3.
Edges: 7 nodes, so n - 1 = 6 edges. Count them on the diagram to confirm.
Path length P to W: the path is P -> S -> V -> W, which is 4 nodes and therefore 3 edges. Path length is always the edge count, one fewer than the node count.
Is R a leaf? Yes. It has no children. Its depth is irrelevant.
9 edges implies how many nodes? n = edges + 1 = 10. Every node but the root is entered by exactly one edge, so the relationship is exact for every tree, whatever its shape.
Exercise 2
The off-by-one, on purpose. Consider this binary tree:
(A)
/ \
(B) (C)
/ \
(D) (E)
\
(F)(a) What is the height of this tree under each of the two conventions, and how would you write the answer so that either marker accepts it?
(b) The following function is applied to the root. What does it return, and which convention is it using?
int height(Node* n) {
if (n == nullptr) return 0;
return 1 + std::max(height(n->left), height(n->right));
}(c) Change one thing in that function so it returns the edge count instead, and say what it then returns for a leaf and for the empty tree.
(d) Using the edge convention throughout: what is the maximum number of nodes a tree of this height could hold, the minimum, and what is the minimum possible height for the 6 nodes it actually has?
Solution
(a) The longest root-to-leaf path is A -> C -> E -> F: 4 nodes, 3 edges. So the height is 3 in edges, 4 in levels. Write it as height = 3 edges (4 levels). That is not hedging — it is the same measurement stated in explicit units, and it is right under either marking scheme.
(b) It returns 4. Trace the deepest branch: F is a leaf, so 1 + max(0, 0) = 1; E gets 1 + max(0, 1) = 2; C gets 1 + max(0, 2) = 3; A gets 1 + max(height(B), 3) and height(B) = 2, so 1 + 3 = 4. Because the base case gives the empty tree 0, a leaf comes out 1 — the function is counting nodes (levels), not edges, even if the definition printed beside it says "measured in edges".
(c) Change the base case to return -1;. Then a leaf computes 1 + max(-1, -1) = 0, which is what the edge definition requires, and the whole tree comes out 3. For the empty tree it returns -1. That value is not a bug: it is the only base case that makes the leaf case come out right, and -1 is the conventional height of the empty tree under the edge convention. If a caller cannot cope with a negative height, guard the empty case at the call site rather than by breaking the recursion.
(d) With h = 3 edges:
- Maximum nodes:
2^(h+1) - 1 = 2^4 - 1 = 15(that would be the perfect tree with four full levels). - Minimum nodes:
h + 1 = 4(a chain of four). - Minimum height for
n = 6:floor(log2 6) = 2. Check it against the max-nodes formula: height 1 holds at most2^2 - 1 = 3nodes, which is too few; height 2 holds up to2^3 - 1 = 7, which is enough. So 2 edges.
The tree in the question has 6 nodes at height 3 when it could have held them at height 2. That one wasted level is what "unbalanced" costs you, and it grows with n.
Exercise 3
Classify, then implement. For each of these three binary trees, decide whether it is perfect, complete, proper (every node has 0 or 2 children), and balanced. Justify each no with the specific node that breaks it.
T1 T2 T3
(A) (A) (A)
/ \ / \ / \
(B) (C) (B) (C) (B) (C)
/ \ / \ / \ / /
(D) (E) (F) (G) (D) (E) (F) (D)
/
(E)Then: write a function that decides completeness for an arbitrary binary tree, and explain why the level-order index test works. Finally, explain why a heap insists on completeness while a binary search tree does not.
Solution
T1 — 7 nodes, all three levels filled. Perfect: yes. Complete: yes (perfect implies complete). Proper: yes, every node has 0 or 2 children. Balanced: yes, every node's subtrees have equal height. This is the tree every formula is checked against: n = 7 = 2^3 - 1 with h = 2.
T2 — 6 nodes; the last level holds D, E, F filling from the left with no gap. Complete: yes. Perfect: no — the last level is missing C's right child, so n = 6 is not 2^(h+1) - 1 = 7. Proper: no — C has a left child and no right child, exactly one child, which is what proper forbids. Balanced: yes — every node's two subtree heights differ by at most 1.
T3 — 5 nodes in the shape A(B(D(E)), C). Perfect: no (level 3 is not filled). Complete: no — number level by level: A=1, B=2, C=3, D=4, and then E is D's left child so its index is 2*4 = 8, which exceeds the node count 5. Concretely, positions 5, 6 and 7 are empty while position 8 is occupied: a gap. Proper: no — B has only a left child, and D has only a left child. Balanced: no — at node A, the left subtree B(D(E)) has height 2 and the right subtree C has height 0, a difference of 2. (B also fails locally: heights 1 and -1.)
The completeness test.
bool isComplete(const Node* root) {
if (root == nullptr) return true;
int n = countNodes(root);
std::queue<std::pair<const Node*, int>> q;
q.push({root, 1});
while (!q.empty()) {
auto [node, index] = q.front();
q.pop();
if (index > n) return false;
if (node->left) q.push({node->left, 2 * index});
if (node->right) q.push({node->right, 2 * index + 1});
}
return true;
}Why it works. Give the root index 1 and each node's children indices 2i and 2i + 1. This is the standard array layout of a binary tree, and it assigns indices 1..n with nothing skipped exactly when every level is full except possibly the last and the last fills from the left. Any hole shifts everything after it upward, so some node ends up with an index greater than n — which is precisely what the check catches. Complexity is O(n) time and O(n) space for the queue (the last level can hold half the nodes). An equivalent formulation enqueues null children too and asserts that no non-null node appears after the first null.
Why heaps insist on it and BSTs do not. A heap's ordering rule only relates a parent to its children, never left to right, so the heap is free to choose its shape — and it chooses completeness, because a complete tree packs into a flat array with no wasted slots, giving O(1) index arithmetic instead of pointer chasing, and guaranteeing height floor(log2 n), which is what makes push and pop O(log n). A BST's ordering rule, by contrast, dictates where every key must go: the insertion order fixes the shape, and the structure has no freedom left to enforce completeness. That is why an unbalanced BST can degenerate into a chain while a heap never can, and why keeping a BST shallow requires extra machinery — rotations, in AVL and red-black trees.
Published and updated 2026-08-09 · DUOCODE TECHNOLOGY