From 4a9021d93657e384626c5357d1bca0835aa02267 Mon Sep 17 00:00:00 2001 From: Ankur Mhatre Date: Fri, 6 Nov 2026 21:43:33 +0530 Subject: [PATCH] New posts --- README.md | 1 + binary-tree-traversals/README.md | 33 +++ .../output/01-sample-tree-all-traversals.txt | 21 ++ .../output/02-inorder-iterative-steps.txt | 22 ++ .../output/03-preorder-iterative-steps.txt | 13 + .../output/04-postorder-two-stacks-steps.txt | 22 ++ .../output/05-postorder-one-stack-steps.txt | 26 ++ .../output/06-level-order-queue-steps.txt | 13 + .../output/07-morris-inorder-steps.txt | 20 ++ .../output/08-height-diameter-sample.txt | 14 ++ .../output/09-diameter-visit-counts.txt | 11 + .../output/10-construct-from-traversals.txt | 21 ++ .../output/11-bst-inorder-sorted.txt | 8 + .../12-deep-recursion-default-stack.txt | 19 ++ .../output/13-deep-recursion-xss512k.txt | 19 ++ .../output/14-deep-recursion-xss4m.txt | 19 ++ binary-tree-traversals/pom.xml | 65 +++++ binary-tree-traversals/scripts/run-all.sh | 15 ++ .../bintree/DeepRecursionDemo.java | 73 ++++++ .../bintree/IterativeTraversals.java | 230 ++++++++++++++++++ .../interviewlab/bintree/LevelOrder.java | 101 ++++++++ .../bintree/RecursiveTraversals.java | 54 ++++ .../interviewlab/bintree/StepListener.java | 16 ++ .../interviewlab/bintree/TreeMetrics.java | 141 +++++++++++ .../ankurm/interviewlab/bintree/TreeNode.java | 18 ++ .../ankurm/interviewlab/bintree/Trees.java | 186 ++++++++++++++ .../bintree/ConstructTreeTest.java | 64 +++++ .../interviewlab/bintree/DeepTreeTest.java | 61 +++++ .../bintree/DiameterVisitCountTest.java | 47 ++++ .../bintree/RandomTreesEquivalenceTest.java | 147 +++++++++++ .../bintree/SampleTreeTranscriptTest.java | 106 ++++++++ .../bintree/StepTraceTranscriptTest.java | 78 ++++++ .../bintree/support/StepTable.java | 51 ++++ .../bintree/support/Transcript.java | 43 ++++ 34 files changed, 1778 insertions(+) create mode 100644 binary-tree-traversals/README.md create mode 100644 binary-tree-traversals/output/01-sample-tree-all-traversals.txt create mode 100644 binary-tree-traversals/output/02-inorder-iterative-steps.txt create mode 100644 binary-tree-traversals/output/03-preorder-iterative-steps.txt create mode 100644 binary-tree-traversals/output/04-postorder-two-stacks-steps.txt create mode 100644 binary-tree-traversals/output/05-postorder-one-stack-steps.txt create mode 100644 binary-tree-traversals/output/06-level-order-queue-steps.txt create mode 100644 binary-tree-traversals/output/07-morris-inorder-steps.txt create mode 100644 binary-tree-traversals/output/08-height-diameter-sample.txt create mode 100644 binary-tree-traversals/output/09-diameter-visit-counts.txt create mode 100644 binary-tree-traversals/output/10-construct-from-traversals.txt create mode 100644 binary-tree-traversals/output/11-bst-inorder-sorted.txt create mode 100644 binary-tree-traversals/output/12-deep-recursion-default-stack.txt create mode 100644 binary-tree-traversals/output/13-deep-recursion-xss512k.txt create mode 100644 binary-tree-traversals/output/14-deep-recursion-xss4m.txt create mode 100644 binary-tree-traversals/pom.xml create mode 100644 binary-tree-traversals/scripts/run-all.sh create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/DeepRecursionDemo.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/IterativeTraversals.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/LevelOrder.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/RecursiveTraversals.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java create mode 100644 binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/ConstructTreeTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DeepTreeTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/DiameterVisitCountTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/RandomTreesEquivalenceTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/SampleTreeTranscriptTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/StepTraceTranscriptTest.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/StepTable.java create mode 100644 binary-tree-traversals/src/test/java/com/ankurm/interviewlab/bintree/support/Transcript.java diff --git a/README.md b/README.md index 09cfd00..d0832b1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/binary-tree-traversals/README.md b/binary-tree-traversals/README.md new file mode 100644 index 0000000..fe865b8 --- /dev/null +++ b/binary-tree-traversals/README.md @@ -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. diff --git a/binary-tree-traversals/output/01-sample-tree-all-traversals.txt b/binary-tree-traversals/output/01-sample-tree-all-traversals.txt new file mode 100644 index 0000000..c79f184 --- /dev/null +++ b/binary-tree-traversals/output/01-sample-tree-all-traversals.txt @@ -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]] diff --git a/binary-tree-traversals/output/02-inorder-iterative-steps.txt b/binary-tree-traversals/output/02-inorder-iterative-steps.txt new file mode 100644 index 0000000..faad987 --- /dev/null +++ b/binary-tree-traversals/output/02-inorder-iterative-steps.txt @@ -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] diff --git a/binary-tree-traversals/output/03-preorder-iterative-steps.txt b/binary-tree-traversals/output/03-preorder-iterative-steps.txt new file mode 100644 index 0000000..4acc94f --- /dev/null +++ b/binary-tree-traversals/output/03-preorder-iterative-steps.txt @@ -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] diff --git a/binary-tree-traversals/output/04-postorder-two-stacks-steps.txt b/binary-tree-traversals/output/04-postorder-two-stacks-steps.txt new file mode 100644 index 0000000..082759d --- /dev/null +++ b/binary-tree-traversals/output/04-postorder-two-stacks-steps.txt @@ -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] diff --git a/binary-tree-traversals/output/05-postorder-one-stack-steps.txt b/binary-tree-traversals/output/05-postorder-one-stack-steps.txt new file mode 100644 index 0000000..0e9f670 --- /dev/null +++ b/binary-tree-traversals/output/05-postorder-one-stack-steps.txt @@ -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] diff --git a/binary-tree-traversals/output/06-level-order-queue-steps.txt b/binary-tree-traversals/output/06-level-order-queue-steps.txt new file mode 100644 index 0000000..efc6de2 --- /dev/null +++ b/binary-tree-traversals/output/06-level-order-queue-steps.txt @@ -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] diff --git a/binary-tree-traversals/output/07-morris-inorder-steps.txt b/binary-tree-traversals/output/07-morris-inorder-steps.txt new file mode 100644 index 0000000..f0af02e --- /dev/null +++ b/binary-tree-traversals/output/07-morris-inorder-steps.txt @@ -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))) diff --git a/binary-tree-traversals/output/08-height-diameter-sample.txt b/binary-tree-traversals/output/08-height-diameter-sample.txt new file mode 100644 index 0000000..78c63d4 --- /dev/null +++ b/binary-tree-traversals/output/08-height-diameter-sample.txt @@ -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 diff --git a/binary-tree-traversals/output/09-diameter-visit-counts.txt b/binary-tree-traversals/output/09-diameter-visit-counts.txt new file mode 100644 index 0000000..9ed17eb --- /dev/null +++ b/binary-tree-traversals/output/09-diameter-visit-counts.txt @@ -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 diff --git a/binary-tree-traversals/output/10-construct-from-traversals.txt b/binary-tree-traversals/output/10-construct-from-traversals.txt new file mode 100644 index 0000000..186745e --- /dev/null +++ b/binary-tree-traversals/output/10-construct-from-traversals.txt @@ -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] diff --git a/binary-tree-traversals/output/11-bst-inorder-sorted.txt b/binary-tree-traversals/output/11-bst-inorder-sorted.txt new file mode 100644 index 0000000..1193776 --- /dev/null +++ b/binary-tree-traversals/output/11-bst-inorder-sorted.txt @@ -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] diff --git a/binary-tree-traversals/output/12-deep-recursion-default-stack.txt b/binary-tree-traversals/output/12-deep-recursion-default-stack.txt new file mode 100644 index 0000000..93db9a2 --- /dev/null +++ b/binary-tree-traversals/output/12-deep-recursion-default-stack.txt @@ -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 diff --git a/binary-tree-traversals/output/13-deep-recursion-xss512k.txt b/binary-tree-traversals/output/13-deep-recursion-xss512k.txt new file mode 100644 index 0000000..eb93520 --- /dev/null +++ b/binary-tree-traversals/output/13-deep-recursion-xss512k.txt @@ -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 diff --git a/binary-tree-traversals/output/14-deep-recursion-xss4m.txt b/binary-tree-traversals/output/14-deep-recursion-xss4m.txt new file mode 100644 index 0000000..ebd8d5a --- /dev/null +++ b/binary-tree-traversals/output/14-deep-recursion-xss4m.txt @@ -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 diff --git a/binary-tree-traversals/pom.xml b/binary-tree-traversals/pom.xml new file mode 100644 index 0000000..17e2725 --- /dev/null +++ b/binary-tree-traversals/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + com.ankurm.interviewlab + binary-tree-traversals + 1.0.0 + jar + + java-interview-lab :: binary-tree-traversals + + 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. + + + + 25 + UTF-8 + 6.1.3 + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.platform + junit-platform-launcher + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.0 + + -Xmx512m -Duser.timezone=UTC -Dfile.encoding=UTF-8 + + + + + diff --git a/binary-tree-traversals/scripts/run-all.sh b/binary-tree-traversals/scripts/run-all.sh new file mode 100644 index 0000000..f718f93 --- /dev/null +++ b/binary-tree-traversals/scripts/run-all.sh @@ -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 diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/DeepRecursionDemo.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/DeepRecursionDemo.java new file mode 100644 index 0000000..b114e95 --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/DeepRecursionDemo.java @@ -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 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); + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/IterativeTraversals.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/IterativeTraversals.java new file mode 100644 index 0000000..de7d21c --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/IterativeTraversals.java @@ -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 inorder(TreeNode root) { + return inorder(root, null); + } + + public static List inorder(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + Deque 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 preorder(TreeNode root) { + return preorder(root, null); + } + + public static List preorder(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + if (root == null) { + return out; + } + Deque 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 postorderTwoStacks(TreeNode root) { + return postorderTwoStacks(root, null); + } + + public static List postorderTwoStacks(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + if (root == null) { + return out; + } + Deque first = new ArrayDeque<>(); + Deque 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 postorderOneStack(TreeNode root) { + return postorderOneStack(root, null); + } + + public static List postorderOneStack(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + Deque 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 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 morrisInorder(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + List 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 morrisPreorder(TreeNode root) { + List 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 a, + Iterable b, List out) { + if (listener == null) { + return; + } + listener.step(action, valuesBottomFirst(a), valuesBottomFirst(b), List.copyOf(out)); + } + + private static void emitThreads(StepListener listener, String action, List threads, + List 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 valuesBottomFirst(Iterable stack) { + List values = new ArrayList<>(); + if (stack != null) { + for (TreeNode n : stack) { + values.add(n.val); + } + } + java.util.Collections.reverse(values); + return values; + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/LevelOrder.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/LevelOrder.java new file mode 100644 index 0000000..70b5798 --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/LevelOrder.java @@ -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 levelOrder(TreeNode root) { + return levelOrder(root, null); + } + + public static List levelOrder(TreeNode root, StepListener listener) { + List out = new ArrayList<>(); + if (root == null) { + return out; + } + Deque 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 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> byLevel(TreeNode root) { + List> levels = new ArrayList<>(); + if (root == null) { + return levels; + } + Deque queue = new ArrayDeque<>(); + queue.offer(root); + while (!queue.isEmpty()) { + int sizeOfThisLevel = queue.size(); + List 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> zigzag(TreeNode root) { + List> levels = new ArrayList<>(); + if (root == null) { + return levels; + } + Deque queue = new ArrayDeque<>(); + queue.offer(root); + boolean leftToRight = true; + while (!queue.isEmpty()) { + int sizeOfThisLevel = queue.size(); + Deque 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; + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/RecursiveTraversals.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/RecursiveTraversals.java new file mode 100644 index 0000000..a3f5a8e --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/RecursiveTraversals.java @@ -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 inorder(TreeNode root) { + List out = new ArrayList<>(); + inorder(root, out); + return out; + } + + private static void inorder(TreeNode node, List out) { + if (node == null) { + return; + } + inorder(node.left, out); + out.add(node.val); + inorder(node.right, out); + } + + public static List preorder(TreeNode root) { + List out = new ArrayList<>(); + preorder(root, out); + return out; + } + + private static void preorder(TreeNode node, List out) { + if (node == null) { + return; + } + out.add(node.val); + preorder(node.left, out); + preorder(node.right, out); + } + + public static List postorder(TreeNode root) { + List out = new ArrayList<>(); + postorder(root, out); + return out; + } + + private static void postorder(TreeNode node, List out) { + if (node == null) { + return; + } + postorder(node.left, out); + postorder(node.right, out); + out.add(node.val); + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java new file mode 100644 index 0000000..a6ccf25 --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/StepListener.java @@ -0,0 +1,16 @@ +package com.ankurm.interviewlab.bintree; + +import java.util.List; + +/** + * Optional observer that the iterative traversals call after every push, pop or queue operation. + * The tables in the article are produced by this hook on the real implementations, not by a + * separate "teaching" copy of the algorithm. + * + *

{@code containerA} is the main stack (bottom first, top last) or the queue (front first). + * {@code containerB} is the second stack of the two-stack postorder; it is empty for the others. + */ +@FunctionalInterface +public interface StepListener { + void step(String action, List containerA, List containerB, List output); +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java new file mode 100644 index 0000000..013fa57 --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeMetrics.java @@ -0,0 +1,141 @@ +package com.ankurm.interviewlab.bintree; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +/** + * Size, leaves, height and diameter. Height is counted in NODES on the longest root-to-leaf path + * (empty tree = 0, single node = 1). Many textbooks count EDGES instead (single node = 0): subtract one. + * The same applies to diameter below. + */ +public final class TreeMetrics { + private TreeMetrics() {} + + public static int countNodes(TreeNode node) { + return node == null ? 0 : 1 + countNodes(node.left) + countNodes(node.right); + } + + public static int countLeaves(TreeNode node) { + if (node == null) { + return 0; + } + if (node.left == null && node.right == null) { + return 1; + } + return countLeaves(node.left) + countLeaves(node.right); + } + + public static int height(TreeNode node) { + if (node == null) { + return 0; + } + return 1 + Math.max(height(node.left), height(node.right)); + } + + /** Height without recursion: the number of levels a queue-based traversal walks through. */ + public static int heightIterative(TreeNode root) { + if (root == null) { + return 0; + } + int levels = 0; + Deque queue = new ArrayDeque<>(); + queue.offer(root); + while (!queue.isEmpty()) { + levels++; + for (int i = queue.size(); i > 0; i--) { + TreeNode node = queue.poll(); + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } + } + return levels; + } + + // ---------------------------------------------------------------- diameter + + /** + * The obvious solution: for every node, ask for the height of both sides (each a full walk of + * that subtree), then recurse into the children. O(n^2) in the worst case. + * {@code visits[0]} counts every node entered by either {@code height} or this method. + */ + public static int diameterNaive(TreeNode node, long[] visits) { + if (node == null) { + return 0; + } + visits[0]++; + int through = heightCounting(node.left, visits) + heightCounting(node.right, visits) + 1; + int inLeft = diameterNaive(node.left, visits); + int inRight = diameterNaive(node.right, visits); + return Math.max(through, Math.max(inLeft, inRight)); + } + + private static int heightCounting(TreeNode node, long[] visits) { + if (node == null) { + return 0; + } + visits[0]++; + return 1 + Math.max(heightCounting(node.left, visits), heightCounting(node.right, visits)); + } + + /** One post-order pass: each node returns its height and updates the best path through itself. */ + public static int diameterOnePass(TreeNode root, long[] visits) { + int[] best = {0}; + heightAndDiameter(root, best, visits); + return best[0]; + } + + private static int heightAndDiameter(TreeNode node, int[] best, long[] visits) { + if (node == null) { + return 0; + } + visits[0]++; + int left = heightAndDiameter(node.left, best, visits); + int right = heightAndDiameter(node.right, best, visits); + best[0] = Math.max(best[0], left + right + 1); + return 1 + Math.max(left, right); + } + + /** The values along one longest path (ties: prefers the left child), for drawing the diagram. */ + public static List diameterPath(TreeNode root) { + TreeNode[] bestNode = {null}; + int[] bestLen = {0}; + locateBest(root, bestNode, bestLen); + if (bestNode[0] == null) { + return List.of(); + } + List path = new ArrayList<>(deepestPath(bestNode[0].left)); + Collections.reverse(path); + path.add(bestNode[0].val); + path.addAll(deepestPath(bestNode[0].right)); + return path; + } + + private static int locateBest(TreeNode node, TreeNode[] bestNode, int[] bestLen) { + if (node == null) { + return 0; + } + int left = locateBest(node.left, bestNode, bestLen); + int right = locateBest(node.right, bestNode, bestLen); + if (left + right + 1 > bestLen[0]) { + bestLen[0] = left + right + 1; + bestNode[0] = node; + } + return 1 + Math.max(left, right); + } + + private static List deepestPath(TreeNode node) { + List path = new ArrayList<>(); + while (node != null) { + path.add(node.val); + node = height(node.left) >= height(node.right) ? node.left : node.right; + } + return path; + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java new file mode 100644 index 0000000..03b9052 --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/TreeNode.java @@ -0,0 +1,18 @@ +package com.ankurm.interviewlab.bintree; + +/** A binary tree node. Public fields on purpose: this is teaching code, not a library. */ +public final class TreeNode { + public int val; + public TreeNode left; + public TreeNode right; + + public TreeNode(int val) { + this.val = val; + } + + public TreeNode(int val, TreeNode left, TreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } +} diff --git a/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java new file mode 100644 index 0000000..c54582b --- /dev/null +++ b/binary-tree-traversals/src/main/java/com/ankurm/interviewlab/bintree/Trees.java @@ -0,0 +1,186 @@ +package com.ankurm.interviewlab.bintree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.function.Consumer; + +/** Tree builders used by the demos and tests, plus tree construction from traversals. */ +public final class Trees { + private Trees() {} + + /** + * The sample tree used by every diagram in the article (values 1..9): + *

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