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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
Executable
+9
@@ -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;
|
||||
}
|
||||
}
|
||||
+145
@@ -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 >= 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);
|
||||
}
|
||||
}
|
||||
+59
@@ -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();
|
||||
}
|
||||
}
|
||||
+110
@@ -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}));
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user