diff --git a/README.md b/README.md
index d0832b1..1224978 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ One module per article. This repository currently holds:
| [`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 |
+| [`sliding-window`](sliding-window/) | Sliding Window and Two Pointers in Java: 12 Interview Problems Solved | this commit |
## `collections` module
diff --git a/sliding-window/README.md b/sliding-window/README.md
new file mode 100644
index 0000000..a6acf28
--- /dev/null
+++ b/sliding-window/README.md
@@ -0,0 +1,75 @@
+# sliding-window
+
+Companion code for the ankurm.com article **Sliding Window and Two Pointers in Java: 12 Interview Problems Solved**.
+
+Twelve classic interview problems solved with the two patterns, each cross-checked against an
+obviously-correct brute-force version on seeded random inputs. Every figure and console line in
+the article is quoted from a file in `output/`, which the JUnit tests write while asserting the
+same numbers. There is no `docs/` folder: the explanations live in the article itself.
+
+## Versions
+
+| Component | Version | Verified against |
+|---|---|---|
+| JDK | Temurin 25.0.5+8 (LTS) | `java -version` |
+| JUnit | 6.1.3 | `junit-bom` `maven-metadata.xml` on Maven Central (latest GA) |
+| Maven | 3.9.x | `mvn -version` |
+
+## Quickstart
+
+```bash
+cd sliding-window
+export JAVA_HOME=/path/to/jdk-25 PATH="$JAVA_HOME/bin:$PATH"
+./scripts/run-all.sh # runs 11 tests and regenerates output/*.txt
+```
+
+The first Maven run must be online so the plugins land in the local cache.
+
+## The twelve problems
+
+| # | Problem | Pattern | Method |
+|---|---|---|---|
+| 1 | Maximum sum of k consecutive elements | fixed window | `FixedWindow.maxSumSubarray` |
+| 2 | Permutation in string | fixed window + counts | `FixedWindow.permutationInString` |
+| 3 | Sliding window maximum | fixed window + monotonic deque | `FixedWindow.slidingWindowMaximum` |
+| 4 | Longest substring without repeating characters | variable window | `VariableWindow.longestUniqueSubstring` |
+| 5 | Longest substring with at most K distinct characters | variable window | `VariableWindow.longestKDistinct` |
+| 6 | Longest repeating character replacement | variable window | `VariableWindow.characterReplacement` |
+| 7 | Max consecutive ones III | variable window | `VariableWindow.longestOnes` |
+| 8 | Minimum size subarray sum | shrinking window | `VariableWindow.minSubArrayLen` |
+| 9 | Minimum window substring | shrinking window | `VariableWindow.minWindow` |
+| 10 | Two Sum II (sorted input) | opposite-end pointers | `TwoPointers.twoSumSorted` |
+| 11 | 3Sum | sort + pointers | `TwoPointers.threeSum` |
+| 12 | Container with most water | opposite-end pointers | `TwoPointers.maxArea` |
+
+## Source layout
+
+```
+sliding-window/
+├── pom.xml
+├── scripts/run-all.sh
+├── src/main/java/com/ankurm/interviewlab/slidingwindow/
+│ ├── FixedWindow.java problems 1-3
+│ ├── VariableWindow.java problems 4-9
+│ ├── TwoPointers.java problems 10-12
+│ ├── BruteForce.java slow, obviously-correct references
+│ ├── DryRuns.java instrumented copies that record every step
+│ ├── Complexity.java operation counters (counted, not timed)
+│ └── Pitfalls.java four plausible-looking wrong versions
+├── src/test/java/.../ SolutionsTest, RandomAgreementTest, DryRunTest, ComplexityTest, PitfallsTest
+└── output/*.txt captured real output
+```
+
+## Captured output (`output/`)
+
+| File | What it shows |
+|---|---|
+| `01-fixed-window-trace.txt` | max-sum window sliding over `[2,1,5,1,3,2]` |
+| `02-longest-unique-trace.txt` | left pointer jumps on `abcabcbb` and the `abba` case |
+| `03-min-window-trace.txt` | grow until valid, shrink until invalid on `ADOBECODEBANC` |
+| `04-sliding-max-deque-trace.txt` | the monotonic deque contents at every step |
+| `05-two-pointers-trace.txt` | Two Sum II and Container With Most Water |
+| `06-all-twelve-results.txt` | one example per problem beside the brute-force answer |
+| `07-random-agreement.txt` | 24,000 seeded comparisons, 0 disagreements |
+| `08-operation-counts.txt` | brute force vs window operation counts at n = 1k..8k |
+| `09-pitfalls.txt` | four classic mistakes, each reproduced |
diff --git a/sliding-window/output/01-fixed-window-trace.txt b/sliding-window/output/01-fixed-window-trace.txt
new file mode 100644
index 0000000..5ae6b53
--- /dev/null
+++ b/sliding-window/output/01-fixed-window-trace.txt
@@ -0,0 +1,9 @@
+Max sum of 3 consecutive elements in [2, 1, 5, 1, 3, 2]
+=======================================================
+
+first window [2, 1, 5] sum=8 best=8
+right=3 +1 -2 window=[1, 5, 1] sum=7 best=8
+right=4 +3 -1 window=[5, 1, 3] sum=9 best=9
+right=5 +2 -5 window=[1, 3, 2] sum=6 best=9
+
+answer = 9
diff --git a/sliding-window/output/02-longest-unique-trace.txt b/sliding-window/output/02-longest-unique-trace.txt
new file mode 100644
index 0000000..b4fb526
--- /dev/null
+++ b/sliding-window/output/02-longest-unique-trace.txt
@@ -0,0 +1,21 @@
+Longest substring without repeating characters
+==============================================
+
+input "abcabcbb"
+ right=0 'a' left=0 window="a" best=1
+ right=1 'b' left=0 window="ab" best=2
+ right=2 'c' left=0 window="abc" best=3
+ right=3 'a' left=1 window="bca" best=3 seen at 0 inside the window, left 0 -> 1
+ right=4 'b' left=2 window="cab" best=3 seen at 1 inside the window, left 1 -> 2
+ right=5 'c' left=3 window="abc" best=3 seen at 2 inside the window, left 2 -> 3
+ right=6 'b' left=5 window="cb" best=3 seen at 4 inside the window, left 3 -> 5
+ right=7 'b' left=7 window="b" best=3 seen at 6 inside the window, left 5 -> 7
+ answer = 3
+
+input "abba"
+ right=0 'a' left=0 window="a" best=1
+ right=1 'b' left=0 window="ab" best=2
+ right=2 'b' left=2 window="b" best=2 seen at 1 inside the window, left 0 -> 2
+ right=3 'a' left=2 window="ba" best=2 seen at 0 but that is left of the window, left stays 2
+ answer = 2
+
diff --git a/sliding-window/output/03-min-window-trace.txt b/sliding-window/output/03-min-window-trace.txt
new file mode 100644
index 0000000..852be07
--- /dev/null
+++ b/sliding-window/output/03-min-window-trace.txt
@@ -0,0 +1,21 @@
+Minimum window substring: s="ADOBECODEBANC" t="ABC"
+===================================================
+
+right=5 'C' window="ADOBEC" is VALID
+ new best "ADOBEC" (length 6)
+ drop 'A' -> window="DOBEC" no longer valid, missing=1
+right=10 'A' window="DOBECODEBA" is VALID
+ drop 'D' -> window="OBECODEBA" still valid
+ drop 'O' -> window="BECODEBA" still valid
+ drop 'B' -> window="ECODEBA" still valid
+ drop 'E' -> window="CODEBA" still valid
+ drop 'C' -> window="ODEBA" no longer valid, missing=1
+right=12 'C' window="ODEBANC" is VALID
+ drop 'O' -> window="DEBANC" still valid
+ drop 'D' -> window="EBANC" still valid
+ new best "EBANC" (length 5)
+ drop 'E' -> window="BANC" still valid
+ new best "BANC" (length 4)
+ drop 'B' -> window="ANC" no longer valid, missing=1
+
+answer = "BANC"
diff --git a/sliding-window/output/04-sliding-max-deque-trace.txt b/sliding-window/output/04-sliding-max-deque-trace.txt
new file mode 100644
index 0000000..41ca7e5
--- /dev/null
+++ b/sliding-window/output/04-sliding-max-deque-trace.txt
@@ -0,0 +1,13 @@
+Sliding window maximum, k=3, input [1, 3, -1, -3, 5, 3, 6, 7]
+=============================================================
+
+right=0 a[right]=1 deque(value@index)=[1@0]
+right=1 a[right]=3 deque(value@index)=[3@1]
+right=2 a[right]=-1 deque(value@index)=[3@1, -1@2] -> window max 3
+right=3 a[right]=-3 deque(value@index)=[3@1, -1@2, -3@3] -> window max 3
+right=4 a[right]=5 deque(value@index)=[5@4] -> window max 5
+right=5 a[right]=3 deque(value@index)=[5@4, 3@5] -> window max 5
+right=6 a[right]=6 deque(value@index)=[6@6] -> window max 6
+right=7 a[right]=7 deque(value@index)=[7@7] -> window max 7
+
+answer = [3, 3, 5, 5, 6, 7]
diff --git a/sliding-window/output/05-two-pointers-trace.txt b/sliding-window/output/05-two-pointers-trace.txt
new file mode 100644
index 0000000..2672950
--- /dev/null
+++ b/sliding-window/output/05-two-pointers-trace.txt
@@ -0,0 +1,21 @@
+Opposite-end two pointers
+=========================
+
+Two Sum II: sorted [1, 3, 4, 6, 8, 11], target 10
+ lo=0 hi=5 1 + 11 = 12 > 10 hi--
+ lo=0 hi=4 1 + 8 = 9 < 10 lo++
+ lo=1 hi=4 3 + 8 = 11 > 10 hi--
+ lo=1 hi=3 3 + 6 = 9 < 10 lo++
+ lo=2 hi=3 4 + 6 = 10 == 10 found
+ answer = indices [2, 3]
+
+Container With Most Water: heights [1, 8, 6, 2, 5, 4, 8, 3, 7]
+ lo=0 hi=8 min(1,7) x 8 = 8 best=8 lo++ (left line is shorter)
+ lo=1 hi=8 min(8,7) x 7 = 49 best=49 hi-- (right line is not taller)
+ lo=1 hi=7 min(8,3) x 6 = 18 best=49 hi-- (right line is not taller)
+ lo=1 hi=6 min(8,8) x 5 = 40 best=49 hi-- (right line is not taller)
+ lo=1 hi=5 min(8,4) x 4 = 16 best=49 hi-- (right line is not taller)
+ lo=1 hi=4 min(8,5) x 3 = 15 best=49 hi-- (right line is not taller)
+ lo=1 hi=3 min(8,2) x 2 = 4 best=49 hi-- (right line is not taller)
+ lo=1 hi=2 min(8,6) x 1 = 6 best=49 hi-- (right line is not taller)
+ answer = 49
diff --git a/sliding-window/output/06-all-twelve-results.txt b/sliding-window/output/06-all-twelve-results.txt
new file mode 100644
index 0000000..6d5bdea
--- /dev/null
+++ b/sliding-window/output/06-all-twelve-results.txt
@@ -0,0 +1,15 @@
+Twelve problems: optimised answer vs brute-force answer on one example each
+===========================================================================
+
+ 1. maxSumSubarray([2, 1, 5, 1, 3, 2], 3) = 9 brute = 9
+ 2. permutationInString("ab", "eidbaooo") = true; ("ab", "eidboaoo") = false brute = true / false
+ 3. slidingWindowMaximum([1, 3, -1, -3, 5, 3, 6, 7], 3) = [3, 3, 5, 5, 6, 7]
+ 4. longestUniqueSubstring("abcabcbb") = 3 brute = 3
+ 5. longestKDistinct("eceba", 2) = 3 brute = 3
+ 6. characterReplacement("AABABBA", 1) = 4 brute = 4
+ 7. longestOnes([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 2) = 6 brute = 6
+ 8. minSubArrayLen(7, [2, 3, 1, 2, 4, 3]) = 2 brute = 2
+ 9. minWindow("ADOBECODEBANC", "ABC") = "BANC"; minWindow("a", "aa") = "" brute = "BANC"
+10. twoSumSorted([2, 7, 11, 15], 9) = [0, 1]; target 100 = [] (none)
+11. threeSum([-1, 0, 1, 2, -1, -4]) = [[-1, -1, 2], [-1, 0, 1]]
+12. maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]) = 49 brute = 49
diff --git a/sliding-window/output/07-random-agreement.txt b/sliding-window/output/07-random-agreement.txt
new file mode 100644
index 0000000..70edb5c
--- /dev/null
+++ b/sliding-window/output/07-random-agreement.txt
@@ -0,0 +1,17 @@
+Randomized agreement with brute force (seed 2026, 2000 cases per problem)
+=========================================================================
+
+ 1. maxSumSubarray 2000 cases, 0 disagreements
+ 2. permutationInString 2000 cases, 0 disagreements
+ 3. slidingWindowMaximum 2000 cases, 0 disagreements
+ 4. longestUniqueSubstring 2000 cases, 0 disagreements
+ 5. longestKDistinct 2000 cases, 0 disagreements
+ 6. characterReplacement 2000 cases, 0 disagreements
+ 7. longestOnes 2000 cases, 0 disagreements
+ 8. minSubArrayLen 2000 cases, 0 disagreements
+ 9. minWindow 2000 cases, 0 disagreements
+10. twoSumSorted 2000 cases, 0 disagreements
+11. threeSum 2000 cases, 0 disagreements
+12. maxArea 2000 cases, 0 disagreements
+
+total: 24000 comparisons (any disagreement would have failed the build)
diff --git a/sliding-window/output/08-operation-counts.txt b/sliding-window/output/08-operation-counts.txt
new file mode 100644
index 0000000..d214d75
--- /dev/null
+++ b/sliding-window/output/08-operation-counts.txt
@@ -0,0 +1,24 @@
+Operation counts, brute force vs window / two pointers (counted, not timed)
+===========================================================================
+
+Max sum of k=100 consecutive elements: element reads
+ n brute force sliding window ratio
+ 1000 90,100 1,900 47.4x
+ 2000 190,100 3,900 48.7x
+ 4000 390,100 7,900 49.4x
+ 8000 790,100 15,900 49.7x
+
+Pair search, target absent (worst case) in a sorted array 0..n-1: checks / steps
+ n all pairs two pointers
+ 1000 499,500 999
+ 2000 1,999,000 1,999
+ 4000 7,998,000 3,999
+ 8000 31,996,000 7,999
+
+minWindow on random strings over A-D, t="ABCD" (seed 42): pointer moves
+The loop is nested but each pointer only moves forward, so total moves stay linear.
+ n right moves left moves valid windows
+ 1000 1,000 996 996
+ 2000 2,000 1,996 1,996
+ 4000 4,000 3,996 3,996
+ 8000 8,000 7,993 7,993
diff --git a/sliding-window/output/09-pitfalls.txt b/sliding-window/output/09-pitfalls.txt
new file mode 100644
index 0000000..31cbc19
--- /dev/null
+++ b/sliding-window/output/09-pitfalls.txt
@@ -0,0 +1,18 @@
+Four classic mistakes, each reproduced
+======================================
+
+1. longest substring without repeats, left = lastSeen + 1 WITHOUT Math.max
+ input "abba": buggy = 3 correct = 2
+ at the final 'a', lastSeen('a') = 0, so the buggy code moves left from 2 back to 1
+
+2. minimum-size subarray window used on input that contains negatives
+ target 3, input [-2, -2, 3]: window = 0 ("none") brute force = 1 ([3])
+ the running sum is -1 when the 3 arrives, so the window never looks valid
+
+3. 3Sum without skipping duplicates
+ input [-1, 0, 1, 2, -1, -4]
+ buggy = [[-1, -1, 2], [-1, 0, 1], [-1, 0, 1]]
+ correct = [[-1, -1, 2], [-1, 0, 1]]
+
+4. two pointers on an array that is NOT sorted
+ input [3, 2, 4], target 6: twoSumSorted = [] but 2 + 4 = 6 exists (brute force: true)
diff --git a/sliding-window/pom.xml b/sliding-window/pom.xml
new file mode 100644
index 0000000..53b9de4
--- /dev/null
+++ b/sliding-window/pom.xml
@@ -0,0 +1,64 @@
+
+
+ 4.0.0
+ com.ankurm.interviewlab
+ sliding-window
+ 1.0.0
+ jar
+
+ java-interview-lab :: sliding-window
+
+ Runnable companion code for ankurm.com's "Sliding Window and Two Pointers in Java" article: 12 interview
+ problems, brute-force cross-checks, dry-run traces, operation counts and the classic pitfalls.
+
+
+
+ 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/sliding-window/scripts/run-all.sh b/sliding-window/scripts/run-all.sh
new file mode 100755
index 0000000..38df9a6
--- /dev/null
+++ b/sliding-window/scripts/run-all.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Regenerates every file in output/. Needs JDK 25 and Maven. The first Maven run must be online.
+# Every file is written by a JUnit test that asserts the same numbers it prints, so the figures
+# in the article are assertions: they fail the build the moment they stop being true.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+mkdir -p output
+mvn -B -q -T1 test
+echo "done:"; ls -1 output
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/BruteForce.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/BruteForce.java
new file mode 100644
index 0000000..8d1a5cd
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/BruteForce.java
@@ -0,0 +1,197 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Obviously-correct, obviously-slow references. The randomized tests compare every optimised
+ * solution against these, so a "clever" bug cannot hide behind a hand-picked example.
+ */
+public final class BruteForce {
+ private BruteForce() {}
+
+ public static long maxSumSubarray(int[] a, int k) {
+ long best = Long.MIN_VALUE;
+ for (int i = 0; i + k <= a.length; i++) {
+ long sum = 0;
+ for (int j = i; j < i + k; j++) {
+ sum += a[j];
+ }
+ best = Math.max(best, sum);
+ }
+ return best;
+ }
+
+ public static boolean permutationInString(String p, String s) {
+ char[] want = p.toCharArray();
+ Arrays.sort(want);
+ for (int i = 0; i + p.length() <= s.length(); i++) {
+ char[] got = s.substring(i, i + p.length()).toCharArray();
+ Arrays.sort(got);
+ if (Arrays.equals(want, got)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static int[] slidingWindowMaximum(int[] a, int k) {
+ int[] out = new int[a.length - k + 1];
+ for (int i = 0; i < out.length; i++) {
+ int max = Integer.MIN_VALUE;
+ for (int j = i; j < i + k; j++) {
+ max = Math.max(max, a[j]);
+ }
+ out[i] = max;
+ }
+ return out;
+ }
+
+ public static int longestUniqueSubstring(String s) {
+ int best = 0;
+ for (int i = 0; i < s.length(); i++) {
+ Set seen = new HashSet<>();
+ for (int j = i; j < s.length() && seen.add(s.charAt(j)); j++) {
+ best = Math.max(best, j - i + 1);
+ }
+ }
+ return best;
+ }
+
+ public static int longestKDistinct(String s, int k) {
+ int best = 0;
+ for (int i = 0; i < s.length(); i++) {
+ Set seen = new HashSet<>();
+ for (int j = i; j < s.length(); j++) {
+ seen.add(s.charAt(j));
+ if (seen.size() > k) {
+ break;
+ }
+ best = Math.max(best, j - i + 1);
+ }
+ }
+ return best;
+ }
+
+ public static int characterReplacement(String s, int k) {
+ int best = 0;
+ for (int i = 0; i < s.length(); i++) {
+ int[] count = new int[128];
+ int maxFreq = 0;
+ for (int j = i; j < s.length(); j++) {
+ maxFreq = Math.max(maxFreq, ++count[s.charAt(j)]);
+ if ((j - i + 1) - maxFreq <= k) {
+ best = Math.max(best, j - i + 1);
+ }
+ }
+ }
+ return best;
+ }
+
+ public static int longestOnes(int[] a, int k) {
+ int best = 0;
+ for (int i = 0; i < a.length; i++) {
+ int zeros = 0;
+ for (int j = i; j < a.length; j++) {
+ if (a[j] == 0 && ++zeros > k) {
+ break;
+ }
+ best = Math.max(best, j - i + 1);
+ }
+ }
+ return best;
+ }
+
+ /** Works for any integers, positive or not; the window version does not. */
+ public static int minSubArrayLen(int target, int[] a) {
+ int best = 0;
+ for (int i = 0; i < a.length; i++) {
+ long sum = 0;
+ for (int j = i; j < a.length; j++) {
+ sum += a[j];
+ if (sum >= target) {
+ int len = j - i + 1;
+ best = (best == 0) ? len : Math.min(best, len);
+ break;
+ }
+ }
+ }
+ return best;
+ }
+
+ public static String minWindow(String s, String t) {
+ String best = "";
+ for (int i = 0; i < s.length(); i++) {
+ for (int j = i + t.length(); j <= s.length(); j++) {
+ if ((best.isEmpty() || j - i < best.length()) && covers(s.substring(i, j), t)) {
+ best = s.substring(i, j);
+ }
+ }
+ }
+ return best;
+ }
+
+ private static boolean covers(String window, String t) {
+ int[] c = new int[128];
+ for (char ch : window.toCharArray()) {
+ c[ch]++;
+ }
+ for (char ch : t.toCharArray()) {
+ if (--c[ch] < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Does any pair sum to target? (existence only; indices may differ from the two-pointer answer) */
+ public static boolean twoSumExists(int[] a, int target) {
+ for (int i = 0; i < a.length; i++) {
+ for (int j = i + 1; j < a.length; j++) {
+ if ((long) a[i] + a[j] == target) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static List> threeSum(int[] a) {
+ Set> found = new HashSet<>();
+ for (int i = 0; i < a.length; i++) {
+ for (int j = i + 1; j < a.length; j++) {
+ for (int k = j + 1; k < a.length; k++) {
+ if (a[i] + a[j] + a[k] == 0) {
+ int[] t = {a[i], a[j], a[k]};
+ Arrays.sort(t);
+ found.add(List.of(t[0], t[1], t[2]));
+ }
+ }
+ }
+ }
+ List> out = new ArrayList<>(found);
+ out.sort((x, y) -> {
+ for (int i = 0; i < 3; i++) {
+ int c = Integer.compare(x.get(i), y.get(i));
+ if (c != 0) {
+ return c;
+ }
+ }
+ return 0;
+ });
+ return out;
+ }
+
+ public static long maxArea(int[] h) {
+ long best = 0;
+ for (int i = 0; i < h.length; i++) {
+ for (int j = i + 1; j < h.length; j++) {
+ best = Math.max(best, (long) Math.min(h[i], h[j]) * (j - i));
+ }
+ }
+ return best;
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Complexity.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Complexity.java
new file mode 100644
index 0000000..c5f3d74
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Complexity.java
@@ -0,0 +1,92 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+/**
+ * Counts operations instead of timing them, so the numbers are identical on every machine and
+ * every run. "Element reads" is a model of work, not a benchmark.
+ */
+public final class Complexity {
+ private Complexity() {}
+
+ /** Brute force max-sum-of-k: every window re-adds its k elements. */
+ public static long bruteMaxSumReads(int[] a, int k) {
+ long reads = 0;
+ for (int i = 0; i + k <= a.length; i++) {
+ for (int j = i; j < i + k; j++) {
+ reads++;
+ }
+ }
+ return reads;
+ }
+
+ /** Window max-sum-of-k: k reads to build the first window, then 2 reads per slide. */
+ public static long windowMaxSumReads(int[] a, int k) {
+ long reads = k;
+ for (int right = k; right < a.length; right++) {
+ reads += 2;
+ }
+ return reads;
+ }
+
+ /** Brute force pair search for a target sum: counts pair checks. */
+ public static long brutePairChecks(int[] a, int target) {
+ long checks = 0;
+ for (int i = 0; i < a.length; i++) {
+ for (int j = i + 1; j < a.length; j++) {
+ checks++;
+ if ((long) a[i] + a[j] == target) {
+ return checks;
+ }
+ }
+ }
+ return checks;
+ }
+
+ /** Two-pointer pair search over a sorted array: counts loop iterations. */
+ public static long twoPointerSteps(int[] sorted, int target) {
+ long steps = 0;
+ int lo = 0;
+ int hi = sorted.length - 1;
+ while (lo < hi) {
+ steps++;
+ long sum = (long) sorted[lo] + sorted[hi];
+ if (sum == target) {
+ return steps;
+ } else if (sum < target) {
+ lo++;
+ } else {
+ hi--;
+ }
+ }
+ return steps;
+ }
+
+ /**
+ * The nested loop in a variable window looks quadratic but is not: {@code left} and
+ * {@code right} each only ever move forward. Returns {rightMoves, leftMoves, windows}.
+ */
+ public static long[] minWindowPointerMoves(String s, String t) {
+ int[] need = new int[128];
+ for (char c : t.toCharArray()) {
+ need[c]++;
+ }
+ int missing = t.length();
+ int left = 0;
+ long leftMoves = 0;
+ long rightMoves = 0;
+ long validWindows = 0;
+ for (int right = 0; right < s.length(); right++) {
+ rightMoves++;
+ if (need[s.charAt(right)]-- > 0) {
+ missing--;
+ }
+ while (missing == 0) {
+ validWindows++;
+ if (++need[s.charAt(left++)] > 0) {
+ missing++;
+ }
+ leftMoves++;
+ }
+ }
+ return new long[] {rightMoves, leftMoves, validWindows};
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/DryRuns.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/DryRuns.java
new file mode 100644
index 0000000..d51f055
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/DryRuns.java
@@ -0,0 +1,174 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Instrumented copies of the real algorithms that record every step. DryRunTest asserts each
+ * copy returns exactly what the real solution returns, so a trace cannot drift from the code
+ * it explains.
+ */
+public final class DryRuns {
+ private DryRuns() {}
+
+ /** A trace plus the answer the traced algorithm arrived at. */
+ public record Run(List lines, Object result) {}
+
+ public static Run maxSumSubarray(int[] a, int k) {
+ List log = new ArrayList<>();
+ long sum = 0;
+ for (int i = 0; i < k; i++) {
+ sum += a[i];
+ }
+ long best = sum;
+ log.add("first window " + range(a, 0, k - 1) + " sum=" + sum + " best=" + best);
+ for (int right = k; right < a.length; right++) {
+ int in = a[right];
+ int out = a[right - k];
+ sum += in - out;
+ best = Math.max(best, sum);
+ log.add("right=" + right + " +" + in + " -" + out + " window=" + range(a, right - k + 1, right)
+ + " sum=" + sum + " best=" + best);
+ }
+ return new Run(log, best);
+ }
+
+ public static Run longestUniqueSubstring(String s) {
+ List log = new ArrayList<>();
+ Map lastSeen = new HashMap<>();
+ int left = 0;
+ int best = 0;
+ for (int right = 0; right < s.length(); right++) {
+ char c = s.charAt(right);
+ String note = "";
+ Integer prev = lastSeen.get(c);
+ if (prev != null) {
+ int moved = Math.max(left, prev + 1);
+ note = (moved == left)
+ ? " seen at " + prev + " but that is left of the window, left stays " + left
+ : " seen at " + prev + " inside the window, left " + left + " -> " + moved;
+ left = moved;
+ }
+ lastSeen.put(c, right);
+ best = Math.max(best, right - left + 1);
+ log.add("right=" + right + " '" + c + "' left=" + left + " window=\""
+ + s.substring(left, right + 1) + "\" best=" + best + note);
+ }
+ return new Run(log, best);
+ }
+
+ public static Run minWindow(String s, String t) {
+ List log = new ArrayList<>();
+ int[] need = new int[128];
+ for (int i = 0; i < t.length(); i++) {
+ need[t.charAt(i)]++;
+ }
+ int missing = t.length();
+ int left = 0;
+ int bestStart = 0;
+ int bestLen = Integer.MAX_VALUE;
+ for (int right = 0; right < s.length(); right++) {
+ if (need[s.charAt(right)]-- > 0) {
+ missing--;
+ }
+ if (missing == 0) {
+ log.add("right=" + right + " '" + s.charAt(right) + "' window=\"" + s.substring(left, right + 1)
+ + "\" is VALID");
+ }
+ while (missing == 0) {
+ if (right - left + 1 < bestLen) {
+ bestLen = right - left + 1;
+ bestStart = left;
+ log.add(" new best \"" + s.substring(left, right + 1) + "\" (length " + bestLen + ")");
+ }
+ char out = s.charAt(left++);
+ if (++need[out] > 0) {
+ missing++;
+ log.add(" drop '" + out + "' -> window=\"" + s.substring(left, right + 1)
+ + "\" no longer valid, missing=" + missing);
+ } else {
+ log.add(" drop '" + out + "' -> window=\"" + s.substring(left, right + 1)
+ + "\" still valid");
+ }
+ }
+ }
+ String result = bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen);
+ return new Run(log, result);
+ }
+
+ public static Run slidingWindowMaximum(int[] a, int k) {
+ List log = new ArrayList<>();
+ int[] out = new int[a.length - k + 1];
+ Deque dq = new ArrayDeque<>();
+ for (int right = 0; right < a.length; right++) {
+ while (!dq.isEmpty() && a[dq.peekLast()] <= a[right]) {
+ dq.pollLast();
+ }
+ dq.addLast(right);
+ if (dq.peekFirst() <= right - k) {
+ dq.pollFirst();
+ }
+ String max = "";
+ if (right >= k - 1) {
+ out[right - k + 1] = a[dq.peekFirst()];
+ max = " -> window max " + out[right - k + 1];
+ }
+ String values = dq.stream().map(i -> a[i] + "@" + i).collect(Collectors.joining(", ", "[", "]"));
+ log.add("right=" + right + " a[right]=" + a[right] + " deque(value@index)=" + values + max);
+ }
+ return new Run(log, out);
+ }
+
+ public static Run twoSumSorted(int[] a, int target) {
+ List log = new ArrayList<>();
+ int lo = 0;
+ int hi = a.length - 1;
+ int[] result = new int[0];
+ while (lo < hi) {
+ int sum = a[lo] + a[hi];
+ String head = "lo=" + lo + " hi=" + hi + " " + a[lo] + " + " + a[hi] + " = " + sum;
+ if (sum == target) {
+ log.add(head + " == " + target + " found");
+ result = new int[] {lo, hi};
+ break;
+ } else if (sum < target) {
+ log.add(head + " < " + target + " lo++");
+ lo++;
+ } else {
+ log.add(head + " > " + target + " hi--");
+ hi--;
+ }
+ }
+ return new Run(log, result);
+ }
+
+ public static Run maxArea(int[] h) {
+ List log = new ArrayList<>();
+ int lo = 0;
+ int hi = h.length - 1;
+ long best = 0;
+ while (lo < hi) {
+ long area = (long) Math.min(h[lo], h[hi]) * (hi - lo);
+ best = Math.max(best, area);
+ String move = h[lo] < h[hi] ? "lo++ (left line is shorter)" : "hi-- (right line is not taller)";
+ log.add("lo=" + lo + " hi=" + hi + " min(" + h[lo] + "," + h[hi] + ") x " + (hi - lo) + " = " + area
+ + " best=" + best + " " + move);
+ if (h[lo] < h[hi]) {
+ lo++;
+ } else {
+ hi--;
+ }
+ }
+ return new Run(log, best);
+ }
+
+ private static String range(int[] a, int from, int to) {
+ return Arrays.toString(Arrays.copyOfRange(a, from, to + 1));
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/FixedWindow.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/FixedWindow.java
new file mode 100644
index 0000000..933c8a5
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/FixedWindow.java
@@ -0,0 +1,83 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+
+/**
+ * Fixed-size windows: the window length is known up front, so the right edge drives everything
+ * and the left edge is just {@code right - k + 1}.
+ */
+public final class FixedWindow {
+ private FixedWindow() {}
+
+ /** Problem 1: largest sum of any {@code k} consecutive elements. O(n) time, O(1) space. */
+ public static long maxSumSubarray(int[] a, int k) {
+ if (k <= 0 || k > a.length) {
+ throw new IllegalArgumentException("k must be in 1.." + a.length + " but was " + k);
+ }
+ long sum = 0;
+ for (int i = 0; i < k; i++) {
+ sum += a[i]; // build the first window
+ }
+ long best = sum;
+ for (int right = k; right < a.length; right++) {
+ sum += a[right] - a[right - k]; // slide: one element in, one element out
+ best = Math.max(best, sum);
+ }
+ return best;
+ }
+
+ /**
+ * Problem 2: does {@code s} contain some permutation of {@code p} as a substring?
+ * Lowercase a-z only. O(26 * n) time because the two count arrays are compared each step.
+ */
+ public static boolean permutationInString(String p, String s) {
+ if (p.length() > s.length()) {
+ return false;
+ }
+ int k = p.length();
+ int[] need = new int[26];
+ int[] have = new int[26];
+ for (int i = 0; i < k; i++) {
+ need[p.charAt(i) - 'a']++;
+ have[s.charAt(i) - 'a']++;
+ }
+ if (Arrays.equals(need, have)) {
+ return true;
+ }
+ for (int right = k; right < s.length(); right++) {
+ have[s.charAt(right) - 'a']++; // character enters
+ have[s.charAt(right - k) - 'a']--; // character leaves
+ if (Arrays.equals(need, have)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Problem 3: the maximum of every window of size {@code k}. A deque of indices whose values are
+ * strictly decreasing from front to back; the front is always the current maximum. O(n) time.
+ */
+ public static int[] slidingWindowMaximum(int[] a, int k) {
+ if (k <= 0 || k > a.length) {
+ throw new IllegalArgumentException("k must be in 1.." + a.length + " but was " + k);
+ }
+ int[] out = new int[a.length - k + 1];
+ Deque dq = new ArrayDeque<>(); // indices, values decreasing
+ for (int right = 0; right < a.length; right++) {
+ while (!dq.isEmpty() && a[dq.peekLast()] <= a[right]) {
+ dq.pollLast(); // smaller-or-equal values can never be a maximum again
+ }
+ dq.addLast(right);
+ if (dq.peekFirst() <= right - k) {
+ dq.pollFirst(); // the front index slid out of the window
+ }
+ if (right >= k - 1) {
+ out[right - k + 1] = a[dq.peekFirst()];
+ }
+ }
+ return out;
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Pitfalls.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Pitfalls.java
new file mode 100644
index 0000000..e19b0f4
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/Pitfalls.java
@@ -0,0 +1,68 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Four plausible-looking versions that are wrong. Each is used only by PitfallsTest. */
+public final class Pitfalls {
+ private Pitfalls() {}
+
+ /** Bug: {@code left = prev + 1} without {@code Math.max}, so left can jump BACKWARDS. */
+ public static int longestUniqueNoMax(String s) {
+ Map lastSeen = new HashMap<>();
+ int left = 0;
+ int best = 0;
+ for (int right = 0; right < s.length(); right++) {
+ char c = s.charAt(right);
+ if (lastSeen.containsKey(c)) {
+ left = lastSeen.get(c) + 1;
+ }
+ lastSeen.put(c, right);
+ best = Math.max(best, right - left + 1);
+ }
+ return best;
+ }
+
+ /** Bug: the shrink-while-valid window assumes positive numbers; negatives break it. */
+ public static int minSubArrayLenWindowOnNegatives(int target, int[] a) {
+ int left = 0;
+ long sum = 0;
+ int best = 0;
+ for (int right = 0; right < a.length; right++) {
+ sum += a[right];
+ while (sum >= target && left <= right) {
+ int len = right - left + 1;
+ best = (best == 0) ? len : Math.min(best, len);
+ sum -= a[left++];
+ }
+ }
+ return best;
+ }
+
+ /** Bug: sorts and uses two pointers but never skips duplicate values. */
+ public static List> threeSumNoDedupe(int[] input) {
+ int[] a = input.clone();
+ Arrays.sort(a);
+ List> out = new ArrayList<>();
+ for (int i = 0; i < a.length - 2; i++) {
+ int lo = i + 1;
+ int hi = a.length - 1;
+ while (lo < hi) {
+ int sum = a[i] + a[lo] + a[hi];
+ if (sum == 0) {
+ out.add(List.of(a[i], a[lo], a[hi]));
+ lo++;
+ hi--;
+ } else if (sum < 0) {
+ lo++;
+ } else {
+ hi--;
+ }
+ }
+ }
+ return out;
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/TwoPointers.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/TwoPointers.java
new file mode 100644
index 0000000..e95e9c9
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/TwoPointers.java
@@ -0,0 +1,83 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Two pointers moving toward each other over sorted (or sortable) data. */
+public final class TwoPointers {
+ private TwoPointers() {}
+
+ /**
+ * Problem 10: two indices (0-based, ascending) whose values sum to {@code target} in a
+ * SORTED array, or an empty array if none. O(n) time, O(1) space.
+ */
+ public static int[] twoSumSorted(int[] a, int target) {
+ int lo = 0;
+ int hi = a.length - 1;
+ while (lo < hi) {
+ long sum = (long) a[lo] + a[hi];
+ if (sum == target) {
+ return new int[] {lo, hi};
+ } else if (sum < target) {
+ lo++; // need a bigger sum: only moving lo up can help
+ } else {
+ hi--; // need a smaller sum: only moving hi down can help
+ }
+ }
+ return new int[0];
+ }
+
+ /** Problem 11: all unique triplets summing to zero. Sort, fix one, two-pointer the rest. O(n^2). */
+ public static List> threeSum(int[] input) {
+ int[] a = input.clone();
+ Arrays.sort(a);
+ List> out = new ArrayList<>();
+ for (int i = 0; i < a.length - 2; i++) {
+ if (a[i] > 0) {
+ break; // sorted: everything to the right is positive too
+ }
+ if (i > 0 && a[i] == a[i - 1]) {
+ continue; // skip duplicate first elements
+ }
+ int lo = i + 1;
+ int hi = a.length - 1;
+ while (lo < hi) {
+ int sum = a[i] + a[lo] + a[hi];
+ if (sum == 0) {
+ out.add(List.of(a[i], a[lo], a[hi]));
+ lo++;
+ hi--;
+ while (lo < hi && a[lo] == a[lo - 1]) {
+ lo++; // skip duplicate second elements
+ }
+ while (lo < hi && a[hi] == a[hi + 1]) {
+ hi--; // skip duplicate third elements
+ }
+ } else if (sum < 0) {
+ lo++;
+ } else {
+ hi--;
+ }
+ }
+ }
+ return out;
+ }
+
+ /** Problem 12: the largest area two vertical lines can hold with the x-axis. O(n). */
+ public static long maxArea(int[] height) {
+ int lo = 0;
+ int hi = height.length - 1;
+ long best = 0;
+ while (lo < hi) {
+ long area = (long) Math.min(height[lo], height[hi]) * (hi - lo);
+ best = Math.max(best, area);
+ if (height[lo] < height[hi]) {
+ lo++; // the shorter line limits the area: discard it
+ } else {
+ hi--;
+ }
+ }
+ return best;
+ }
+}
diff --git a/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/VariableWindow.java b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/VariableWindow.java
new file mode 100644
index 0000000..6d07a75
--- /dev/null
+++ b/sliding-window/src/main/java/com/ankurm/interviewlab/slidingwindow/VariableWindow.java
@@ -0,0 +1,145 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Variable-size windows: the right edge always moves forward, and the left edge moves forward
+ * only to repair a broken rule (or, for "minimum" problems, to shrink a valid window).
+ */
+public final class VariableWindow {
+ private VariableWindow() {}
+
+ /** Problem 4: length of the longest substring with no repeated character. O(n). */
+ public static int longestUniqueSubstring(String s) {
+ Map lastSeen = new HashMap<>();
+ int left = 0;
+ int best = 0;
+ for (int right = 0; right < s.length(); right++) {
+ char c = s.charAt(right);
+ Integer prev = lastSeen.get(c);
+ if (prev != null) {
+ left = Math.max(left, prev + 1); // never move left backwards
+ }
+ lastSeen.put(c, right);
+ best = Math.max(best, right - left + 1);
+ }
+ return best;
+ }
+
+ /** Problem 5: longest substring with at most {@code k} distinct characters. O(n). */
+ public static int longestKDistinct(String s, int k) {
+ if (k <= 0) {
+ return 0;
+ }
+ Map count = new HashMap<>();
+ int left = 0;
+ int best = 0;
+ for (int right = 0; right < s.length(); right++) {
+ count.merge(s.charAt(right), 1, Integer::sum);
+ while (count.size() > k) { // rule broken: shrink from the left
+ char out = s.charAt(left++);
+ if (count.merge(out, -1, Integer::sum) == 0) {
+ count.remove(out);
+ }
+ }
+ best = Math.max(best, right - left + 1);
+ }
+ return best;
+ }
+
+ /**
+ * Problem 6: longest substring that can be turned into one repeated character with at most
+ * {@code k} replacements. ASCII input. O(n). {@code maxFreq} is deliberately never decreased:
+ * the window only ever needs to grow to beat the best answer, and a stale maxFreq can only
+ * keep the window the same size, never inflate the answer.
+ */
+ public static int characterReplacement(String s, int k) {
+ int[] count = new int[128];
+ int left = 0;
+ int maxFreq = 0;
+ int best = 0;
+ for (int right = 0; right < s.length(); right++) {
+ maxFreq = Math.max(maxFreq, ++count[s.charAt(right)]);
+ if ((right - left + 1) - maxFreq > k) { // more than k characters need replacing
+ count[s.charAt(left++)]--; // slide (window size stays the same)
+ }
+ best = Math.max(best, right - left + 1);
+ }
+ return best;
+ }
+
+ /** Problem 7: longest run of 1s if at most {@code k} zeros may be flipped. O(n). */
+ public static int longestOnes(int[] a, int k) {
+ int left = 0;
+ int zeros = 0;
+ int best = 0;
+ for (int right = 0; right < a.length; right++) {
+ if (a[right] == 0) {
+ zeros++;
+ }
+ while (zeros > k) {
+ if (a[left++] == 0) {
+ zeros--;
+ }
+ }
+ best = Math.max(best, right - left + 1);
+ }
+ return best;
+ }
+
+ /**
+ * Problem 8: length of the shortest subarray with sum >= target, or 0 if none.
+ * Requires every element to be positive: that is what makes "shrink while still valid" correct.
+ */
+ public static int minSubArrayLen(int target, int[] a) {
+ if (target <= 0) {
+ throw new IllegalArgumentException("target must be positive but was " + target);
+ }
+ int left = 0;
+ long sum = 0;
+ int best = 0;
+ for (int right = 0; right < a.length; right++) {
+ sum += a[right];
+ while (sum >= target) { // valid: record it, then try a smaller one
+ int len = right - left + 1;
+ best = (best == 0) ? len : Math.min(best, len);
+ sum -= a[left++];
+ }
+ }
+ return best;
+ }
+
+ /**
+ * Problem 9: smallest substring of {@code s} that contains every character of {@code t}
+ * (with multiplicity), or "" if none. ASCII input. O(|s| + |t|).
+ */
+ public static String minWindow(String s, String t) {
+ if (t.isEmpty() || t.length() > s.length()) {
+ return "";
+ }
+ int[] need = new int[128];
+ for (int i = 0; i < t.length(); i++) {
+ need[t.charAt(i)]++;
+ }
+ int missing = t.length(); // characters still owed to the window
+ int left = 0;
+ int bestStart = 0;
+ int bestLen = Integer.MAX_VALUE;
+ for (int right = 0; right < s.length(); right++) {
+ if (need[s.charAt(right)]-- > 0) {
+ missing--; // this character was genuinely needed
+ }
+ while (missing == 0) { // valid: shrink as far as possible
+ if (right - left + 1 < bestLen) {
+ bestLen = right - left + 1;
+ bestStart = left;
+ }
+ if (++need[s.charAt(left++)] > 0) {
+ missing++; // we just gave back a needed character
+ }
+ }
+ }
+ return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen);
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/ComplexityTest.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/ComplexityTest.java
new file mode 100644
index 0000000..aee1edd
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/ComplexityTest.java
@@ -0,0 +1,59 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ankurm.interviewlab.slidingwindow.support.Transcript;
+import java.util.Random;
+import org.junit.jupiter.api.Test;
+
+/** Counts operations (not time): deterministic on every machine. */
+class ComplexityTest {
+
+ @Test
+ void operationCounts() {
+ Transcript t = Transcript.start("08-operation-counts.txt",
+ "Operation counts, brute force vs window / two pointers (counted, not timed)");
+
+ t.line("Max sum of k=100 consecutive elements: element reads");
+ t.line(String.format("%8s %14s %14s %8s", "n", "brute force", "sliding window", "ratio"));
+ for (int n : new int[] {1_000, 2_000, 4_000, 8_000}) {
+ int[] a = new int[n];
+ long brute = Complexity.bruteMaxSumReads(a, 100);
+ long win = Complexity.windowMaxSumReads(a, 100);
+ assertEquals((long) (n - 100 + 1) * 100, brute);
+ assertEquals(100L + 2L * (n - 100), win);
+ t.line(String.format("%8d %,14d %,14d %7.1fx", n, brute, win, (double) brute / win));
+ }
+
+ t.blank().line("Pair search, target absent (worst case) in a sorted array 0..n-1: checks / steps");
+ t.line(String.format("%8s %14s %14s", "n", "all pairs", "two pointers"));
+ for (int n : new int[] {1_000, 2_000, 4_000, 8_000}) {
+ int[] sorted = new int[n];
+ for (int i = 0; i < n; i++) {
+ sorted[i] = i;
+ }
+ long brute = Complexity.brutePairChecks(sorted, -1);
+ long steps = Complexity.twoPointerSteps(sorted, -1);
+ assertEquals((long) n * (n - 1) / 2, brute);
+ assertEquals(n - 1L, steps);
+ t.line(String.format("%8d %,14d %,14d", n, brute, steps));
+ }
+
+ t.blank().line("minWindow on random strings over A-D, t=\"ABCD\" (seed 42): pointer moves");
+ t.line("The loop is nested but each pointer only moves forward, so total moves stay linear.");
+ t.line(String.format("%8s %12s %11s %14s", "n", "right moves", "left moves", "valid windows"));
+ Random rnd = new Random(42);
+ for (int n : new int[] {1_000, 2_000, 4_000, 8_000}) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n; i++) {
+ sb.append((char) ('A' + rnd.nextInt(4)));
+ }
+ long[] m = Complexity.minWindowPointerMoves(sb.toString(), "ABCD");
+ assertEquals(n, m[0]);
+ assertTrue(m[1] <= n, "left can never pass right");
+ t.line(String.format("%8d %,12d %,11d %,14d", n, m[0], m[1], m[2]));
+ }
+ t.flush();
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/DryRunTest.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/DryRunTest.java
new file mode 100644
index 0000000..0ff835d
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/DryRunTest.java
@@ -0,0 +1,86 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.ankurm.interviewlab.slidingwindow.DryRuns.Run;
+import com.ankurm.interviewlab.slidingwindow.support.Transcript;
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+
+/** Writes the step-by-step traces and asserts each traced copy agrees with the real solution. */
+class DryRunTest {
+
+ @Test
+ void fixedWindowTrace() {
+ int[] a = {2, 1, 5, 1, 3, 2};
+ Run run = DryRuns.maxSumSubarray(a, 3);
+ assertEquals(FixedWindow.maxSumSubarray(a, 3), run.result());
+ assertEquals(9L, run.result());
+ Transcript t = Transcript.start("01-fixed-window-trace.txt",
+ "Max sum of 3 consecutive elements in " + Arrays.toString(a));
+ run.lines().forEach(t::line);
+ t.blank().line("answer = " + run.result()).flush();
+ }
+
+ @Test
+ void longestUniqueTrace() {
+ Transcript t = Transcript.start("02-longest-unique-trace.txt", "Longest substring without repeating characters");
+ for (String s : new String[] {"abcabcbb", "abba"}) {
+ Run run = DryRuns.longestUniqueSubstring(s);
+ assertEquals(VariableWindow.longestUniqueSubstring(s), run.result());
+ t.line("input \"" + s + "\"");
+ run.lines().forEach(l -> t.line(" " + l));
+ t.line(" answer = " + run.result()).blank();
+ }
+ assertEquals(3, DryRuns.longestUniqueSubstring("abcabcbb").result());
+ assertEquals(2, DryRuns.longestUniqueSubstring("abba").result());
+ t.flush();
+ }
+
+ @Test
+ void minWindowTrace() {
+ String s = "ADOBECODEBANC";
+ String need = "ABC";
+ Run run = DryRuns.minWindow(s, need);
+ assertEquals(VariableWindow.minWindow(s, need), run.result());
+ assertEquals("BANC", run.result());
+ Transcript t = Transcript.start("03-min-window-trace.txt",
+ "Minimum window substring: s=\"" + s + "\" t=\"" + need + "\"");
+ run.lines().forEach(t::line);
+ t.blank().line("answer = \"" + run.result() + "\"").flush();
+ }
+
+ @Test
+ void slidingMaxTrace() {
+ int[] a = {1, 3, -1, -3, 5, 3, 6, 7};
+ Run run = DryRuns.slidingWindowMaximum(a, 3);
+ assertArrayEquals(FixedWindow.slidingWindowMaximum(a, 3), (int[]) run.result());
+ assertArrayEquals(new int[] {3, 3, 5, 5, 6, 7}, (int[]) run.result());
+ Transcript t = Transcript.start("04-sliding-max-deque-trace.txt",
+ "Sliding window maximum, k=3, input " + Arrays.toString(a));
+ run.lines().forEach(t::line);
+ t.blank().line("answer = " + Arrays.toString((int[]) run.result())).flush();
+ }
+
+ @Test
+ void twoPointerTraces() {
+ int[] sorted = {1, 3, 4, 6, 8, 11};
+ Run two = DryRuns.twoSumSorted(sorted, 10);
+ assertArrayEquals(TwoPointers.twoSumSorted(sorted, 10), (int[]) two.result());
+ assertArrayEquals(new int[] {2, 3}, (int[]) two.result());
+
+ int[] h = {1, 8, 6, 2, 5, 4, 8, 3, 7};
+ Run area = DryRuns.maxArea(h);
+ assertEquals(TwoPointers.maxArea(h), area.result());
+ assertEquals(49L, area.result());
+
+ Transcript t = Transcript.start("05-two-pointers-trace.txt", "Opposite-end two pointers");
+ t.line("Two Sum II: sorted " + Arrays.toString(sorted) + ", target 10");
+ two.lines().forEach(l -> t.line(" " + l));
+ t.line(" answer = indices " + Arrays.toString((int[]) two.result())).blank();
+ t.line("Container With Most Water: heights " + Arrays.toString(h));
+ area.lines().forEach(l -> t.line(" " + l));
+ t.line(" answer = " + area.result()).flush();
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/PitfallsTest.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/PitfallsTest.java
new file mode 100644
index 0000000..7bd56e2
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/PitfallsTest.java
@@ -0,0 +1,57 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ankurm.interviewlab.slidingwindow.support.Transcript;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Four wrong-but-plausible versions, shown failing against the correct answer. */
+class PitfallsTest {
+
+ @Test
+ void pitfalls() {
+ Transcript t = Transcript.start("09-pitfalls.txt", "Four classic mistakes, each reproduced");
+
+ int wrong1 = Pitfalls.longestUniqueNoMax("abba");
+ int right1 = VariableWindow.longestUniqueSubstring("abba");
+ assertEquals(3, wrong1);
+ assertEquals(2, right1);
+ t.line("1. longest substring without repeats, left = lastSeen + 1 WITHOUT Math.max");
+ t.line(" input \"abba\": buggy = " + wrong1 + " correct = " + right1);
+ t.line(" at the final 'a', lastSeen('a') = 0, so the buggy code moves left from 2 back to 1");
+ t.blank();
+
+ int[] neg = {-2, -2, 3};
+ int wrong2 = Pitfalls.minSubArrayLenWindowOnNegatives(3, neg);
+ int right2 = BruteForce.minSubArrayLen(3, neg);
+ assertEquals(0, wrong2);
+ assertEquals(1, right2);
+ t.line("2. minimum-size subarray window used on input that contains negatives");
+ t.line(" target 3, input " + Arrays.toString(neg) + ": window = " + wrong2 + " (\"none\") brute force = " + right2 + " ([3])");
+ t.line(" the running sum is -1 when the 3 arrives, so the window never looks valid");
+ t.blank();
+
+ int[] in3 = {-1, 0, 1, 2, -1, -4};
+ List> wrong3 = Pitfalls.threeSumNoDedupe(in3);
+ List> right3 = TwoPointers.threeSum(in3);
+ assertEquals(3, wrong3.size());
+ assertEquals(2, right3.size());
+ t.line("3. 3Sum without skipping duplicates");
+ t.line(" input " + Arrays.toString(in3));
+ t.line(" buggy = " + wrong3);
+ t.line(" correct = " + right3);
+ t.blank();
+
+ int[] unsorted = {3, 2, 4};
+ int[] got = TwoPointers.twoSumSorted(unsorted, 6);
+ assertEquals(0, got.length);
+ assertTrue(BruteForce.twoSumExists(unsorted, 6));
+ t.line("4. two pointers on an array that is NOT sorted");
+ t.line(" input " + Arrays.toString(unsorted) + ", target 6: twoSumSorted = " + Arrays.toString(got)
+ + " but 2 + 4 = 6 exists (brute force: " + BruteForce.twoSumExists(unsorted, 6) + ")");
+ t.flush();
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/RandomAgreementTest.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/RandomAgreementTest.java
new file mode 100644
index 0000000..c7828d5
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/RandomAgreementTest.java
@@ -0,0 +1,110 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ankurm.interviewlab.slidingwindow.support.Transcript;
+import java.util.Arrays;
+import java.util.Random;
+import org.junit.jupiter.api.Test;
+
+/** Seeded random inputs: every optimised solution must agree with its brute-force reference. */
+class RandomAgreementTest {
+ private static final int CASES = 2000;
+ private final Random rnd = new Random(2026);
+
+ private int[] ints(int minLen, int maxLen, int lo, int hi) {
+ int[] a = new int[minLen + rnd.nextInt(maxLen - minLen + 1)];
+ for (int i = 0; i < a.length; i++) {
+ a[i] = lo + rnd.nextInt(hi - lo + 1);
+ }
+ return a;
+ }
+
+ private String str(String alphabet, int minLen, int maxLen) {
+ int n = minLen + rnd.nextInt(maxLen - minLen + 1);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n; i++) {
+ sb.append(alphabet.charAt(rnd.nextInt(alphabet.length())));
+ }
+ return sb.toString();
+ }
+
+ @Test
+ void everyProblemAgreesWithBruteForce() {
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(1, 12, -5, 9);
+ int k = 1 + rnd.nextInt(a.length);
+ assertEquals(BruteForce.maxSumSubarray(a, k), FixedWindow.maxSumSubarray(a, k), Arrays.toString(a) + " k=" + k);
+ }
+ for (int c = 0; c < CASES; c++) {
+ String p = str("abc", 1, 4);
+ String s = str("abc", 0, 10);
+ assertEquals(BruteForce.permutationInString(p, s), FixedWindow.permutationInString(p, s), p + " in " + s);
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(1, 12, -5, 9);
+ int k = 1 + rnd.nextInt(a.length);
+ assertArrayEquals(BruteForce.slidingWindowMaximum(a, k), FixedWindow.slidingWindowMaximum(a, k), Arrays.toString(a) + " k=" + k);
+ }
+ for (int c = 0; c < CASES; c++) {
+ String s = str("abcd", 0, 12);
+ assertEquals(BruteForce.longestUniqueSubstring(s), VariableWindow.longestUniqueSubstring(s), s);
+ }
+ for (int c = 0; c < CASES; c++) {
+ String s = str("abcd", 0, 12);
+ int k = rnd.nextInt(5);
+ assertEquals(BruteForce.longestKDistinct(s, k), VariableWindow.longestKDistinct(s, k), s + " k=" + k);
+ }
+ for (int c = 0; c < CASES; c++) {
+ String s = str("ABC", 0, 12);
+ int k = rnd.nextInt(4);
+ assertEquals(BruteForce.characterReplacement(s, k), VariableWindow.characterReplacement(s, k), s + " k=" + k);
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(0, 14, 0, 1);
+ int k = rnd.nextInt(5);
+ assertEquals(BruteForce.longestOnes(a, k), VariableWindow.longestOnes(a, k), Arrays.toString(a) + " k=" + k);
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(0, 12, 1, 6);
+ int target = 1 + rnd.nextInt(20);
+ assertEquals(BruteForce.minSubArrayLen(target, a), VariableWindow.minSubArrayLen(target, a), Arrays.toString(a) + " target=" + target);
+ }
+ for (int c = 0; c < CASES; c++) {
+ String s = str("ABC", 0, 12);
+ String t = str("ABC", 1, 4);
+ assertEquals(BruteForce.minWindow(s, t), VariableWindow.minWindow(s, t), s + " / " + t);
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(0, 10, -6, 6);
+ Arrays.sort(a);
+ int target = -8 + rnd.nextInt(17);
+ int[] got = TwoPointers.twoSumSorted(a, target);
+ assertEquals(BruteForce.twoSumExists(a, target), got.length == 2, Arrays.toString(a) + " target=" + target);
+ if (got.length == 2) {
+ assertTrue(got[0] < got[1]);
+ assertEquals(target, a[got[0]] + a[got[1]]);
+ }
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] a = ints(0, 9, -4, 4);
+ assertEquals(BruteForce.threeSum(a), TwoPointers.threeSum(a), Arrays.toString(a));
+ }
+ for (int c = 0; c < CASES; c++) {
+ int[] h = ints(2, 10, 0, 9);
+ assertEquals(BruteForce.maxArea(h), TwoPointers.maxArea(h), Arrays.toString(h));
+ }
+
+ Transcript t = Transcript.start("07-random-agreement.txt",
+ "Randomized agreement with brute force (seed 2026, " + CASES + " cases per problem)");
+ String[] names = {"maxSumSubarray", "permutationInString", "slidingWindowMaximum", "longestUniqueSubstring",
+ "longestKDistinct", "characterReplacement", "longestOnes", "minSubArrayLen", "minWindow",
+ "twoSumSorted", "threeSum", "maxArea"};
+ for (int i = 0; i < names.length; i++) {
+ t.line(String.format("%2d. %-24s %d cases, 0 disagreements", i + 1, names[i], CASES));
+ }
+ t.blank().line("total: " + (CASES * names.length) + " comparisons (any disagreement would have failed the build)").flush();
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/SolutionsTest.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/SolutionsTest.java
new file mode 100644
index 0000000..833a400
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/SolutionsTest.java
@@ -0,0 +1,95 @@
+package com.ankurm.interviewlab.slidingwindow;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ankurm.interviewlab.slidingwindow.support.Transcript;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** One hand-picked example per problem, printed beside the brute-force answer. */
+class SolutionsTest {
+
+ @Test
+ void allTwelveProblems() {
+ Transcript t = Transcript.start("06-all-twelve-results.txt",
+ "Twelve problems: optimised answer vs brute-force answer on one example each");
+
+ int[] a1 = {2, 1, 5, 1, 3, 2};
+ long r1 = FixedWindow.maxSumSubarray(a1, 3);
+ assertEquals(9L, r1);
+ assertEquals(BruteForce.maxSumSubarray(a1, 3), r1);
+ t.line(" 1. maxSumSubarray(" + Arrays.toString(a1) + ", 3) = " + r1 + " brute = " + BruteForce.maxSumSubarray(a1, 3));
+
+ assertTrue(FixedWindow.permutationInString("ab", "eidbaooo"));
+ assertFalse(FixedWindow.permutationInString("ab", "eidboaoo"));
+ t.line(" 2. permutationInString(\"ab\", \"eidbaooo\") = true; (\"ab\", \"eidboaoo\") = false brute = "
+ + BruteForce.permutationInString("ab", "eidbaooo") + " / " + BruteForce.permutationInString("ab", "eidboaoo"));
+
+ int[] a3 = {1, 3, -1, -3, 5, 3, 6, 7};
+ int[] r3 = FixedWindow.slidingWindowMaximum(a3, 3);
+ assertArrayEquals(new int[] {3, 3, 5, 5, 6, 7}, r3);
+ assertArrayEquals(BruteForce.slidingWindowMaximum(a3, 3), r3);
+ t.line(" 3. slidingWindowMaximum(" + Arrays.toString(a3) + ", 3) = " + Arrays.toString(r3));
+
+ assertEquals(3, VariableWindow.longestUniqueSubstring("abcabcbb"));
+ t.line(" 4. longestUniqueSubstring(\"abcabcbb\") = 3 brute = " + BruteForce.longestUniqueSubstring("abcabcbb"));
+
+ assertEquals(3, VariableWindow.longestKDistinct("eceba", 2));
+ t.line(" 5. longestKDistinct(\"eceba\", 2) = 3 brute = " + BruteForce.longestKDistinct("eceba", 2));
+
+ assertEquals(4, VariableWindow.characterReplacement("AABABBA", 1));
+ t.line(" 6. characterReplacement(\"AABABBA\", 1) = 4 brute = " + BruteForce.characterReplacement("AABABBA", 1));
+
+ int[] a7 = {1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0};
+ assertEquals(6, VariableWindow.longestOnes(a7, 2));
+ t.line(" 7. longestOnes(" + Arrays.toString(a7) + ", 2) = 6 brute = " + BruteForce.longestOnes(a7, 2));
+
+ int[] a8 = {2, 3, 1, 2, 4, 3};
+ assertEquals(2, VariableWindow.minSubArrayLen(7, a8));
+ t.line(" 8. minSubArrayLen(7, " + Arrays.toString(a8) + ") = 2 brute = " + BruteForce.minSubArrayLen(7, a8));
+
+ assertEquals("BANC", VariableWindow.minWindow("ADOBECODEBANC", "ABC"));
+ assertEquals("", VariableWindow.minWindow("a", "aa"));
+ t.line(" 9. minWindow(\"ADOBECODEBANC\", \"ABC\") = \"BANC\"; minWindow(\"a\", \"aa\") = \"\" brute = \""
+ + BruteForce.minWindow("ADOBECODEBANC", "ABC") + "\"");
+
+ int[] a10 = {2, 7, 11, 15};
+ assertArrayEquals(new int[] {0, 1}, TwoPointers.twoSumSorted(a10, 9));
+ assertEquals(0, TwoPointers.twoSumSorted(a10, 100).length);
+ t.line("10. twoSumSorted(" + Arrays.toString(a10) + ", 9) = [0, 1]; target 100 = [] (none)");
+
+ int[] a11 = {-1, 0, 1, 2, -1, -4};
+ List> r11 = TwoPointers.threeSum(a11);
+ assertEquals(List.of(List.of(-1, -1, 2), List.of(-1, 0, 1)), r11);
+ assertEquals(BruteForce.threeSum(a11), r11);
+ t.line("11. threeSum(" + Arrays.toString(a11) + ") = " + r11);
+
+ int[] a12 = {1, 8, 6, 2, 5, 4, 8, 3, 7};
+ assertEquals(49L, TwoPointers.maxArea(a12));
+ t.line("12. maxArea(" + Arrays.toString(a12) + ") = 49 brute = " + BruteForce.maxArea(a12));
+ t.flush();
+ }
+
+ @Test
+ void invalidArgumentsAreRejected() {
+ assertThrows(IllegalArgumentException.class, () -> FixedWindow.maxSumSubarray(new int[] {1, 2}, 3));
+ assertThrows(IllegalArgumentException.class, () -> FixedWindow.maxSumSubarray(new int[] {1, 2}, 0));
+ assertThrows(IllegalArgumentException.class, () -> VariableWindow.minSubArrayLen(0, new int[] {1}));
+ }
+
+ @Test
+ void edgeCases() {
+ assertEquals(0, VariableWindow.longestUniqueSubstring(""));
+ assertEquals(0, VariableWindow.longestKDistinct("abc", 0));
+ assertEquals(0, VariableWindow.minSubArrayLen(100, new int[] {1, 2, 3}));
+ assertEquals(1, VariableWindow.minSubArrayLen(3, new int[] {1, 2, 3}));
+ assertEquals(0, TwoPointers.maxArea(new int[] {5}));
+ assertTrue(TwoPointers.threeSum(new int[] {0, 0}).isEmpty());
+ assertEquals(List.of(List.of(0, 0, 0)), TwoPointers.threeSum(new int[] {0, 0, 0, 0}));
+ }
+}
diff --git a/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/support/Transcript.java b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/support/Transcript.java
new file mode 100644
index 0000000..072edf8
--- /dev/null
+++ b/sliding-window/src/test/java/com/ankurm/interviewlab/slidingwindow/support/Transcript.java
@@ -0,0 +1,43 @@
+package com.ankurm.interviewlab.slidingwindow.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);
+ }
+ }
+}