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); + } + } +}