New posts
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+230
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
+141
@@ -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();
|
||||
}
|
||||
}
|
||||
+64
@@ -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();
|
||||
}
|
||||
}
|
||||
+61
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
+47
@@ -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();
|
||||
}
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
+78
@@ -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();
|
||||
}
|
||||
}
|
||||
+51
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user