out) {
+ if (node == null) {
+ return;
+ }
+ postorder(node.left, out);
+ postorder(node.right, out);
+ out.add(node.val);
+ }
+}
diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java
new file mode 100644
index 0000000..a6ccf25
--- /dev/null
+++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java
@@ -0,0 +1,16 @@
+package com.ankurm.interviewlab.bintree;
+
+import java.util.List;
+
+/**
+ * Optional observer that the iterative traversals call after every push, pop or queue operation.
+ * The tables in the article are produced by this hook on the real implementations, not by a
+ * separate "teaching" copy of the algorithm.
+ *
+ * {@code containerA} is the main stack (bottom first, top last) or the queue (front first).
+ * {@code containerB} is the second stack of the two-stack postorder; it is empty for the others.
+ */
+@FunctionalInterface
+public interface StepListener {
+ void step(String action, List containerA, List containerB, List output);
+}
diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java
new file mode 100644
index 0000000..013fa57
--- /dev/null
+++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java
@@ -0,0 +1,141 @@
+package com.ankurm.interviewlab.bintree;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Size, leaves, height and diameter. Height is counted in NODES on the longest root-to-leaf path
+ * (empty tree = 0, single node = 1). Many textbooks count EDGES instead (single node = 0): subtract one.
+ * The same applies to diameter below.
+ */
+public final class TreeMetrics {
+ private TreeMetrics() {}
+
+ public static int countNodes(TreeNode node) {
+ return node == null ? 0 : 1 + countNodes(node.left) + countNodes(node.right);
+ }
+
+ public static int countLeaves(TreeNode node) {
+ if (node == null) {
+ return 0;
+ }
+ if (node.left == null && node.right == null) {
+ return 1;
+ }
+ return countLeaves(node.left) + countLeaves(node.right);
+ }
+
+ public static int height(TreeNode node) {
+ if (node == null) {
+ return 0;
+ }
+ return 1 + Math.max(height(node.left), height(node.right));
+ }
+
+ /** Height without recursion: the number of levels a queue-based traversal walks through. */
+ public static int heightIterative(TreeNode root) {
+ if (root == null) {
+ return 0;
+ }
+ int levels = 0;
+ Deque queue = new ArrayDeque<>();
+ queue.offer(root);
+ while (!queue.isEmpty()) {
+ levels++;
+ for (int i = queue.size(); i > 0; i--) {
+ TreeNode node = queue.poll();
+ if (node.left != null) {
+ queue.offer(node.left);
+ }
+ if (node.right != null) {
+ queue.offer(node.right);
+ }
+ }
+ }
+ return levels;
+ }
+
+ // ---------------------------------------------------------------- diameter
+
+ /**
+ * The obvious solution: for every node, ask for the height of both sides (each a full walk of
+ * that subtree), then recurse into the children. O(n^2) in the worst case.
+ * {@code visits[0]} counts every node entered by either {@code height} or this method.
+ */
+ public static int diameterNaive(TreeNode node, long[] visits) {
+ if (node == null) {
+ return 0;
+ }
+ visits[0]++;
+ int through = heightCounting(node.left, visits) + heightCounting(node.right, visits) + 1;
+ int inLeft = diameterNaive(node.left, visits);
+ int inRight = diameterNaive(node.right, visits);
+ return Math.max(through, Math.max(inLeft, inRight));
+ }
+
+ private static int heightCounting(TreeNode node, long[] visits) {
+ if (node == null) {
+ return 0;
+ }
+ visits[0]++;
+ return 1 + Math.max(heightCounting(node.left, visits), heightCounting(node.right, visits));
+ }
+
+ /** One post-order pass: each node returns its height and updates the best path through itself. */
+ public static int diameterOnePass(TreeNode root, long[] visits) {
+ int[] best = {0};
+ heightAndDiameter(root, best, visits);
+ return best[0];
+ }
+
+ private static int heightAndDiameter(TreeNode node, int[] best, long[] visits) {
+ if (node == null) {
+ return 0;
+ }
+ visits[0]++;
+ int left = heightAndDiameter(node.left, best, visits);
+ int right = heightAndDiameter(node.right, best, visits);
+ best[0] = Math.max(best[0], left + right + 1);
+ return 1 + Math.max(left, right);
+ }
+
+ /** The values along one longest path (ties: prefers the left child), for drawing the diagram. */
+ public static List diameterPath(TreeNode root) {
+ TreeNode[] bestNode = {null};
+ int[] bestLen = {0};
+ locateBest(root, bestNode, bestLen);
+ if (bestNode[0] == null) {
+ return List.of();
+ }
+ List path = new ArrayList<>(deepestPath(bestNode[0].left));
+ Collections.reverse(path);
+ path.add(bestNode[0].val);
+ path.addAll(deepestPath(bestNode[0].right));
+ return path;
+ }
+
+ private static int locateBest(TreeNode node, TreeNode[] bestNode, int[] bestLen) {
+ if (node == null) {
+ return 0;
+ }
+ int left = locateBest(node.left, bestNode, bestLen);
+ int right = locateBest(node.right, bestNode, bestLen);
+ if (left + right + 1 > bestLen[0]) {
+ bestLen[0] = left + right + 1;
+ bestNode[0] = node;
+ }
+ return 1 + Math.max(left, right);
+ }
+
+ private static List deepestPath(TreeNode node) {
+ List path = new ArrayList<>();
+ while (node != null) {
+ path.add(node.val);
+ node = height(node.left) >= height(node.right) ? node.left : node.right;
+ }
+ return path;
+ }
+}
diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java
new file mode 100644
index 0000000..03b9052
--- /dev/null
+++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java
@@ -0,0 +1,18 @@
+package com.ankurm.interviewlab.bintree;
+
+/** A binary tree node. Public fields on purpose: this is teaching code, not a library. */
+public final class TreeNode {
+ public int val;
+ public TreeNode left;
+ public TreeNode right;
+
+ public TreeNode(int val) {
+ this.val = val;
+ }
+
+ public TreeNode(int val, TreeNode left, TreeNode right) {
+ this.val = val;
+ this.left = left;
+ this.right = right;
+ }
+}
diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java
new file mode 100644
index 0000000..c54582b
--- /dev/null
+++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java
@@ -0,0 +1,186 @@
+package com.ankurm.interviewlab.bintree;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Consumer;
+
+/** Tree builders used by the demos and tests, plus tree construction from traversals. */
+public final class Trees {
+ private Trees() {}
+
+ /**
+ * The sample tree used by every diagram in the article (values 1..9):
+ *
+ * 1
+ * / \
+ * 2 3
+ * / \ \
+ * 4 5 6
+ * / / \
+ * 7 8 9
+ *
+ */
+ public static TreeNode sample() {
+ return new TreeNode(1,
+ new TreeNode(2,
+ new TreeNode(4),
+ new TreeNode(5, new TreeNode(7), null)),
+ new TreeNode(3,
+ null,
+ new TreeNode(6, new TreeNode(8), new TreeNode(9))));
+ }
+
+ /** A random binary tree whose values are a random permutation of 1..n (so values are unique). */
+ public static TreeNode random(Random random, int n) {
+ if (n == 0) {
+ return null;
+ }
+ List values = new ArrayList<>();
+ for (int i = 1; i <= n; i++) {
+ values.add(i);
+ }
+ Collections.shuffle(values, random);
+ TreeNode root = new TreeNode(values.get(0));
+ List open = new ArrayList<>(); // nodes that still have a free child slot
+ open.add(root);
+ for (int i = 1; i < n; i++) {
+ int pick = random.nextInt(open.size());
+ TreeNode parent = open.get(pick);
+ TreeNode child = new TreeNode(values.get(i));
+ boolean goLeft = parent.left == null && (parent.right != null || random.nextBoolean());
+ if (goLeft) {
+ parent.left = child;
+ } else {
+ parent.right = child;
+ }
+ if (parent.left != null && parent.right != null) {
+ open.set(pick, open.get(open.size() - 1));
+ open.remove(open.size() - 1);
+ }
+ open.add(child);
+ }
+ return root;
+ }
+
+ /** n nodes, each the LEFT child of the previous one; built with a loop, so building never overflows. */
+ public static TreeNode leftSkewed(int n) {
+ TreeNode root = null;
+ for (int i = n; i >= 1; i--) {
+ root = new TreeNode(i, root, null);
+ }
+ return root;
+ }
+
+ /** A perfect tree of the given number of levels, values 1.. in level order. */
+ public static TreeNode perfect(int levels) {
+ return perfect(1, levels);
+ }
+
+ private static TreeNode perfect(int value, int levels) {
+ if (levels == 0) {
+ return null;
+ }
+ return new TreeNode(value, perfect(2 * value, levels - 1), perfect(2 * value + 1, levels - 1));
+ }
+
+ /** Ordinary binary SEARCH tree insertion (smaller left, larger right, duplicates ignored). */
+ public static TreeNode bst(int... values) {
+ TreeNode root = null;
+ for (int v : values) {
+ root = insert(root, v);
+ }
+ return root;
+ }
+
+ private static TreeNode insert(TreeNode node, int v) {
+ if (node == null) {
+ return new TreeNode(v);
+ }
+ if (v < node.val) {
+ node.left = insert(node.left, v);
+ } else if (v > node.val) {
+ node.right = insert(node.right, v);
+ }
+ return node;
+ }
+
+ // ----------------------------------------------- construction from traversals
+
+ /**
+ * Rebuilds the tree from its inorder and preorder sequences (values must be unique).
+ * {@code trace} (nullable) receives one line per recursive call, which is how the article shows the splitting.
+ */
+ public static TreeNode fromInorderPreorder(int[] inorder, int[] preorder, Consumer trace) {
+ Map position = new HashMap<>();
+ for (int i = 0; i < inorder.length; i++) {
+ position.put(inorder[i], i);
+ }
+ return buildPre(preorder, new int[] {0}, 0, inorder.length - 1, position, trace, 0);
+ }
+
+ private static TreeNode buildPre(int[] pre, int[] next, int lo, int hi, Map position,
+ Consumer trace, int depth) {
+ if (lo > hi) {
+ return null;
+ }
+ int rootValue = pre[next[0]++];
+ int mid = position.get(rootValue);
+ if (trace != null) {
+ trace.accept(" ".repeat(depth) + "root " + rootValue + " inorder slice [" + lo + ".." + hi
+ + "] left size " + (mid - lo) + ", right size " + (hi - mid));
+ }
+ TreeNode node = new TreeNode(rootValue);
+ node.left = buildPre(pre, next, lo, mid - 1, position, trace, depth + 1);
+ node.right = buildPre(pre, next, mid + 1, hi, position, trace, depth + 1);
+ return node;
+ }
+
+ /** Rebuilds the tree from inorder and postorder (the root is the LAST postorder element; build right first). */
+ public static TreeNode fromInorderPostorder(int[] inorder, int[] postorder) {
+ Map position = new HashMap<>();
+ for (int i = 0; i < inorder.length; i++) {
+ position.put(inorder[i], i);
+ }
+ return buildPost(postorder, new int[] {postorder.length - 1}, 0, inorder.length - 1, position);
+ }
+
+ private static TreeNode buildPost(int[] post, int[] next, int lo, int hi, Map position) {
+ if (lo > hi) {
+ return null;
+ }
+ int rootValue = post[next[0]--];
+ int mid = position.get(rootValue);
+ TreeNode node = new TreeNode(rootValue);
+ node.right = buildPost(post, next, mid + 1, hi, position);
+ node.left = buildPost(post, next, lo, mid - 1, position);
+ return node;
+ }
+
+ // -------------------------------------------------------------- utilities
+
+ public static boolean sameShapeAndValues(TreeNode a, TreeNode b) {
+ if (a == null || b == null) {
+ return a == b;
+ }
+ return a.val == b.val && sameShapeAndValues(a.left, b.left) && sameShapeAndValues(a.right, b.right);
+ }
+
+ /** Compact one-line form: value(left,right), with "-" for a missing child, e.g. 1(2(4,5),3(6,-)). */
+ public static String bracket(TreeNode node) {
+ if (node == null) {
+ return "-";
+ }
+ if (node.left == null && node.right == null) {
+ return Integer.toString(node.val);
+ }
+ return node.val + "(" + bracket(node.left) + "," + bracket(node.right) + ")";
+ }
+
+ public static int[] toArray(List list) {
+ return list.stream().mapToInt(Integer::intValue).toArray();
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/ConstructTreeTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/ConstructTreeTest.java
new file mode 100644
index 0000000..c310549
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/ConstructTreeTest.java
@@ -0,0 +1,64 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.ankurm.interviewlab.bintree.support.Transcript;
+
+/** Outputs 10 and 11: exam staples (rebuild from traversals, BST inorder is sorted). */
+class ConstructTreeTest {
+
+ @Test
+ void rebuildFromInorderAndPreorder() {
+ int[] inorder = {4, 2, 5, 1, 6, 3};
+ int[] preorder = {1, 2, 4, 5, 3, 6};
+ int[] postorder = {4, 5, 2, 6, 3, 1};
+
+ Transcript t = Transcript.start("10-construct-from-traversals.txt", "Constructing a tree from two traversals");
+ t.line("inorder : " + List.of(4, 2, 5, 1, 6, 3));
+ t.line("preorder : " + List.of(1, 2, 4, 5, 3, 6)).blank();
+ t.line("from inorder + preorder, one line per recursive call:");
+ TreeNode fromPre = Trees.fromInorderPreorder(inorder, preorder, t::line);
+ t.line("result : " + Trees.bracket(fromPre));
+ t.line("postorder of the result: " + RecursiveTraversals.postorder(fromPre)).blank();
+
+ assertEquals("1(2(4,5),3(6,-))", Trees.bracket(fromPre));
+ assertEquals(List.of(4, 5, 2, 6, 3, 1), RecursiveTraversals.postorder(fromPre));
+
+ TreeNode fromPost = Trees.fromInorderPostorder(inorder, postorder);
+ assertTrue(Trees.sameShapeAndValues(fromPre, fromPost));
+ t.line("from inorder + postorder the same tree comes back: " + Trees.bracket(fromPost)).blank();
+
+ // Why inorder is needed: preorder + postorder cannot tell a left child from a right child.
+ TreeNode leftOnly = new TreeNode(1, new TreeNode(2), null);
+ TreeNode rightOnly = new TreeNode(1, null, new TreeNode(2));
+ assertEquals(RecursiveTraversals.preorder(leftOnly), RecursiveTraversals.preorder(rightOnly));
+ assertEquals(RecursiveTraversals.postorder(leftOnly), RecursiveTraversals.postorder(rightOnly));
+ t.line("two different trees with the same preorder AND postorder:");
+ t.line(" " + Trees.bracket(leftOnly) + " preorder " + RecursiveTraversals.preorder(leftOnly)
+ + " postorder " + RecursiveTraversals.postorder(leftOnly));
+ t.line(" " + Trees.bracket(rightOnly) + " preorder " + RecursiveTraversals.preorder(rightOnly)
+ + " postorder " + RecursiveTraversals.postorder(rightOnly));
+ t.flush();
+ }
+
+ @Test
+ void inorderOfABinarySearchTreeIsSorted() {
+ TreeNode bst = Trees.bst(50, 30, 70, 20, 40, 60, 80);
+ List inorder = RecursiveTraversals.inorder(bst);
+ assertEquals(List.of(20, 30, 40, 50, 60, 70, 80), inorder);
+ assertEquals(inorder, IterativeTraversals.inorder(bst));
+
+ Transcript t = Transcript.start("11-bst-inorder-sorted.txt", "Inorder of a binary search tree is sorted");
+ t.line("inserted in this order : [50, 30, 70, 20, 40, 60, 80]");
+ t.line("tree : " + Trees.bracket(bst));
+ t.line("preorder : " + RecursiveTraversals.preorder(bst));
+ t.line("inorder : " + inorder);
+ t.line("postorder : " + RecursiveTraversals.postorder(bst));
+ t.flush();
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DeepTreeTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DeepTreeTest.java
new file mode 100644
index 0000000..6f1429d
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DeepTreeTest.java
@@ -0,0 +1,61 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Recursion overflows on a deep skewed tree; the iterative versions do not. The thread is given a
+ * deliberately small 256 KB stack so the outcome does not depend on the machine's default -Xss.
+ */
+class DeepTreeTest {
+
+ private static final int DEPTH = 200_000;
+
+ @Test
+ void recursiveInorderOverflowsButIterativeSurvives() throws Exception {
+ TreeNode root = Trees.leftSkewed(DEPTH);
+ AtomicReference recursiveOutcome = new AtomicReference<>();
+ AtomicReference> iterative = new AtomicReference<>();
+ AtomicReference> iterativePost = new AtomicReference<>();
+ AtomicReference>> levels = new AtomicReference<>();
+
+ Thread worker = new Thread(null, () -> {
+ try {
+ RecursiveTraversals.inorder(root);
+ } catch (Throwable t) {
+ recursiveOutcome.set(t);
+ }
+ iterative.set(IterativeTraversals.inorder(root));
+ iterativePost.set(IterativeTraversals.postorderOneStack(root));
+ levels.set(LevelOrder.byLevel(root));
+ }, "small-stack", 256 * 1024);
+ worker.start();
+ worker.join();
+
+ assertEquals(StackOverflowError.class, recursiveOutcome.get().getClass());
+ assertEquals(DEPTH, iterative.get().size());
+ assertEquals(DEPTH, iterativePost.get().size());
+ assertEquals(DEPTH, levels.get().size(), "a skewed tree has one node per level");
+ assertEquals(DEPTH, TreeMetrics.heightIterative(root));
+ }
+
+ @Test
+ void buildingAndMorrisNeedNoRecursion() {
+ TreeNode root = Trees.leftSkewed(DEPTH);
+ assertDoesNotThrow(() -> assertEquals(DEPTH, IterativeTraversals.morrisInorder(root).size()));
+ assertThrows(StackOverflowError.class, () -> {
+ Thread t = new Thread(null, () -> TreeMetrics.height(Trees.leftSkewed(DEPTH)), "h", 256 * 1024);
+ AtomicReference ex = new AtomicReference<>();
+ t.setUncaughtExceptionHandler((th, e) -> ex.set(e));
+ t.start();
+ t.join();
+ throw ex.get();
+ });
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DiameterVisitCountTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DiameterVisitCountTest.java
new file mode 100644
index 0000000..ecb60d9
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DiameterVisitCountTest.java
@@ -0,0 +1,47 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import com.ankurm.interviewlab.bintree.support.Transcript;
+
+/** Output 09: counted (not timed) node visits, naive O(n^2) diameter versus the one-pass version. */
+class DiameterVisitCountTest {
+
+ @Test
+ void naiveVisitsGrowQuadraticallyOnSkewedTreesAndOnePassStaysLinear() {
+ Transcript t = Transcript.start("09-diameter-visit-counts.txt",
+ "Diameter: node visits, naive vs one pass (counted, not timed)");
+ t.line(String.format("%-22s %8s %14s %12s %10s", "tree", "nodes", "naive visits", "one-pass", "diameter"));
+
+ TreeNode[] trees = {
+ Trees.sample(), Trees.perfect(4), Trees.perfect(7), Trees.perfect(10),
+ Trees.leftSkewed(100), Trees.leftSkewed(1000), Trees.leftSkewed(2000)
+ };
+ String[] names = {
+ "sample (9)", "perfect, 4 levels", "perfect, 7 levels", "perfect, 10 levels",
+ "left-skewed 100", "left-skewed 1000", "left-skewed 2000"
+ };
+ for (int i = 0; i < trees.length; i++) {
+ long[] naive = {0};
+ long[] onePass = {0};
+ int n = TreeMetrics.countNodes(trees[i]);
+ int a = TreeMetrics.diameterNaive(trees[i], naive);
+ int b = TreeMetrics.diameterOnePass(trees[i], onePass);
+ assertEquals(a, b);
+ assertEquals(n, onePass[0], "one pass touches each node exactly once");
+ assertTrue(naive[0] >= onePass[0]);
+ t.line(String.format("%-22s %8d %14d %12d %10d", names[i], n, naive[0], onePass[0], b));
+ }
+
+ // On a skewed tree the naive version visits exactly n(n+1)/2 nodes: the sum 1 + 2 + ... + n.
+ for (int n : new int[] {100, 1000, 2000}) {
+ long[] naive = {0};
+ TreeMetrics.diameterNaive(Trees.leftSkewed(n), naive);
+ assertEquals((long) n * (n + 1) / 2, naive[0]);
+ }
+ t.flush();
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/RandomTreesEquivalenceTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/RandomTreesEquivalenceTest.java
new file mode 100644
index 0000000..32f9a31
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/RandomTreesEquivalenceTest.java
@@ -0,0 +1,147 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.junit.jupiter.api.Test;
+
+/** All implementations must agree with each other on many random trees. */
+class RandomTreesEquivalenceTest {
+
+ @Test
+ void allDepthFirstImplementationsAgreeOnRandomTrees() {
+ Random random = new Random(20261106L);
+ for (int round = 0; round < 1000; round++) {
+ int n = random.nextInt(80);
+ TreeNode root = Trees.random(random, n);
+ String shape = Trees.bracket(root);
+
+ List in = RecursiveTraversals.inorder(root);
+ List pre = RecursiveTraversals.preorder(root);
+ List post = RecursiveTraversals.postorder(root);
+ assertEquals(n, in.size());
+
+ assertEquals(in, IterativeTraversals.inorder(root), shape);
+ assertEquals(pre, IterativeTraversals.preorder(root), shape);
+ assertEquals(post, IterativeTraversals.postorderTwoStacks(root), shape);
+ assertEquals(post, IterativeTraversals.postorderOneStack(root), shape);
+ assertEquals(in, IterativeTraversals.morrisInorder(root), shape);
+ assertEquals(shape, Trees.bracket(root), "Morris inorder must restore the tree");
+ assertEquals(pre, IterativeTraversals.morrisPreorder(root), shape);
+ assertEquals(shape, Trees.bracket(root), "Morris preorder must restore the tree");
+
+ // construction round trips
+ TreeNode a = Trees.fromInorderPreorder(Trees.toArray(in), Trees.toArray(pre), null);
+ TreeNode b = Trees.fromInorderPostorder(Trees.toArray(in), Trees.toArray(post));
+ assertEquals(shape, Trees.bracket(a));
+ assertEquals(shape, Trees.bracket(b));
+ }
+ }
+
+ @Test
+ void levelOrderFamilyAndMetricsAgreeOnRandomTrees() {
+ Random random = new Random(1106L);
+ for (int round = 0; round < 1000; round++) {
+ int n = random.nextInt(80);
+ TreeNode root = Trees.random(random, n);
+ String shape = Trees.bracket(root);
+
+ List> levels = LevelOrder.byLevel(root);
+ List flat = new ArrayList<>();
+ levels.forEach(flat::addAll);
+ assertEquals(LevelOrder.levelOrder(root), flat, shape);
+ assertEquals(levels.size(), TreeMetrics.height(root), shape);
+ assertEquals(TreeMetrics.height(root), TreeMetrics.heightIterative(root), shape);
+
+ List> zig = LevelOrder.zigzag(root);
+ assertEquals(levels.size(), zig.size());
+ for (int i = 0; i < levels.size(); i++) {
+ List expected = new ArrayList<>(levels.get(i));
+ if (i % 2 == 1) {
+ java.util.Collections.reverse(expected);
+ }
+ assertEquals(expected, zig.get(i), shape);
+ }
+
+ assertEquals(n, TreeMetrics.countNodes(root));
+ int leaves = 0;
+ for (TreeNode node : allNodes(root)) {
+ if (node.left == null && node.right == null) {
+ leaves++;
+ }
+ }
+ assertEquals(leaves, TreeMetrics.countLeaves(root), shape);
+
+ long[] naive = {0};
+ long[] onePass = {0};
+ int expectedDiameter = bruteForceDiameterInNodes(root);
+ assertEquals(expectedDiameter, TreeMetrics.diameterNaive(root, naive), shape);
+ assertEquals(expectedDiameter, TreeMetrics.diameterOnePass(root, onePass), shape);
+ assertEquals(n, onePass[0]);
+ assertEquals(expectedDiameter, TreeMetrics.diameterPath(root).size(), shape);
+ }
+ }
+
+ // ---- independent oracle: double BFS on the tree seen as an undirected graph ----
+
+ private static List allNodes(TreeNode root) {
+ List nodes = new ArrayList<>();
+ Deque stack = new ArrayDeque<>();
+ if (root != null) {
+ stack.push(root);
+ }
+ while (!stack.isEmpty()) {
+ TreeNode node = stack.pop();
+ nodes.add(node);
+ if (node.left != null) {
+ stack.push(node.left);
+ }
+ if (node.right != null) {
+ stack.push(node.right);
+ }
+ }
+ return nodes;
+ }
+
+ private static int bruteForceDiameterInNodes(TreeNode root) {
+ if (root == null) {
+ return 0;
+ }
+ Map> graph = new IdentityHashMap<>();
+ for (TreeNode node : allNodes(root)) {
+ graph.computeIfAbsent(node, k -> new ArrayList<>());
+ for (TreeNode child : new TreeNode[] {node.left, node.right}) {
+ if (child != null) {
+ graph.computeIfAbsent(child, k -> new ArrayList<>()).add(node);
+ graph.get(node).add(child);
+ }
+ }
+ }
+ // longest shortest-path over ALL pairs: BFS from every node (fine for n < 80)
+ int best = 0;
+ for (TreeNode start : graph.keySet()) {
+ Map dist = new IdentityHashMap<>();
+ Deque queue = new ArrayDeque<>();
+ queue.add(start);
+ dist.put(start, 1);
+ while (!queue.isEmpty()) {
+ TreeNode cur = queue.poll();
+ best = Math.max(best, dist.get(cur));
+ for (TreeNode next : graph.get(cur)) {
+ if (!dist.containsKey(next)) {
+ dist.put(next, dist.get(cur) + 1);
+ queue.add(next);
+ }
+ }
+ }
+ }
+ return best;
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/SampleTreeTranscriptTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/SampleTreeTranscriptTest.java
new file mode 100644
index 0000000..83ce7b0
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/SampleTreeTranscriptTest.java
@@ -0,0 +1,106 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.ankurm.interviewlab.bintree.support.Transcript;
+
+/** Output 01 and 08: every traversal of the sample tree, with the numbers the diagrams use. */
+class SampleTreeTranscriptTest {
+
+ static final List INORDER = List.of(4, 2, 7, 5, 1, 3, 8, 6, 9);
+ static final List PREORDER = List.of(1, 2, 4, 5, 7, 3, 6, 8, 9);
+ static final List POSTORDER = List.of(4, 7, 5, 2, 8, 9, 6, 3, 1);
+ static final List LEVEL = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
+
+ @Test
+ void allTraversalsOfTheSampleTree() {
+ TreeNode root = Trees.sample();
+ String before = Trees.bracket(root);
+
+ Transcript t = Transcript.start("01-sample-tree-all-traversals.txt",
+ "Sample tree: every traversal, recursive and iterative");
+ t.line("tree (value(left,right), - = no child): " + before).blank();
+
+ assertEquals(INORDER, RecursiveTraversals.inorder(root));
+ assertEquals(PREORDER, RecursiveTraversals.preorder(root));
+ assertEquals(POSTORDER, RecursiveTraversals.postorder(root));
+ t.line("recursive inorder : " + RecursiveTraversals.inorder(root));
+ t.line("recursive preorder : " + RecursiveTraversals.preorder(root));
+ t.line("recursive postorder : " + RecursiveTraversals.postorder(root)).blank();
+
+ assertEquals(INORDER, IterativeTraversals.inorder(root));
+ assertEquals(PREORDER, IterativeTraversals.preorder(root));
+ assertEquals(POSTORDER, IterativeTraversals.postorderTwoStacks(root));
+ assertEquals(POSTORDER, IterativeTraversals.postorderOneStack(root));
+ t.line("iterative inorder : " + IterativeTraversals.inorder(root));
+ t.line("iterative preorder : " + IterativeTraversals.preorder(root));
+ t.line("iterative postorder (2 stacks): " + IterativeTraversals.postorderTwoStacks(root));
+ t.line("iterative postorder (1 stack) : " + IterativeTraversals.postorderOneStack(root)).blank();
+
+ assertEquals(INORDER, IterativeTraversals.morrisInorder(root));
+ assertEquals(PREORDER, IterativeTraversals.morrisPreorder(root));
+ assertEquals(before, Trees.bracket(root), "Morris must restore every right pointer");
+ t.line("Morris inorder : " + IterativeTraversals.morrisInorder(root));
+ t.line("Morris preorder : " + IterativeTraversals.morrisPreorder(root));
+ t.line("tree after Morris : " + Trees.bracket(root) + " (unchanged)").blank();
+
+ assertEquals(LEVEL, LevelOrder.levelOrder(root));
+ assertEquals(List.of(List.of(1), List.of(2, 3), List.of(4, 5, 6), List.of(7, 8, 9)), LevelOrder.byLevel(root));
+ assertEquals(List.of(List.of(1), List.of(3, 2), List.of(4, 5, 6), List.of(9, 8, 7)), LevelOrder.zigzag(root));
+ t.line("level order (queue) : " + LevelOrder.levelOrder(root));
+ t.line("level order by level : " + LevelOrder.byLevel(root));
+ t.line("zigzag : " + LevelOrder.zigzag(root));
+ t.flush();
+ }
+
+ @Test
+ void heightAndDiameterOfTheSampleTree() {
+ TreeNode root = Trees.sample();
+ long[] naive = {0};
+ long[] onePass = {0};
+ int diameterNaive = TreeMetrics.diameterNaive(root, naive);
+ int diameterOnePass = TreeMetrics.diameterOnePass(root, onePass);
+ List path = TreeMetrics.diameterPath(root);
+
+ assertEquals(9, TreeMetrics.countNodes(root));
+ assertEquals(4, TreeMetrics.countLeaves(root));
+ assertEquals(4, TreeMetrics.height(root));
+ assertEquals(4, TreeMetrics.heightIterative(root));
+ assertEquals(7, diameterNaive);
+ assertEquals(7, diameterOnePass);
+ assertEquals(9, onePass[0], "one pass visits each node exactly once");
+ assertTrue(naive[0] > onePass[0]);
+ assertEquals(List.of(7, 5, 2, 1, 3, 6, 8), path);
+
+ Transcript t = Transcript.start("08-height-diameter-sample.txt", "Sample tree: size, height and diameter");
+ t.line("nodes : " + TreeMetrics.countNodes(root));
+ t.line("leaves : " + TreeMetrics.countLeaves(root));
+ t.line("height, counted in nodes : " + TreeMetrics.height(root));
+ t.line("height, counted in edges : " + (TreeMetrics.height(root) - 1));
+ t.line("height via level order (BFS) : " + TreeMetrics.heightIterative(root));
+ t.line("diameter, counted in nodes : " + diameterOnePass);
+ t.line("diameter, counted in edges : " + (diameterOnePass - 1));
+ t.line("one longest path : " + path);
+ java.util.TreeMap heights = new java.util.TreeMap<>();
+ collectHeights(root, heights);
+ assertEquals("{1=4, 2=3, 3=3, 4=1, 5=2, 6=2, 7=1, 8=1, 9=1}", heights.toString());
+ t.line("height of the subtree at node : " + heights);
+ t.line("node visits, naive O(n^2) way : " + naive[0]);
+ t.line("node visits, one-pass way : " + onePass[0]);
+ t.flush();
+ }
+
+ private static void collectHeights(TreeNode node, java.util.Map into) {
+ if (node == null) {
+ return;
+ }
+ into.put(node.val, TreeMetrics.height(node));
+ collectHeights(node.left, into);
+ collectHeights(node.right, into);
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/StepTraceTranscriptTest.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/StepTraceTranscriptTest.java
new file mode 100644
index 0000000..302bd41
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/StepTraceTranscriptTest.java
@@ -0,0 +1,78 @@
+package com.ankurm.interviewlab.bintree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.ankurm.interviewlab.bintree.support.StepTable;
+import com.ankurm.interviewlab.bintree.support.Transcript;
+
+/** Outputs 02-07: the step-by-step stack, queue and thread tables for the sample tree. */
+class StepTraceTranscriptTest {
+
+ private static final String STACK = "stack (bottom..top)";
+
+ @Test
+ void inorderSteps() {
+ StepTable table = new StepTable(STACK, null);
+ assertEquals(SampleTreeTranscriptTest.INORDER, IterativeTraversals.inorder(Trees.sample(), table));
+ assertEquals(18, table.size(), "9 pushes + 9 pops");
+ Transcript t = Transcript.start("02-inorder-iterative-steps.txt", "Iterative inorder on the sample tree, step by step");
+ table.writeTo(t);
+ t.flush();
+ }
+
+ @Test
+ void preorderSteps() {
+ StepTable table = new StepTable(STACK, null);
+ assertEquals(SampleTreeTranscriptTest.PREORDER, IterativeTraversals.preorder(Trees.sample(), table));
+ assertEquals(9, table.size());
+ Transcript t = Transcript.start("03-preorder-iterative-steps.txt", "Iterative preorder on the sample tree, step by step");
+ table.writeTo(t);
+ t.flush();
+ }
+
+ @Test
+ void postorderTwoStacksSteps() {
+ StepTable table = new StepTable("s1 (bottom..top)", "s2 (bottom..top)");
+ assertEquals(SampleTreeTranscriptTest.POSTORDER, IterativeTraversals.postorderTwoStacks(Trees.sample(), table));
+ assertEquals(18, table.size());
+ Transcript t = Transcript.start("04-postorder-two-stacks-steps.txt", "Iterative postorder with two stacks, step by step");
+ table.writeTo(t);
+ t.flush();
+ }
+
+ @Test
+ void postorderOneStackSteps() {
+ StepTable table = new StepTable(STACK, null);
+ assertEquals(SampleTreeTranscriptTest.POSTORDER, IterativeTraversals.postorderOneStack(Trees.sample(), table));
+ Transcript t = Transcript.start("05-postorder-one-stack-steps.txt", "Iterative postorder with one stack, step by step");
+ table.writeTo(t);
+ t.flush();
+ }
+
+ @Test
+ void levelOrderSteps() {
+ StepTable table = new StepTable("queue (front..back)", null);
+ assertEquals(SampleTreeTranscriptTest.LEVEL, LevelOrder.levelOrder(Trees.sample(), table));
+ assertEquals(9, table.size());
+ Transcript t = Transcript.start("06-level-order-queue-steps.txt", "Level order with a queue, step by step");
+ table.writeTo(t);
+ t.flush();
+ }
+
+ @Test
+ void morrisSteps() {
+ TreeNode root = Trees.sample();
+ String before = Trees.bracket(root);
+ StepTable table = new StepTable("threads (from node)", null);
+ assertEquals(SampleTreeTranscriptTest.INORDER, IterativeTraversals.morrisInorder(root, table));
+ assertEquals(before, Trees.bracket(root));
+ Transcript t = Transcript.start("07-morris-inorder-steps.txt", "Morris inorder on the sample tree, step by step");
+ table.writeTo(t);
+ t.line("");
+ t.line("tree before: " + before);
+ t.line("tree after : " + Trees.bracket(root));
+ t.flush();
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/StepTable.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/StepTable.java
new file mode 100644
index 0000000..84a969a
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/StepTable.java
@@ -0,0 +1,51 @@
+package com.ankurm.interviewlab.bintree.support;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.ankurm.interviewlab.bintree.StepListener;
+
+/** Collects the steps reported by a traversal and renders them as a fixed-width text table. */
+public final class StepTable implements StepListener {
+ public record Row(int step, String action, String a, String b, String out) {}
+
+ private final List rows = new ArrayList<>();
+ private final String headerA;
+ private final String headerB; // null for single-container traversals
+
+ public StepTable(String headerA, String headerB) {
+ this.headerA = headerA;
+ this.headerB = headerB;
+ }
+
+ @Override
+ public void step(String action, List a, List b, List output) {
+ rows.add(new Row(rows.size() + 1, action, format(a), format(b), format(output)));
+ }
+
+ public int size() {
+ return rows.size();
+ }
+
+ public List rows() {
+ return rows;
+ }
+
+ public static String format(List values) {
+ return values.stream().map(String::valueOf).collect(Collectors.joining(" ", "[", "]"));
+ }
+
+ public void writeTo(Transcript t) {
+ int wa = Math.max(headerA.length(), rows.stream().mapToInt(r -> r.action().length()).max().orElse(0));
+ int wb = Math.max(headerA.length(), rows.stream().mapToInt(r -> r.a().length()).max().orElse(0));
+ int wc = headerB == null ? 0
+ : Math.max(headerB.length(), rows.stream().mapToInt(r -> r.b().length()).max().orElse(0));
+ t.line(String.format("%-4s | %-" + wa + "s | %-" + wb + "s |%s %s", "step", "action", headerA,
+ headerB == null ? "" : String.format(" %-" + wc + "s |", headerB), "output so far"));
+ for (Row r : rows) {
+ t.line(String.format("%-4d | %-" + wa + "s | %-" + wb + "s |%s %s", r.step(), r.action(), r.a(),
+ headerB == null ? "" : String.format(" %-" + wc + "s |", r.b()), r.out()));
+ }
+ }
+}
diff --git a/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/Transcript.java b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/Transcript.java
new file mode 100644
index 0000000..09f89d1
--- /dev/null
+++ b/binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/Transcript.java
@@ -0,0 +1,43 @@
+package com.ankurm.interviewlab.bintree.support;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Writes output/NN-*.txt while the calling test asserts the same numbers it prints. */
+public final class Transcript {
+ private final Path path;
+ private final List lines = new ArrayList<>();
+
+ private Transcript(Path path, String header) {
+ this.path = path;
+ lines.add(header);
+ lines.add("=".repeat(Math.min(header.length(), 100)));
+ lines.add("");
+ }
+
+ public static Transcript start(String fileName, String header) {
+ Path dir = Path.of("output");
+ try {
+ Files.createDirectories(dir);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return new Transcript(dir.resolve(fileName), header);
+ }
+
+ public Transcript line(String text) { lines.add(text); return this; }
+ public Transcript blank() { lines.add(""); return this; }
+
+ public void flush() {
+ try {
+ Files.write(path, (String.join("\n", lines) + "\n").getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}