New posts

This commit is contained in:
2026-11-06 21:43:33 +05:30
parent 8d8d7956ba
commit 4a9021d936
34 changed files with 1778 additions and 0 deletions
+1
View File
@@ -16,6 +16,7 @@ One module per article. This repository currently holds:
| [`multithreading`](multithreading/) | Java Multithreading Programs for Interviews: Odd-Even Printing, Ping-Pong, Ordered Threads, Deadlock | this commit |
| [`disarium-number`](disarium-number/) | Disarium Number in Java: Program, Algorithm and All Disarium Numbers from 1 to 1000 | this commit |
| [`lru-cache`](lru-cache/) | LRU Cache in Java: LinkedHashMap, HashMap + Doubly Linked List and a Thread-Safe Version | published |
| [`binary-tree-traversals`](binary-tree-traversals/) | Binary Tree Traversals in Java: Inorder, Preorder, Postorder and Level Order (Recursive and Iterative) | this commit |
## `collections` module
+33
View File
@@ -0,0 +1,33 @@
# binary-tree-traversals
Runnable companion for the ankurm.com article
[Binary Tree Traversals in Java: Inorder, Preorder, Postorder and Level Order (Recursive and Iterative)](https://ankurm.com/binary-tree-traversal-java-inorder-preorder-postorder-level-order/).
Recursive and iterative inorder, preorder and postorder (two-stack and one-stack), Morris traversal,
level order (plain, by level, zigzag), height, diameter (naive O(n^2) vs one pass, with counted node
visits), tree construction from traversals, and a `StackOverflowError` demo on a deep skewed tree.
| Component | Version | Verified against |
|---|---|---|
| JDK | 25.0.5+8 (Temurin, LTS) | `java -version` |
| JUnit | 6.1.3 | `maven-metadata.xml` on Maven Central (same as the sibling modules) |
| Maven | 3.9.11 | `mvn -version` |
```bash
export JAVA_HOME=/path/to/jdk-25 && export PATH="$JAVA_HOME/bin:$PATH"
cd binary-tree-traversals
mvn test # 15 tests; rewrites output/01-11
./scripts/run-all.sh # also regenerates output/12-14 (StackOverflowError depth at three -Xss sizes)
```
| Source (`src/main/java/com/ankurm/interviewlab/bintree/`) | What it is |
|---|---|
| `TreeNode`, `Trees` | node class; sample tree, random/skewed/perfect/BST builders, build from inorder+preorder or inorder+postorder |
| `RecursiveTraversals` | the three textbook recursive orders |
| `IterativeTraversals` | explicit-stack inorder/preorder/postorder, Morris in/pre-order, `StepListener` hook for the step tables |
| `LevelOrder` | queue BFS, by-level grouping, zigzag |
| `TreeMetrics` | counts, height, diameter (naive and one pass, with visit counters) |
| `DeepRecursionDemo` | 1,000,000-node skewed tree: recursion overflows, iteration does not |
`output/12` to `output/14` are measurements: the overflow depth depends on the machine, the JVM and
`-Xss`, so they are not asserted. Everything else is asserted by the tests that write it.
@@ -0,0 +1,21 @@
Sample tree: every traversal, recursive and iterative
=====================================================
tree (value(left,right), - = no child): 1(2(4,5(7,-)),3(-,6(8,9)))
recursive inorder : [4, 2, 7, 5, 1, 3, 8, 6, 9]
recursive preorder : [1, 2, 4, 5, 7, 3, 6, 8, 9]
recursive postorder : [4, 7, 5, 2, 8, 9, 6, 3, 1]
iterative inorder : [4, 2, 7, 5, 1, 3, 8, 6, 9]
iterative preorder : [1, 2, 4, 5, 7, 3, 6, 8, 9]
iterative postorder (2 stacks): [4, 7, 5, 2, 8, 9, 6, 3, 1]
iterative postorder (1 stack) : [4, 7, 5, 2, 8, 9, 6, 3, 1]
Morris inorder : [4, 2, 7, 5, 1, 3, 8, 6, 9]
Morris preorder : [1, 2, 4, 5, 7, 3, 6, 8, 9]
tree after Morris : 1(2(4,5(7,-)),3(-,6(8,9))) (unchanged)
level order (queue) : [1, 2, 3, 4, 5, 6, 7, 8, 9]
level order by level : [[1], [2, 3], [4, 5, 6], [7, 8, 9]]
zigzag : [[1], [3, 2], [4, 5, 6], [9, 8, 7]]
@@ -0,0 +1,22 @@
Iterative inorder on the sample tree, step by step
==================================================
step | action | stack (bottom..top) | output so far
1 | push 1 | [1] | []
2 | push 2 | [1 2] | []
3 | push 4 | [1 2 4] | []
4 | pop 4, visit | [1 2] | [4]
5 | pop 2, visit | [1] | [4 2]
6 | push 5 | [1 5] | [4 2]
7 | push 7 | [1 5 7] | [4 2]
8 | pop 7, visit | [1 5] | [4 2 7]
9 | pop 5, visit | [1] | [4 2 7 5]
10 | pop 1, visit | [] | [4 2 7 5 1]
11 | push 3 | [3] | [4 2 7 5 1]
12 | pop 3, visit | [] | [4 2 7 5 1 3]
13 | push 6 | [6] | [4 2 7 5 1 3]
14 | push 8 | [6 8] | [4 2 7 5 1 3]
15 | pop 8, visit | [6] | [4 2 7 5 1 3 8]
16 | pop 6, visit | [] | [4 2 7 5 1 3 8 6]
17 | push 9 | [9] | [4 2 7 5 1 3 8 6]
18 | pop 9, visit | [] | [4 2 7 5 1 3 8 6 9]
@@ -0,0 +1,13 @@
Iterative preorder on the sample tree, step by step
===================================================
step | action | stack (bottom..top) | output so far
1 | pop 1, visit, push its children | [3 2] | [1]
2 | pop 2, visit, push its children | [3 5 4] | [1 2]
3 | pop 4, visit, push its children | [3 5] | [1 2 4]
4 | pop 5, visit, push its children | [3 7] | [1 2 4 5]
5 | pop 7, visit, push its children | [3] | [1 2 4 5 7]
6 | pop 3, visit, push its children | [6] | [1 2 4 5 7 3]
7 | pop 6, visit, push its children | [9 8] | [1 2 4 5 7 3 6]
8 | pop 8, visit, push its children | [9] | [1 2 4 5 7 3 6 8]
9 | pop 9, visit, push its children | [] | [1 2 4 5 7 3 6 8 9]
@@ -0,0 +1,22 @@
Iterative postorder with two stacks, step by step
=================================================
step | action | s1 (bottom..top) | s2 (bottom..top) | output so far
1 | move 1 to s2, push its children on s1 | [2 3] | [1] | []
2 | move 3 to s2, push its children on s1 | [2 6] | [1 3] | []
3 | move 6 to s2, push its children on s1 | [2 8 9] | [1 3 6] | []
4 | move 9 to s2, push its children on s1 | [2 8] | [1 3 6 9] | []
5 | move 8 to s2, push its children on s1 | [2] | [1 3 6 9 8] | []
6 | move 2 to s2, push its children on s1 | [4 5] | [1 3 6 9 8 2] | []
7 | move 5 to s2, push its children on s1 | [4 7] | [1 3 6 9 8 2 5] | []
8 | move 7 to s2, push its children on s1 | [4] | [1 3 6 9 8 2 5 7] | []
9 | move 4 to s2, push its children on s1 | [] | [1 3 6 9 8 2 5 7 4] | []
10 | pop 4 from s2 to output | [] | [1 3 6 9 8 2 5 7] | [4]
11 | pop 7 from s2 to output | [] | [1 3 6 9 8 2 5] | [4 7]
12 | pop 5 from s2 to output | [] | [1 3 6 9 8 2] | [4 7 5]
13 | pop 2 from s2 to output | [] | [1 3 6 9 8] | [4 7 5 2]
14 | pop 8 from s2 to output | [] | [1 3 6 9] | [4 7 5 2 8]
15 | pop 9 from s2 to output | [] | [1 3 6] | [4 7 5 2 8 9]
16 | pop 6 from s2 to output | [] | [1 3] | [4 7 5 2 8 9 6]
17 | pop 3 from s2 to output | [] | [1] | [4 7 5 2 8 9 6 3]
18 | pop 1 from s2 to output | [] | [] | [4 7 5 2 8 9 6 3 1]
@@ -0,0 +1,26 @@
Iterative postorder with one stack, step by step
================================================
step | action | stack (bottom..top) | output so far
1 | push 1 | [1] | []
2 | push 2 | [1 2] | []
3 | push 4 | [1 2 4] | []
4 | pop 4, visit | [1 2] | [4]
5 | peek 2: right child 5 not done, go right | [1 2] | [4]
6 | push 5 | [1 2 5] | [4]
7 | push 7 | [1 2 5 7] | [4]
8 | pop 7, visit | [1 2 5] | [4 7]
9 | pop 5, visit | [1 2] | [4 7 5]
10 | pop 2, visit | [1] | [4 7 5 2]
11 | peek 1: right child 3 not done, go right | [1] | [4 7 5 2]
12 | push 3 | [1 3] | [4 7 5 2]
13 | peek 3: right child 6 not done, go right | [1 3] | [4 7 5 2]
14 | push 6 | [1 3 6] | [4 7 5 2]
15 | push 8 | [1 3 6 8] | [4 7 5 2]
16 | pop 8, visit | [1 3 6] | [4 7 5 2 8]
17 | peek 6: right child 9 not done, go right | [1 3 6] | [4 7 5 2 8]
18 | push 9 | [1 3 6 9] | [4 7 5 2 8]
19 | pop 9, visit | [1 3 6] | [4 7 5 2 8 9]
20 | pop 6, visit | [1 3] | [4 7 5 2 8 9 6]
21 | pop 3, visit | [1] | [4 7 5 2 8 9 6 3]
22 | pop 1, visit | [] | [4 7 5 2 8 9 6 3 1]
@@ -0,0 +1,13 @@
Level order with a queue, step by step
======================================
step | action | queue (front..back) | output so far
1 | poll 1, visit, offer its children | [2 3] | [1]
2 | poll 2, visit, offer its children | [3 4 5] | [1 2]
3 | poll 3, visit, offer its children | [4 5 6] | [1 2 3]
4 | poll 4, visit, offer its children | [5 6] | [1 2 3 4]
5 | poll 5, visit, offer its children | [6 7] | [1 2 3 4 5]
6 | poll 6, visit, offer its children | [7 8 9] | [1 2 3 4 5 6]
7 | poll 7, visit, offer its children | [8 9] | [1 2 3 4 5 6 7]
8 | poll 8, visit, offer its children | [9] | [1 2 3 4 5 6 7 8]
9 | poll 9, visit, offer its children | [] | [1 2 3 4 5 6 7 8 9]
@@ -0,0 +1,20 @@
Morris inorder on the sample tree, step by step
===============================================
step | action | threads (from node) | output so far
1 | at 1: thread 5 -> 1, go left | [5] | []
2 | at 2: thread 4 -> 2, go left | [5 4] | []
3 | at 4: no left child, visit, go right | [5 4] | [4]
4 | at 2: thread from 4 found, remove it, visit, go right | [5] | [4 2]
5 | at 5: thread 7 -> 5, go left | [5 7] | [4 2]
6 | at 7: no left child, visit, go right | [5 7] | [4 2 7]
7 | at 5: thread from 7 found, remove it, visit, go right | [5] | [4 2 7 5]
8 | at 1: thread from 5 found, remove it, visit, go right | [] | [4 2 7 5 1]
9 | at 3: no left child, visit, go right | [] | [4 2 7 5 1 3]
10 | at 6: thread 8 -> 6, go left | [8] | [4 2 7 5 1 3]
11 | at 8: no left child, visit, go right | [8] | [4 2 7 5 1 3 8]
12 | at 6: thread from 8 found, remove it, visit, go right | [] | [4 2 7 5 1 3 8 6]
13 | at 9: no left child, visit, go right | [] | [4 2 7 5 1 3 8 6 9]
tree before: 1(2(4,5(7,-)),3(-,6(8,9)))
tree after : 1(2(4,5(7,-)),3(-,6(8,9)))
@@ -0,0 +1,14 @@
Sample tree: size, height and diameter
======================================
nodes : 9
leaves : 4
height, counted in nodes : 4
height, counted in edges : 3
height via level order (BFS) : 4
diameter, counted in nodes : 7
diameter, counted in edges : 6
one longest path : [7, 5, 2, 1, 3, 6, 8]
height of the subtree at node : {1=4, 2=3, 3=3, 4=1, 5=2, 6=2, 7=1, 8=1, 9=1}
node visits, naive O(n^2) way : 26
node visits, one-pass way : 9
@@ -0,0 +1,11 @@
Diameter: node visits, naive vs one pass (counted, not timed)
=============================================================
tree nodes naive visits one-pass diameter
sample (9) 9 26 9 7
perfect, 4 levels 15 49 15 7
perfect, 7 levels 127 769 127 13
perfect, 10 levels 1023 9217 1023 19
left-skewed 100 100 5050 100 100
left-skewed 1000 1000 500500 1000 1000
left-skewed 2000 2000 2001000 2000 2000
@@ -0,0 +1,21 @@
Constructing a tree from two traversals
=======================================
inorder : [4, 2, 5, 1, 6, 3]
preorder : [1, 2, 4, 5, 3, 6]
from inorder + preorder, one line per recursive call:
root 1 inorder slice [0..5] left size 3, right size 2
root 2 inorder slice [0..2] left size 1, right size 1
root 4 inorder slice [0..0] left size 0, right size 0
root 5 inorder slice [2..2] left size 0, right size 0
root 3 inorder slice [4..5] left size 1, right size 0
root 6 inorder slice [4..4] left size 0, right size 0
result : 1(2(4,5),3(6,-))
postorder of the result: [4, 5, 2, 6, 3, 1]
from inorder + postorder the same tree comes back: 1(2(4,5),3(6,-))
two different trees with the same preorder AND postorder:
1(2,-) preorder [1, 2] postorder [2, 1]
1(-,2) preorder [1, 2] postorder [2, 1]
@@ -0,0 +1,8 @@
Inorder of a binary search tree is sorted
=========================================
inserted in this order : [50, 30, 70, 20, 40, 60, 80]
tree : 50(30(20,40),70(60,80))
preorder : [50, 30, 20, 40, 70, 60, 80]
inorder : [20, 30, 40, 50, 60, 70, 80]
postorder : [20, 40, 30, 60, 80, 70, 50]
@@ -0,0 +1,19 @@
JVM: Eclipse Adoptium 25.0.5, ThreadStackSize=1024 KB, cores=2
Built a left-skewed tree with 1000000 nodes using a loop (no recursion).
Depth reached by a recursive probe before StackOverflowError (3 attempts in one JVM):
attempt 1: StackOverflowError after reaching depth 21258
attempt 2: StackOverflowError after reaching depth 29514
attempt 3: StackOverflowError after reaching depth 29514
RecursiveTraversals.inorder on the same tree:
caught java.lang.StackOverflowError
stack trace length: 1024 frames (the JVM truncates traces; the real depth is larger)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
...
IterativeTraversals.inorder on the same tree:
visited 1000000 nodes, first=1000000, last=1, no error
@@ -0,0 +1,19 @@
JVM: Eclipse Adoptium 25.0.5, ThreadStackSize=512 KB, cores=2
Built a left-skewed tree with 1000000 nodes using a loop (no recursion).
Depth reached by a recursive probe before StackOverflowError (3 attempts in one JVM):
attempt 1: StackOverflowError after reaching depth 5175
attempt 2: StackOverflowError after reaching depth 12829
attempt 3: StackOverflowError after reaching depth 13130
RecursiveTraversals.inorder on the same tree:
caught java.lang.StackOverflowError
stack trace length: 1024 frames (the JVM truncates traces; the real depth is larger)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
...
IterativeTraversals.inorder on the same tree:
visited 1000000 nodes, first=1000000, last=1, no error
@@ -0,0 +1,19 @@
JVM: Eclipse Adoptium 25.0.5, ThreadStackSize=4096 KB, cores=2
Built a left-skewed tree with 1000000 nodes using a loop (no recursion).
Depth reached by a recursive probe before StackOverflowError (3 attempts in one JVM):
attempt 1: StackOverflowError after reaching depth 51051
attempt 2: StackOverflowError after reaching depth 127818
attempt 3: StackOverflowError after reaching depth 127818
RecursiveTraversals.inorder on the same tree:
caught java.lang.StackOverflowError
stack trace length: 1024 frames (the JVM truncates traces; the real depth is larger)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
at com.ankurm.interviewlab.bintree.RecursiveTraversals.inorder(RecursiveTraversals.java:20)
...
IterativeTraversals.inorder on the same tree:
visited 1000000 nodes, first=1000000, last=1, no error
+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm.interviewlab</groupId>
<artifactId>binary-tree-traversals</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>java-interview-lab :: binary-tree-traversals</name>
<description>
Runnable companion code for ankurm.com's "Binary Tree Traversals in Java" article: recursive and
iterative inorder, preorder and postorder, Morris traversal, level order, height, diameter, tree
construction from traversals, and a StackOverflowError demonstration on a deep skewed tree.
</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>-Xmx512m -Duser.timezone=UTC -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Needs JDK 25 and Maven. The first Maven run must be online.
# 01-11 are written by JUnit tests that assert the same facts (deterministic).
# 12-14 are MEASUREMENTS of StackOverflowError depth: they depend on the machine, the JVM build
# and the -Xss setting, and the depth even varies between attempts inside one JVM.
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p output
mvn -B -q test
mvn -B -q compile
run() { java "$@" -cp target/classes com.ankurm.interviewlab.bintree.DeepRecursionDemo 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' ; }
run > output/12-deep-recursion-default-stack.txt
run -Xss512k > output/13-deep-recursion-xss512k.txt
run -Xss4m > output/14-deep-recursion-xss4m.txt
echo "done:"; ls -1 output
@@ -0,0 +1,73 @@
package com.ankurm.interviewlab.bintree;
import java.lang.management.ManagementFactory;
import java.util.List;
import com.sun.management.HotSpotDiagnosticMXBean;
/**
* Builds a 1,000,000-node left-skewed tree and traverses it recursively until the JVM throws
* StackOverflowError, then traverses the same tree iteratively. The depth reached depends on the
* thread stack size (-Xss), the JIT state and the frame layout, so it varies per machine and per run:
* this demo prints it, it does not assert it.
*/
public final class DeepRecursionDemo {
private static final int NODES = 1_000_000;
private static int deepestEntered;
private DeepRecursionDemo() {}
public static void main(String[] args) {
HotSpotDiagnosticMXBean vm = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
System.out.println("JVM: " + System.getProperty("java.vendor") + " " + System.getProperty("java.version")
+ ", ThreadStackSize=" + vm.getVMOption("ThreadStackSize").getValue() + " KB, cores="
+ Runtime.getRuntime().availableProcessors());
TreeNode root = Trees.leftSkewed(NODES);
System.out.println("Built a left-skewed tree with " + NODES + " nodes using a loop (no recursion).");
System.out.println();
System.out.println("Depth reached by a recursive probe before StackOverflowError (3 attempts in one JVM):");
for (int attempt = 1; attempt <= 3; attempt++) {
deepestEntered = 0;
try {
probe(root, 1);
System.out.println(" attempt " + attempt + ": finished without error, depth " + deepestEntered);
} catch (StackOverflowError e) {
System.out.println(" attempt " + attempt + ": StackOverflowError after reaching depth " + deepestEntered);
}
}
System.out.println();
System.out.println("RecursiveTraversals.inorder on the same tree:");
try {
RecursiveTraversals.inorder(root);
System.out.println(" finished without error");
} catch (StackOverflowError e) {
StackTraceElement[] trace = e.getStackTrace();
System.out.println(" caught " + e);
System.out.println(" stack trace length: " + trace.length + " frames (the JVM truncates traces; the real depth is larger)");
for (int i = 0; i < Math.min(4, trace.length); i++) {
System.out.println(" at " + trace[i]);
}
System.out.println(" ...");
}
System.out.println();
System.out.println("IterativeTraversals.inorder on the same tree:");
List<Integer> result = IterativeTraversals.inorder(root);
System.out.println(" visited " + result.size() + " nodes, first=" + result.get(0) + ", last="
+ result.get(result.size() - 1) + ", no error");
}
/** Same shape as the recursive inorder, but remembers how deep it got. */
private static void probe(TreeNode node, int depth) {
if (node == null) {
return;
}
if (depth > deepestEntered) {
deepestEntered = depth;
}
probe(node.left, depth + 1);
probe(node.right, depth + 1);
}
}
@@ -0,0 +1,230 @@
package com.ankurm.interviewlab.bintree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* Depth-first traversals with an explicit stack instead of the JVM call stack, plus Morris
* traversal, which needs no stack at all. Every method has an overload taking a
* {@link StepListener} so the article's step tables come from the real code.
*/
public final class IterativeTraversals {
private IterativeTraversals() {}
// ---------------------------------------------------------------- inorder
public static List<Integer> inorder(TreeNode root) {
return inorder(root, null);
}
public static List<Integer> inorder(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) { // go as far left as possible, remembering the way back
stack.push(current);
emit(listener, "push " + current.val, stack, null, out);
current = current.left;
}
current = stack.pop(); // nothing further left: this node is next
out.add(current.val);
emit(listener, "pop " + current.val + ", visit", stack, null, out);
current = current.right; // then the whole right subtree
}
return out;
}
// --------------------------------------------------------------- preorder
public static List<Integer> preorder(TreeNode root) {
return preorder(root, null);
}
public static List<Integer> preorder(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
if (root == null) {
return out;
}
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
out.add(node.val);
if (node.right != null) {
stack.push(node.right); // pushed first so it is popped last
}
if (node.left != null) {
stack.push(node.left);
}
emit(listener, "pop " + node.val + ", visit, push its children", stack, null, out);
}
return out;
}
// ------------------------------------------------------- postorder, 2 stacks
public static List<Integer> postorderTwoStacks(TreeNode root) {
return postorderTwoStacks(root, null);
}
public static List<Integer> postorderTwoStacks(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
if (root == null) {
return out;
}
Deque<TreeNode> first = new ArrayDeque<>();
Deque<TreeNode> second = new ArrayDeque<>();
first.push(root);
while (!first.isEmpty()) { // phase 1: root-right-left order lands on `second`
TreeNode node = first.pop();
second.push(node);
if (node.left != null) {
first.push(node.left);
}
if (node.right != null) {
first.push(node.right);
}
emit(listener, "move " + node.val + " to s2, push its children on s1", first, second, out);
}
while (!second.isEmpty()) { // phase 2: popping reverses it into left-right-root
TreeNode node = second.pop();
out.add(node.val);
emit(listener, "pop " + node.val + " from s2 to output", first, second, out);
}
return out;
}
// ------------------------------------------------------- postorder, 1 stack
public static List<Integer> postorderOneStack(TreeNode root) {
return postorderOneStack(root, null);
}
public static List<Integer> postorderOneStack(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode current = root;
TreeNode lastVisited = null; // the node we most recently wrote to the output
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
emit(listener, "push " + current.val, stack, null, out);
current = current.left;
}
TreeNode top = stack.peek();
if (top.right != null && top.right != lastVisited) {
emit(listener, "peek " + top.val + ": right child " + top.right.val + " not done, go right",
stack, null, out);
current = top.right; // right subtree still unvisited
} else {
stack.pop(); // both children finished (or absent)
out.add(top.val);
lastVisited = top;
emit(listener, "pop " + top.val + ", visit", stack, null, out);
}
}
return out;
}
// ------------------------------------------------------------------ Morris
public static List<Integer> morrisInorder(TreeNode root) {
return morrisInorder(root, null);
}
/**
* Inorder in O(1) extra space by temporarily threading each node's inorder predecessor
* back to it. The tree is restored before the method returns. For this method the listener's
* {@code containerA} holds the nodes whose right pointer is currently a temporary thread.
*/
public static List<Integer> morrisInorder(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
List<Integer> threads = new ArrayList<>();
TreeNode current = root;
while (current != null) {
if (current.left == null) {
out.add(current.val);
emitThreads(listener, "at " + current.val + ": no left child, visit, go right", threads, out);
current = current.right;
} else {
TreeNode predecessor = current.left;
while (predecessor.right != null && predecessor.right != current) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
predecessor.right = current; // create the thread
threads.add(predecessor.val);
emitThreads(listener, "at " + current.val + ": thread " + predecessor.val + " -> " + current.val
+ ", go left", threads, out);
current = current.left;
} else {
predecessor.right = null; // thread found: left side finished, remove it
threads.remove(Integer.valueOf(predecessor.val));
out.add(current.val);
emitThreads(listener, "at " + current.val + ": thread from " + predecessor.val
+ " found, remove it, visit, go right", threads, out);
current = current.right;
}
}
}
return out;
}
public static List<Integer> morrisPreorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
TreeNode current = root;
while (current != null) {
if (current.left == null) {
out.add(current.val);
current = current.right;
} else {
TreeNode predecessor = current.left;
while (predecessor.right != null && predecessor.right != current) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
out.add(current.val); // preorder visits on the FIRST arrival
predecessor.right = current;
current = current.left;
} else {
predecessor.right = null;
current = current.right;
}
}
}
return out;
}
// ---------------------------------------------------------------- helpers
private static void emit(StepListener listener, String action, Iterable<TreeNode> a,
Iterable<TreeNode> b, List<Integer> out) {
if (listener == null) {
return;
}
listener.step(action, valuesBottomFirst(a), valuesBottomFirst(b), List.copyOf(out));
}
private static void emitThreads(StepListener listener, String action, List<Integer> threads,
List<Integer> out) {
if (listener == null) {
return;
}
listener.step(action, List.copyOf(threads), List.of(), List.copyOf(out));
}
/** ArrayDeque iterates top-of-stack first when used with push; we print bottom first. */
private static List<Integer> valuesBottomFirst(Iterable<TreeNode> stack) {
List<Integer> values = new ArrayList<>();
if (stack != null) {
for (TreeNode n : stack) {
values.add(n.val);
}
}
java.util.Collections.reverse(values);
return values;
}
}
@@ -0,0 +1,101 @@
package com.ankurm.interviewlab.bintree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/** Breadth-first traversals: a queue instead of a stack. */
public final class LevelOrder {
private LevelOrder() {}
public static List<Integer> levelOrder(TreeNode root) {
return levelOrder(root, null);
}
public static List<Integer> levelOrder(TreeNode root, StepListener listener) {
List<Integer> out = new ArrayList<>();
if (root == null) {
return out;
}
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
out.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (listener != null) {
List<Integer> snapshot = new ArrayList<>();
for (TreeNode n : queue) {
snapshot.add(n.val); // ArrayDeque iterates front to back
}
listener.step("poll " + node.val + ", visit, offer its children", snapshot, List.of(),
List.copyOf(out));
}
}
return out;
}
/** One list per level: the trick is to freeze {@code queue.size()} before the inner loop. */
public static List<List<Integer>> byLevel(TreeNode root) {
List<List<Integer>> levels = new ArrayList<>();
if (root == null) {
return levels;
}
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int sizeOfThisLevel = queue.size();
List<Integer> level = new ArrayList<>(sizeOfThisLevel);
for (int i = 0; i < sizeOfThisLevel; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
levels.add(level);
}
return levels;
}
/** Left-to-right on level 0, right-to-left on level 1, and so on. */
public static List<List<Integer>> zigzag(TreeNode root) {
List<List<Integer>> levels = new ArrayList<>();
if (root == null) {
return levels;
}
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
boolean leftToRight = true;
while (!queue.isEmpty()) {
int sizeOfThisLevel = queue.size();
Deque<Integer> level = new ArrayDeque<>();
for (int i = 0; i < sizeOfThisLevel; i++) {
TreeNode node = queue.poll();
if (leftToRight) {
level.addLast(node.val);
} else {
level.addFirst(node.val);
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
levels.add(new ArrayList<>(level));
leftToRight = !leftToRight;
}
return levels;
}
}
@@ -0,0 +1,54 @@
package com.ankurm.interviewlab.bintree;
import java.util.ArrayList;
import java.util.List;
/** The three depth-first orders, written the way textbooks write them. */
public final class RecursiveTraversals {
private RecursiveTraversals() {}
public static List<Integer> inorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
inorder(root, out);
return out;
}
private static void inorder(TreeNode node, List<Integer> out) {
if (node == null) {
return;
}
inorder(node.left, out);
out.add(node.val);
inorder(node.right, out);
}
public static List<Integer> preorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
preorder(root, out);
return out;
}
private static void preorder(TreeNode node, List<Integer> out) {
if (node == null) {
return;
}
out.add(node.val);
preorder(node.left, out);
preorder(node.right, out);
}
public static List<Integer> postorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
postorder(root, out);
return out;
}
private static void postorder(TreeNode node, List<Integer> out) {
if (node == null) {
return;
}
postorder(node.left, out);
postorder(node.right, out);
out.add(node.val);
}
}
@@ -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.
*
* <p>{@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<Integer> containerA, List<Integer> containerB, List<Integer> output);
}
@@ -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<TreeNode> 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<Integer> diameterPath(TreeNode root) {
TreeNode[] bestNode = {null};
int[] bestLen = {0};
locateBest(root, bestNode, bestLen);
if (bestNode[0] == null) {
return List.of();
}
List<Integer> 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<Integer> deepestPath(TreeNode node) {
List<Integer> path = new ArrayList<>();
while (node != null) {
path.add(node.val);
node = height(node.left) >= height(node.right) ? node.left : node.right;
}
return path;
}
}
@@ -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;
}
}
@@ -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):
* <pre>
* 1
* / \
* 2 3
* / \ \
* 4 5 6
* / / \
* 7 8 9
* </pre>
*/
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<Integer> 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<TreeNode> 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<String> trace) {
Map<Integer, Integer> 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<Integer, Integer> position,
Consumer<String> 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<Integer, Integer> 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<Integer, Integer> 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<Integer> list) {
return list.stream().mapToInt(Integer::intValue).toArray();
}
}
@@ -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<Integer> 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();
}
}
@@ -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<Throwable> recursiveOutcome = new AtomicReference<>();
AtomicReference<List<Integer>> iterative = new AtomicReference<>();
AtomicReference<List<Integer>> iterativePost = new AtomicReference<>();
AtomicReference<List<List<Integer>>> 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<Throwable> ex = new AtomicReference<>();
t.setUncaughtExceptionHandler((th, e) -> ex.set(e));
t.start();
t.join();
throw ex.get();
});
}
}
@@ -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();
}
}
@@ -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<Integer> in = RecursiveTraversals.inorder(root);
List<Integer> pre = RecursiveTraversals.preorder(root);
List<Integer> 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<List<Integer>> levels = LevelOrder.byLevel(root);
List<Integer> 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<List<Integer>> zig = LevelOrder.zigzag(root);
assertEquals(levels.size(), zig.size());
for (int i = 0; i < levels.size(); i++) {
List<Integer> 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<TreeNode> allNodes(TreeNode root) {
List<TreeNode> nodes = new ArrayList<>();
Deque<TreeNode> 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<TreeNode, List<TreeNode>> 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<TreeNode, Integer> dist = new IdentityHashMap<>();
Deque<TreeNode> 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;
}
}
@@ -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<Integer> INORDER = List.of(4, 2, 7, 5, 1, 3, 8, 6, 9);
static final List<Integer> PREORDER = List.of(1, 2, 4, 5, 7, 3, 6, 8, 9);
static final List<Integer> POSTORDER = List.of(4, 7, 5, 2, 8, 9, 6, 3, 1);
static final List<Integer> 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<Integer> 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<Integer, Integer> 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<Integer, Integer> into) {
if (node == null) {
return;
}
into.put(node.val, TreeMetrics.height(node));
collectHeights(node.left, into);
collectHeights(node.right, into);
}
}
@@ -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();
}
}
@@ -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<Row> 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<Integer> a, List<Integer> b, List<Integer> output) {
rows.add(new Row(rows.size() + 1, action, format(a), format(b), format(output)));
}
public int size() {
return rows.size();
}
public List<Row> rows() {
return rows;
}
public static String format(List<Integer> 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()));
}
}
}
@@ -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<String> 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);
}
}
}