Add sliding-window module: 12 sliding window and two-pointer interview problems, brute-force cross-checks, dry-run traces, operation counts, pitfalls (11 tests)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b6EqSkUWKNBc8i3Q3U
This commit is contained in:
2026-11-09 08:40:07 +00:00
co-authored by Claude Sonnet 5.5
parent 4a9021d936
commit 911aef5515
26 changed files with 1600 additions and 0 deletions
+1
View File
@@ -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 | | [`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 | | [`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 | | [`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 ## `collections` module
+75
View File
@@ -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 |
@@ -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
@@ -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
@@ -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"
@@ -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]
@@ -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
@@ -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
@@ -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)
@@ -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
+18
View File
@@ -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)
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm.interviewlab</groupId>
<artifactId>sliding-window</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>java-interview-lab :: sliding-window</name>
<description>
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.
</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>-Xmx512m -Duser.timezone=UTC -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
+9
View File
@@ -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
@@ -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<Character> 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<Character> 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<List<Integer>> threeSum(int[] a) {
Set<List<Integer>> 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<List<Integer>> 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;
}
}
@@ -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};
}
}
@@ -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<String> lines, Object result) {}
public static Run maxSumSubarray(int[] a, int k) {
List<String> 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<String> log = new ArrayList<>();
Map<Character, Integer> 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<String> 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<String> log = new ArrayList<>();
int[] out = new int[a.length - k + 1];
Deque<Integer> 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<String> 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<String> 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));
}
}
@@ -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<Integer> 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;
}
}
@@ -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<Character, Integer> 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<List<Integer>> threeSumNoDedupe(int[] input) {
int[] a = input.clone();
Arrays.sort(a);
List<List<Integer>> 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;
}
}
@@ -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<List<Integer>> threeSum(int[] input) {
int[] a = input.clone();
Arrays.sort(a);
List<List<Integer>> 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;
}
}
@@ -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<Character, Integer> 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<Character, Integer> 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 &gt;= 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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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<List<Integer>> wrong3 = Pitfalls.threeSumNoDedupe(in3);
List<List<Integer>> 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();
}
}
@@ -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();
}
}
@@ -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<List<Integer>> 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}));
}
}
@@ -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<String> lines = new ArrayList<>();
private Transcript(Path path, String header) {
this.path = path;
lines.add(header);
lines.add("=".repeat(Math.min(header.length(), 100)));
lines.add("");
}
public static Transcript start(String fileName, String header) {
Path dir = Path.of("output");
try {
Files.createDirectories(dir);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return new Transcript(dir.resolve(fileName), header);
}
public Transcript line(String text) { lines.add(text); return this; }
public Transcript blank() { lines.add(""); return this; }
public void flush() {
try {
Files.write(path, (String.join("\n", lines) + "\n").getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}