Add stack-queue module: ten stack and queue interview problems, monotonic stack, min stack, queue from stacks and stack from a queue, 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_017ic4b6nxynun5JFivebVb1
This commit is contained in:
Claude
2026-11-12 22:35:43 +05:30
parent c5c435ae20
commit b9aeb78b24
36 changed files with 2164 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# stack-queue
Companion code for the ankurm.com article **Stack and Queue Problems in Java: Balanced Parentheses, Next Greater Element, Min Stack**.
Ten classic interview problems on stacks and queues, 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 stack-queue
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 ten problems
| # | Problem | Idea | Method |
|---|---|---|---|
| 1 | Balanced parentheses | stack of expected closers | `Parentheses.isBalanced` |
| 2 | Evaluate Reverse Polish Notation | stack of operands | `Rpn.evaluate` |
| 3 | Min stack | second stack, or one stack of differences | `MinStack`, `EncodedMinStack` |
| 4 | Next greater element | monotonic stack | `MonotonicStack.nextGreater`, `nextGreaterSubset` |
| 5 | Daily temperatures | monotonic stack of indices | `MonotonicStack.dailyTemperatures` |
| 6 | Next greater element, circular | monotonic stack, two laps | `MonotonicStack.nextGreaterCircular` |
| 7 | Largest rectangle in a histogram | monotonic stack with a sentinel | `MonotonicStack.largestRectangle` |
| 8 | Decode string | stack of parked text and counts | `DecodeString.decode` |
| 9 | Queue using two stacks | pour only when the output stack is empty | `QueueViaStacks` |
| 10 | Stack using one queue | rotate after every push | `StackViaQueue` |
## Source layout
```
stack-queue/
├── pom.xml
├── scripts/run-all.sh
├── src/main/java/com/ankurm/interviewlab/stackqueue/
│ ├── Parentheses.java, Rpn.java, DecodeString.java problems 1, 2, 8
│ ├── IntMinStack.java, MinStack.java, EncodedMinStack.java problem 3
│ ├── MonotonicStack.java problems 4-7
│ ├── QueueViaStacks.java, StackViaQueue.java problems 9, 10
│ ├── BruteForce.java slow, obviously-correct references
│ ├── DryRuns.java instrumented copies that record every step
│ ├── Complexity.java operation counters (counted, not timed)
│ ├── Pitfalls.java plausible-looking wrong versions
│ └── JdkStackDemo.java java.util.Stack versus ArrayDeque
├── src/test/java/.../ SolutionsTest, DryRunTest, RandomAgreementTest, ComplexityTest, PitfallsTest, JdkStackTest
└── output/*.txt captured real output
```
@@ -0,0 +1,28 @@
Balanced brackets: every step of four inputs
============================================
input: {[()]} firstMismatch = -1
read '{' at 0: push '}' waiting for (top first): }
read '[' at 1: push ']' waiting for (top first): ] }
read '(' at 2: push ')' waiting for (top first): ) ] }
read ')' at 3: pop ')', matches waiting for (top first): ] }
read ']' at 4: pop ']', matches waiting for (top first): }
read '}' at 5: pop '}', matches waiting for (top first): (empty)
end of input: waiting for (empty) -> balanced
input: ([)] firstMismatch = 2
read '(' at 0: push ')' waiting for (top first): )
read '[' at 1: push ']' waiting for (top first): ] )
read ')' at 2: expected ']' -> NOT balanced
input: ()) firstMismatch = 2
read '(' at 0: push ')' waiting for (top first): )
read ')' at 1: pop ')', matches waiting for (top first): (empty)
read ')' at 2: stack is empty, nothing to close -> NOT balanced
input: (() firstMismatch = 0
read '(' at 0: push ')' waiting for (top first): )
read '(' at 1: push ')' waiting for (top first): ) )
read ')' at 2: pop ')', matches waiting for (top first): )
end of input: waiting for ) -> NOT balanced, openers left over
+17
View File
@@ -0,0 +1,17 @@
Reverse Polish Notation: stack after every token
================================================
tokens: [2, 1, +, 3, *] result = 9
token 2: push 2 stack(top first) = [2]
token 1: push 1 stack(top first) = [1, 2]
token +: pop 1 then 2, compute 2 + 1 = 3 stack(top first) = [3]
token 3: push 3 stack(top first) = [3, 3]
token *: pop 3 then 3, compute 3 * 3 = 9 stack(top first) = [9]
tokens: [4, 13, 5, /, +] result = 6
token 4: push 4 stack(top first) = [4]
token 13: push 13 stack(top first) = [13, 4]
token 5: push 5 stack(top first) = [5, 13, 4]
token /: pop 5 then 13, compute 13 / 5 = 2 stack(top first) = [2, 4]
token +: pop 2 then 4, compute 4 + 2 = 6 stack(top first) = [6]
+16
View File
@@ -0,0 +1,16 @@
Min stack: both internal stacks after every operation
=====================================================
push 5 values=[5] mins=[5] (new or equal minimum, also pushed on mins)
push 3 values=[3, 5] mins=[3, 5] (new or equal minimum, also pushed on mins)
push 7 values=[7, 3, 5] mins=[3, 5]
push 3 values=[3, 7, 3, 5] mins=[3, 3, 5] (new or equal minimum, also pushed on mins)
getMin values=[3, 7, 3, 5] mins=[3, 3, 5] getMin() = 3
pop -> 3 values=[7, 3, 5] mins=[3, 5] (it was the minimum, mins popped too)
getMin values=[7, 3, 5] mins=[3, 5] getMin() = 3
pop -> 7 values=[3, 5] mins=[3, 5]
getMin values=[3, 5] mins=[3, 5] getMin() = 3
pop -> 3 values=[5] mins=[5] (it was the minimum, mins popped too)
getMin values=[5] mins=[5] getMin() = 5
getMin results: [3, 3, 3, 5] (identical for MinStack and EncodedMinStack)
@@ -0,0 +1,17 @@
Next greater element: the stack of pending indices, step by step
================================================================
input [2, 1, 2, 4, 3]
i=0 value=2: push 0 pending(top first) = [0]
i=1 value=1: push 1 pending(top first) = [1, 0]
i=2 value=2: pop index 1 (value 1) -> its answer is 2; push 2 pending(top first) = [2, 0]
i=3 value=4: pop index 2 (value 2) -> its answer is 4; pop index 0 (value 2) -> its answer is 4; push 3 pending(top first) = [3]
i=4 value=3: push 4 pending(top first) = [4, 3]
indices still pending have no greater element to their right: [4, 3] -> stay -1
result [4, 2, 4, -1, -1]
dailyTemperatures [73, 74, 75, 71, 69, 72, 76, 73]
= [1, 1, 4, 2, 1, 1, 0, 0]
nextGreaterCircular [1, 2, 1] = [2, -1, 2]
nextGreaterSubset [4, 1, 2] within [1, 3, 4, 2] = [-1, 3, -1]
+25
View File
@@ -0,0 +1,25 @@
Largest rectangle in a histogram: pops, widths and areas
========================================================
heights [2, 1, 5, 6, 2, 3]
i=0 height=2:
push index 0 stack(top first) = [0]
i=1 height=1:
pop bar of height 2: left edge index -1, right edge index 0, width 1, area 2 best = 2
push index 1 stack(top first) = [1]
i=2 height=5:
push index 2 stack(top first) = [2, 1]
i=3 height=6:
push index 3 stack(top first) = [3, 2, 1]
i=4 height=2:
pop bar of height 6: left edge index 2, right edge index 3, width 1, area 6 best = 6
pop bar of height 5: left edge index 1, right edge index 3, width 2, area 10 best = 10
push index 4 stack(top first) = [4, 1]
i=5 height=3:
push index 5 stack(top first) = [5, 4, 1]
i=6 (sentinel) height=0:
pop bar of height 3: left edge index 4, right edge index 5, width 1, area 3 best = 10
pop bar of height 2: left edge index 1, right edge index 5, width 4, area 8 best = 10
pop bar of height 1: left edge index -1, right edge index 5, width 6, area 6 best = 10
push index 6 stack(top first) = [6]
largest rectangle = 10
@@ -0,0 +1,16 @@
Queue from two stacks, and stack from one queue
===============================================
QueueViaStacks (stacks shown top first)
enqueue(1) in=[1] out=[]
enqueue(2) in=[2, 1] out=[]
enqueue(3) in=[3, 2, 1] out=[]
dequeue() = 1 in=[] out=[2, 3] transfers so far: 3
enqueue(4) in=[4] out=[2, 3]
dequeue() x3 = 2, 3, 4 in=[] out=[] transfers so far: 4
StackViaQueue
push(1) queue(front=top)=[1] rotations so far: 0
push(2) queue(front=top)=[2, 1] rotations so far: 1
push(3) queue(front=top)=[3, 2, 1] rotations so far: 3
pop() x2 = 3, 2 queue(front=top)=[1]
+13
View File
@@ -0,0 +1,13 @@
All ten problems on their headline examples
===========================================
1. isBalanced("{[()]}") = true; isBalanced("([)]") = false
2. evaluate([2, 1, +, 3, *]) = 9; 7 / -3 truncates toward zero: -2
3. push -2, 0, -3: getMin = -3; pop; top = 0, getMin = -2 (both implementations agree)
4. nextGreater([1, 3, 4, 2]) = [3, 4, -1, -1]
5. dailyTemperatures([73, 74, 75, 71, 69, 72, 76, 73]) = [1, 1, 4, 2, 1, 1, 0, 0]
6. nextGreaterCircular([1, 2, 3, 4, 3]) = [2, 3, 4, -1, 4]
7. largestRectangle([2, 1, 5, 6, 2, 3]) = 10
8. decode("3[a2[bc]]") = "abcbcabcbcabcbc"; decode("2[abc]3[cd]ef") = "abcabccdcdcdef"
9. enqueue a, b, c; dequeue = a; enqueue d; dequeue = b; peek = c; size = 2
10. push a, b, c; top = c; pop = c; pop = b; size = 1
@@ -0,0 +1,12 @@
Random agreement with brute force (seed 20261112, 5000 cases per row)
=====================================================================
nextGreater vs scan-right 5000 cases, 0 mismatches
dailyTemperatures vs scan-right 5000 cases, 0 mismatches
nextGreaterCircular vs wrap-around scan 5000 cases, 0 mismatches
largestRectangle vs widen-each-bar 5000 cases, 0 mismatches
isBalanced vs delete-adjacent-pairs 5000 cases, 0 mismatches
decode vs recursive descent 5000 cases, 0 mismatches
evaluate (RPN) vs expression-tree evaluation 5000 cases, 0 mismatches
MinStack and EncodedMinStack vs recompute-the-min 5000 sequences (150000 operations), 0 mismatches
QueueViaStacks and StackViaQueue vs ArrayDeque 5000 sequences (150000 operations), 0 mismatches
@@ -0,0 +1,24 @@
Operation counts (counted, not timed)
=====================================
nextGreater on a strictly decreasing array (the worst case for the scan-right version)
n scan-right compares stack compares stack push+pop
1000 499500 999 1000
10000 49995000 9999 10000
100000 4999950000 99999 100000
nextGreater on random values (n = 100000): every index is pushed once and popped at most once
pushes = 100000, pops = 99985, comparisons = 199977 (bound: comparisons <= 2n = 200000)
QueueViaStacks: n enqueues followed by n dequeues
n total element moves moves in the 1st dequeue
1000 1000 1000
10000 10000 10000
100000 100000 100000
one dequeue can cost n moves, but each element moves exactly once, so n enqueues + n dequeues cost 3n steps in all
StackViaQueue: n pushes
n total rotations n(n-1)/2
100 4950 4950
1000 499500 499500
5000 12497500 12497500
@@ -0,0 +1,19 @@
java.util.Stack versus ArrayDeque used as a stack
=================================================
ArrayDeque as a stack: push(1), push(2), push(3), then pop until empty -> [3, 2, 1]
ArrayDeque as a queue: offer(1), offer(2), offer(3), then poll until empty -> [1, 2, 3]
push(1), push(2), push(3), then iterate:
java.util.Stack -> [1, 2, 3] (bottom to top)
ArrayDeque -> [3, 2, 1] (top to bottom)
Stack.class.getSuperclass() = java.util.Vector
Stack.pop() is synchronized = true
ArrayDeque.pop() is synchronized = false
push(null) on Stack = accepted
push(null) on ArrayDeque = java.lang.NullPointerException
ArrayDeque: push(1), push(2), peek() = 2 (push is addFirst)
ArrayDeque: offer(1), offer(2), peek() = 1 (offer is addLast)
+33
View File
@@ -0,0 +1,33 @@
Eight classic mistakes, each reproduced
=======================================
1. balanced brackets by counting each kind, with no stack
input "([)]": counts-only = true stack = false
every count ends at zero, but the ] arrives while the ( is not the innermost open bracket
2. min stack that compares boxed Integers with ==
push [10, 20, 5], pop, getMin(): broken = 10 correct = 10
push [1000, 2000, 500], pop, getMin(): broken = 500 correct = 1000
Integer.valueOf caches -128..127, so small values share one object and == happens to work
3. min stack that pushes onto the min stack only on a strictly smaller value
push 3, push 3, pop, getMin(): strict -> NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Deque.peek()" is null
correct -> 3 (the stack still holds one 3)
4. encoded min stack with int differences instead of long
push Integer.MIN_VALUE, push Integer.MAX_VALUE, top(): int diffs = -2147483648 long diffs = 2147483647
MAX_VALUE - MIN_VALUE does not fit in an int, wraps to -1, and -1 looks like "new minimum"
5. next greater element popping on <= instead of <
input [2, 2, 3]: buggy = [2, 3, -1] correct = [3, 3, -1]
the first 2 is told its next greater element is 2
6. largest rectangle without the final height-0 bar
input [2, 4]: buggy = 0 correct = 4
nothing ever pops the bars that are still on the stack when the loop ends
7. queue from two stacks that pours on every dequeue
enqueue 1, 2; dequeue; enqueue 3; dequeue: first = 1 (ok), second: broken = 3 correct = 2
the 3 was poured on top of the 2 that was still waiting in the output stack
8. popping an empty structure: both conversions throw NoSuchElementException, as ArrayDeque.pop does
+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>stack-queue</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>java-interview-lab :: stack-queue</name>
<description>
Runnable companion code for ankurm.com's "Stack and Queue Problems in Java" article: ten interview problems, monotonic stack,
min stack, queue from stacks and stack from a queue, brute-force cross-checks, dry-run traces, operation counts and 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,106 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.Arrays;
/** Slow, obviously-correct references. The randomized tests compare every stack solution against these. */
public final class BruteForce {
private BruteForce() {}
public static int[] nextGreater(int[] a) {
int[] result = new int[a.length];
for (int i = 0; i < a.length; i++) {
result[i] = -1;
for (int j = i + 1; j < a.length; j++) {
if (a[j] > a[i]) {
result[i] = a[j];
break;
}
}
}
return result;
}
public static int[] dailyTemperatures(int[] temps) {
int[] wait = new int[temps.length];
for (int i = 0; i < temps.length; i++) {
for (int j = i + 1; j < temps.length; j++) {
if (temps[j] > temps[i]) {
wait[i] = j - i;
break;
}
}
}
return wait;
}
public static int[] nextGreaterCircular(int[] a) {
int n = a.length;
int[] result = new int[n];
Arrays.fill(result, -1);
for (int i = 0; i < n; i++) {
for (int step = 1; step < n; step++) {
int candidate = a[(i + step) % n];
if (candidate > a[i]) {
result[i] = candidate;
break;
}
}
}
return result;
}
/** For every bar, widen left and right while the neighbours are at least as tall. */
public static int largestRectangle(int[] h) {
int best = 0;
for (int i = 0; i < h.length; i++) {
int left = i;
while (left > 0 && h[left - 1] >= h[i]) {
left--;
}
int right = i;
while (right < h.length - 1 && h[right + 1] >= h[i]) {
right++;
}
best = Math.max(best, h[i] * (right - left + 1));
}
return best;
}
/** Keep only brackets, then delete adjacent matching pairs until nothing changes. Balanced iff nothing is left. */
public static boolean isBalanced(String s) {
String t = s.replaceAll("[^()\\[\\]{}]", "");
String before;
do {
before = t;
t = t.replace("()", "").replace("[]", "").replace("{}", "");
} while (!t.equals(before));
return t.isEmpty();
}
/** Recursive-descent decode: the index travels in a one-element array. */
public static String decode(String s) {
return decodeFrom(s, new int[] {0});
}
private static String decodeFrom(String s, int[] pos) {
StringBuilder out = new StringBuilder();
while (pos[0] < s.length() && s.charAt(pos[0]) != ']') {
char ch = s.charAt(pos[0]);
if (Character.isDigit(ch)) {
int count = 0;
while (Character.isDigit(s.charAt(pos[0]))) {
count = count * 10 + (s.charAt(pos[0]) - '0');
pos[0]++;
}
pos[0]++; // skip the opening bracket
String inner = decodeFrom(s, pos);
pos[0]++; // skip the closing bracket
out.append(inner.repeat(count));
} else {
out.append(ch);
pos[0]++;
}
}
return out.toString();
}
}
@@ -0,0 +1,52 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
/** Operation counters. These count work done, they do not time it. */
public final class Complexity {
private Complexity() {}
public record Counts(long comparisons, long pushes, long pops) {
public long stackOps() {
return pushes + pops;
}
}
/** nextGreater with every comparison, push and pop counted. Returns the answers too, so tests can check them. */
public static Counts monotonicNextGreater(int[] a, int[] answerOut) {
long comparisons = 0;
long pushes = 0;
long pops = 0;
Arrays.fill(answerOut, -1);
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < a.length; i++) {
while (!pending.isEmpty()) {
comparisons++;
if (a[pending.peek()] >= a[i]) {
break;
}
answerOut[pending.pop()] = a[i];
pops++;
}
pending.push(i);
pushes++;
}
return new Counts(comparisons, pushes, pops);
}
/** The brute-force version, counting each a[j] > a[i] comparison. */
public static long bruteForceComparisons(int[] a) {
long comparisons = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i + 1; j < a.length; j++) {
comparisons++;
if (a[j] > a[i]) {
break;
}
}
}
return comparisons;
}
}
@@ -0,0 +1,34 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
/** Problem 8: decode strings such as 3[a2[bc]]. Each open bracket parks the text built so far, and its count. */
public final class DecodeString {
private DecodeString() {}
public static String decode(String s) {
Deque<StringBuilder> parked = new ArrayDeque<>();
Deque<Integer> repeats = new ArrayDeque<>();
StringBuilder current = new StringBuilder();
int number = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (Character.isDigit(ch)) {
number = number * 10 + (ch - '0');
} else if (ch == '[') {
repeats.push(number);
parked.push(current);
current = new StringBuilder();
number = 0;
} else if (ch == ']') {
StringBuilder outer = parked.pop();
outer.append(current.toString().repeat(repeats.pop()));
current = outer;
} else {
current.append(ch);
}
}
return current.toString();
}
}
@@ -0,0 +1,151 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
/**
* Instrumented copies of the solutions that record every step. The tests assert each copy returns the same answer
* as the real method, so the traces describe what the real code does.
*/
public final class DryRuns {
private DryRuns() {}
public record Trace<T>(List<String> lines, T result) {}
/** Closers waiting on the stack, top first, separated by spaces, so they do not clash with list brackets. */
private static String closers(Deque<Character> stack) {
if (stack.isEmpty()) {
return "(empty)";
}
StringBuilder sb = new StringBuilder();
for (char ch : stack) {
sb.append(sb.length() == 0 ? "" : " ").append(ch);
}
return sb.toString();
}
public static Trace<Boolean> balanced(String s) {
List<String> lines = new ArrayList<>();
Deque<Character> expected = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
String prefix = "read '" + ch + "' at " + i + ": ";
if (ch == '(' || ch == '{' || ch == '[') {
char closer = ch == '(' ? ')' : ch == '{' ? '}' : ']';
expected.push(closer);
lines.add(prefix + String.format("%-18s", "push '" + closer + "'") + " waiting for (top first): " + closers(expected));
} else {
if (expected.isEmpty()) {
lines.add(prefix + "stack is empty, nothing to close -> NOT balanced");
return new Trace<>(lines, false);
}
char want = expected.pop();
if (want != ch) {
lines.add(prefix + "expected '" + want + "' -> NOT balanced");
return new Trace<>(lines, false);
}
lines.add(prefix + String.format("%-18s", "pop '" + want + "', matches") + " waiting for (top first): " + closers(expected));
}
}
boolean ok = expected.isEmpty();
lines.add("end of input: waiting for " + closers(expected) + (ok ? " -> balanced" : " -> NOT balanced, openers left over"));
return new Trace<>(lines, ok);
}
public static Trace<Integer> rpn(String[] tokens) {
List<String> lines = new ArrayList<>();
Deque<Integer> operands = new ArrayDeque<>();
for (String token : tokens) {
if (token.length() == 1 && "+-*/".contains(token)) {
int right = operands.pop();
int left = operands.pop();
int value = Rpn.apply(token, left, right);
operands.push(value);
lines.add("token " + token + ": pop " + right + " then " + left + ", compute " + left + " " + token + " "
+ right + " = " + value + " stack(top first) = " + operands);
} else {
operands.push(Integer.parseInt(token));
lines.add("token " + token + ": push " + token + " stack(top first) = " + operands);
}
}
return new Trace<>(lines, operands.peek());
}
/** Runs a fixed sequence on the parallel-stack min stack, showing both internal stacks after every step. */
public static Trace<List<Integer>> minStack(String... ops) {
List<String> lines = new ArrayList<>();
List<Integer> minsSeen = new ArrayList<>();
Deque<Integer> values = new ArrayDeque<>();
Deque<Integer> minimums = new ArrayDeque<>();
for (String op : ops) {
if (op.startsWith("push ")) {
int x = Integer.parseInt(op.substring(5));
values.push(x);
String note = "";
if (minimums.isEmpty() || x <= minimums.peek()) {
minimums.push(x);
note = " (new or equal minimum, also pushed on mins)";
}
lines.add(String.format("%-9s values=%-14s mins=%-10s%s", op, values, minimums, note).stripTrailing());
} else if (op.equals("pop")) {
int x = values.pop();
String note = "";
if (x == minimums.peek()) {
minimums.pop();
note = " (it was the minimum, mins popped too)";
}
lines.add(String.format("%-9s values=%-14s mins=%-10s%s", op + " -> " + x, values, minimums, note).stripTrailing());
} else {
int min = minimums.peek();
minsSeen.add(min);
lines.add(String.format("%-9s values=%-14s mins=%-10s getMin() = %d", op, values, minimums, min));
}
}
return new Trace<>(lines, minsSeen);
}
public static Trace<int[]> nextGreater(int[] a) {
List<String> lines = new ArrayList<>();
int[] result = new int[a.length];
Arrays.fill(result, -1);
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < a.length; i++) {
StringBuilder step = new StringBuilder("i=" + i + " value=" + a[i] + ":");
while (!pending.isEmpty() && a[pending.peek()] < a[i]) {
int j = pending.pop();
result[j] = a[i];
step.append(" pop index ").append(j).append(" (value ").append(a[j]).append(") -> its answer is ").append(a[i]).append(';');
}
pending.push(i);
step.append(" push ").append(i).append(" pending(top first) = ").append(pending);
lines.add(step.toString());
}
lines.add("indices still pending have no greater element to their right: " + pending + " -> stay -1");
return new Trace<>(lines, result);
}
public static Trace<Integer> histogram(int[] heights) {
List<String> lines = new ArrayList<>();
int n = heights.length;
int best = 0;
Deque<Integer> rising = new ArrayDeque<>();
for (int i = 0; i <= n; i++) {
int current = (i == n) ? 0 : heights[i];
lines.add("i=" + i + (i == n ? " (sentinel)" : "") + " height=" + current + ":");
while (!rising.isEmpty() && heights[rising.peek()] >= current) {
int height = heights[rising.pop()];
int leftEdge = rising.isEmpty() ? -1 : rising.peek();
int width = i - leftEdge - 1;
best = Math.max(best, height * width);
lines.add(" pop bar of height " + height + ": left edge index " + leftEdge + ", right edge index " + (i - 1)
+ ", width " + width + ", area " + (height * width) + " best = " + best);
}
rising.push(i);
lines.add(" push index " + i + " stack(top first) = " + rising);
}
return new Trace<>(lines, best);
}
}
@@ -0,0 +1,64 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
/**
* Problem 3, version B: one stack, one extra variable. Each entry stores its distance from the minimum that was in
* force when it was pushed. A negative entry marks a push that created a new minimum, and it carries the old
* minimum inside it. The differences are longs because the difference of two ints can overflow an int.
*/
public final class EncodedMinStack implements IntMinStack {
private final Deque<Long> diffs = new ArrayDeque<>();
private long min;
@Override
public void push(int x) {
if (diffs.isEmpty()) {
diffs.push(0L);
min = x;
return;
}
diffs.push((long) x - min);
if (x < min) {
min = x;
}
}
@Override
public int pop() {
requireNonEmpty();
long diff = diffs.pop();
if (diff < 0) {
int removed = (int) min; // a negative entry means the popped value was the minimum
min = min - diff; // restore the previous minimum
return removed;
}
return (int) (min + diff);
}
@Override
public int top() {
requireNonEmpty();
long diff = diffs.peek();
return diff < 0 ? (int) min : (int) (min + diff);
}
@Override
public int getMin() {
requireNonEmpty();
return (int) min;
}
@Override
public int size() {
return diffs.size();
}
private void requireNonEmpty() {
if (diffs.isEmpty()) {
throw new NoSuchElementException("stack is empty");
}
}
}
@@ -0,0 +1,17 @@
package com.ankurm.interviewlab.stackqueue;
/** A stack of ints that also reports its minimum in O(1). */
public interface IntMinStack {
void push(int x);
/** Removes and returns the top. Throws NoSuchElementException when empty. */
int pop();
/** Returns the top without removing it. Throws NoSuchElementException when empty. */
int top();
/** Returns the smallest value currently in the stack. Throws NoSuchElementException when empty. */
int getMin();
int size();
}
@@ -0,0 +1,104 @@
package com.ankurm.interviewlab.stackqueue;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
/** What the JDK gives you for a stack, and why the answer is ArrayDeque rather than java.util.Stack. */
public final class JdkStackDemo {
private JdkStackDemo() {}
/** Last in, first out: push 1, 2, 3, then pop until empty. */
public static List<Integer> stackPopOrder() {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
List<Integer> popped = new ArrayList<>();
while (!stack.isEmpty()) {
popped.add(stack.pop());
}
return popped;
}
/** First in, first out: offer 1, 2, 3, then poll until empty. */
public static List<Integer> queuePollOrder() {
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
List<Integer> polled = new ArrayList<>();
while (!queue.isEmpty()) {
polled.add(queue.poll());
}
return polled;
}
/** Items in iteration order after push(1), push(2), push(3) on the legacy class. Bottom to top. */
public static List<Integer> legacyStackOrder() {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
return new ArrayList<>(stack);
}
/** Items in iteration order after push(1), push(2), push(3) on an ArrayDeque. Top to bottom. */
public static List<Integer> dequeOrder() {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
return new ArrayList<>(stack);
}
public static String legacySuperclass() {
return Stack.class.getSuperclass().getName();
}
public static boolean legacyPopIsSynchronized() throws NoSuchMethodException {
return Modifier.isSynchronized(Stack.class.getMethod("pop").getModifiers());
}
public static boolean dequePopIsSynchronized() throws NoSuchMethodException {
return Modifier.isSynchronized(ArrayDeque.class.getMethod("pop").getModifiers());
}
/** The legacy class accepts null; ArrayDeque rejects it. Returns the exception class name, or "accepted". */
public static String pushNullOnLegacy() {
try {
new Stack<Integer>().push(null);
return "accepted";
} catch (RuntimeException e) {
return e.getClass().getName();
}
}
public static String pushNullOnDeque() {
try {
new ArrayDeque<Integer>().push(null);
return "accepted";
} catch (RuntimeException e) {
return e.getClass().getName();
}
}
/** push is addFirst, offer is addLast. Mixing them on one deque reads from the wrong end. */
public static int peekAfterPushes() {
Deque<Integer> d = new ArrayDeque<>();
d.push(1);
d.push(2);
return d.peek();
}
public static int peekAfterOffers() {
Deque<Integer> d = new ArrayDeque<>();
d.offer(1);
d.offer(2);
return d.peek();
}
}
@@ -0,0 +1,52 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
/** Problem 3, version A: a second stack that remembers the minimum at every depth where it changed. */
public final class MinStack implements IntMinStack {
private final Deque<Integer> values = new ArrayDeque<>();
private final Deque<Integer> minimums = new ArrayDeque<>();
@Override
public void push(int x) {
values.push(x);
if (minimums.isEmpty() || x <= minimums.peek()) { // <=, not <: a duplicate minimum needs its own entry
minimums.push(x);
}
}
@Override
public int pop() {
requireNonEmpty();
int x = values.pop();
if (x == minimums.peek()) { // int against Integer: unboxed, compared by value
minimums.pop();
}
return x;
}
@Override
public int top() {
requireNonEmpty();
return values.peek();
}
@Override
public int getMin() {
requireNonEmpty();
return minimums.peek();
}
@Override
public int size() {
return values.size();
}
private void requireNonEmpty() {
if (values.isEmpty()) {
throw new NoSuchElementException("stack is empty");
}
}
}
@@ -0,0 +1,96 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
/**
* Problems 4 to 7: the monotonic stack pattern. The stack holds indices whose answer is not known yet, and the
* values at those indices never increase from bottom to top. A new value that breaks that order settles every
* index it is larger than.
*/
public final class MonotonicStack {
private MonotonicStack() {}
/** Problem 4: for each element, the first strictly greater element to its right, or -1. */
public static int[] nextGreater(int[] a) {
int[] result = new int[a.length];
Arrays.fill(result, -1);
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < a.length; i++) {
while (!pending.isEmpty() && a[pending.peek()] < a[i]) {
result[pending.pop()] = a[i];
}
pending.push(i);
}
return result;
}
/** Problem 4, interview form: for each value of nums1 (a subset of nums2, all distinct), its next greater in nums2. */
public static int[] nextGreaterSubset(int[] nums1, int[] nums2) {
Map<Integer, Integer> nextOf = new HashMap<>();
Deque<Integer> pending = new ArrayDeque<>();
for (int x : nums2) {
while (!pending.isEmpty() && pending.peek() < x) {
nextOf.put(pending.pop(), x);
}
pending.push(x);
}
int[] answer = new int[nums1.length];
for (int k = 0; k < nums1.length; k++) {
answer[k] = nextOf.getOrDefault(nums1[k], -1);
}
return answer;
}
/** Problem 5: for each day, how many days until a warmer one; 0 if none. */
public static int[] dailyTemperatures(int[] temps) {
int[] wait = new int[temps.length];
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < temps.length; i++) {
while (!pending.isEmpty() && temps[pending.peek()] < temps[i]) {
int day = pending.pop();
wait[day] = i - day;
}
pending.push(i);
}
return wait;
}
/** Problem 6: next greater element in a circular array, so the search may wrap around past the end. */
public static int[] nextGreaterCircular(int[] a) {
int n = a.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < 2 * n; i++) {
int value = a[i % n];
while (!pending.isEmpty() && a[pending.peek()] < value) {
result[pending.pop()] = value;
}
if (i < n) {
pending.push(i); // second lap only settles answers, it never adds new work
}
}
return result;
}
/** Problem 7: area of the largest rectangle inside a histogram. The stack holds indices of rising bar heights. */
public static int largestRectangle(int[] heights) {
int n = heights.length;
int best = 0;
Deque<Integer> rising = new ArrayDeque<>();
for (int i = 0; i <= n; i++) {
int current = (i == n) ? 0 : heights[i]; // a final bar of height 0 flushes the stack
while (!rising.isEmpty() && heights[rising.peek()] >= current) {
int height = heights[rising.pop()];
int leftEdge = rising.isEmpty() ? -1 : rising.peek();
best = Math.max(best, height * (i - leftEdge - 1));
}
rising.push(i);
}
return best;
}
}
@@ -0,0 +1,55 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
/** Problem 1: balanced brackets. */
public final class Parentheses {
private Parentheses() {}
/**
* True when every bracket is closed by the matching kind, in the right order. Characters that are not
* brackets are ignored. The stack holds the closer each open bracket is waiting for.
*/
public static boolean isBalanced(String s) {
Deque<Character> expected = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '(' -> expected.push(')');
case '{' -> expected.push('}');
case '[' -> expected.push(']');
case ')', '}', ']' -> {
if (expected.isEmpty() || expected.pop() != ch) {
return false;
}
}
default -> { }
}
}
return expected.isEmpty();
}
/**
* Index of the first offending character, or -1 when the string is balanced. That is either a closer
* with no matching opener, or, when the scan ends with openers left over, the earliest opener never closed.
*/
public static int firstMismatch(String s) {
Deque<Integer> openers = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(' || ch == '{' || ch == '[') {
openers.push(i);
} else if (ch == ')' || ch == '}' || ch == ']') {
if (openers.isEmpty() || !matches(s.charAt(openers.pop()), ch)) {
return i;
}
}
}
return openers.isEmpty() ? -1 : openers.peekLast();
}
private static boolean matches(char open, char close) {
return (open == '(' && close == ')') || (open == '{' && close == '}') || (open == '[' && close == ']');
}
}
@@ -0,0 +1,205 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.NoSuchElementException;
/** Plausible-looking wrong versions. Each one passes a casual test and fails the case the test chooses. */
public final class Pitfalls {
private Pitfalls() {}
/** Wrong: counts each bracket kind, never checks order. */
public static boolean balancedCountsOnly(String s) {
int round = 0;
int curly = 0;
int square = 0;
for (int i = 0; i < s.length(); i++) {
switch (s.charAt(i)) {
case '(' -> round++;
case ')' -> round--;
case '{' -> curly++;
case '}' -> curly--;
case '[' -> square++;
case ']' -> square--;
default -> { }
}
if (round < 0 || curly < 0 || square < 0) {
return false;
}
}
return round == 0 && curly == 0 && square == 0;
}
/** Wrong: compares two boxed Integers with ==, which compares references. Works only while values are cached. */
public static final class BoxedEqualsMinStack implements IntMinStack {
private final Deque<Integer> values = new ArrayDeque<>();
private final Deque<Integer> minimums = new ArrayDeque<>();
@Override
public void push(int x) {
values.push(x);
if (minimums.isEmpty() || x <= minimums.peek()) {
minimums.push(x);
}
}
@Override
public int pop() {
Integer x = values.pop();
if (x == minimums.peek()) { // Integer == Integer: reference comparison
minimums.pop();
}
return x;
}
@Override
public int top() {
return values.peek();
}
@Override
public int getMin() {
return minimums.peek();
}
@Override
public int size() {
return values.size();
}
}
/** Wrong: pushes onto the min stack only on a strictly smaller value, so duplicates of the minimum are lost. */
public static final class StrictMinStack implements IntMinStack {
private final Deque<Integer> values = new ArrayDeque<>();
private final Deque<Integer> minimums = new ArrayDeque<>();
@Override
public void push(int x) {
values.push(x);
if (minimums.isEmpty() || x < minimums.peek()) { // should be <=
minimums.push(x);
}
}
@Override
public int pop() {
int x = values.pop();
if (x == minimums.peek()) {
minimums.pop();
}
return x;
}
@Override
public int top() {
return values.peek();
}
@Override
public int getMin() {
return minimums.peek();
}
@Override
public int size() {
return values.size();
}
}
/** Wrong: the encoded min stack with int differences, which overflow for values far apart. */
public static final class IntDiffMinStack implements IntMinStack {
private final Deque<Integer> diffs = new ArrayDeque<>();
private int min;
@Override
public void push(int x) {
if (diffs.isEmpty()) {
diffs.push(0);
min = x;
return;
}
diffs.push(x - min); // overflows when x and min are more than Integer.MAX_VALUE apart
if (x < min) {
min = x;
}
}
@Override
public int pop() {
int diff = diffs.pop();
if (diff < 0) {
int removed = min;
min = min - diff;
return removed;
}
return min + diff;
}
@Override
public int top() {
int diff = diffs.peek();
return diff < 0 ? min : min + diff;
}
@Override
public int getMin() {
if (diffs.isEmpty()) {
throw new NoSuchElementException("stack is empty");
}
return min;
}
@Override
public int size() {
return diffs.size();
}
}
/** Wrong: pops on {@code <=}, so an equal value is reported as "greater". */
public static int[] nextGreaterPopsEqual(int[] a) {
int[] result = new int[a.length];
Arrays.fill(result, -1);
Deque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < a.length; i++) {
while (!pending.isEmpty() && a[pending.peek()] <= a[i]) { // should be <
result[pending.pop()] = a[i];
}
pending.push(i);
}
return result;
}
/** Wrong: the histogram loop without the final height-0 bar, so bars still on the stack are never measured. */
public static int largestRectangleNoSentinel(int[] heights) {
int n = heights.length;
int best = 0;
Deque<Integer> rising = new ArrayDeque<>();
for (int i = 0; i < n; i++) { // should run to i <= n
while (!rising.isEmpty() && heights[rising.peek()] >= heights[i]) {
int height = heights[rising.pop()];
int leftEdge = rising.isEmpty() ? -1 : rising.peek();
best = Math.max(best, height * (i - leftEdge - 1));
}
rising.push(i);
}
return best;
}
/** Wrong: pours {@code in} onto {@code out} on every dequeue, even when {@code out} still holds older items. */
public static final class PourAlwaysQueue {
private final Deque<Integer> in = new ArrayDeque<>();
private final Deque<Integer> out = new ArrayDeque<>();
public void enqueue(int x) {
in.push(x);
}
public int dequeue() {
while (!in.isEmpty()) { // should be guarded by out.isEmpty()
out.push(in.pop());
}
return out.pop();
}
}
}
@@ -0,0 +1,58 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
/**
* Problem 9: a FIFO queue built from two LIFO stacks. New items go onto {@code in}. Items leave from {@code out};
* when {@code out} is empty, everything in {@code in} is poured over, which reverses it into queue order.
*/
public final class QueueViaStacks<T> {
private final Deque<T> in = new ArrayDeque<>();
private final Deque<T> out = new ArrayDeque<>();
private long transfers;
public void enqueue(T item) {
in.push(item);
}
public T dequeue() {
pourIfNeeded();
if (out.isEmpty()) {
throw new NoSuchElementException("queue is empty");
}
return out.pop();
}
public T peek() {
pourIfNeeded();
if (out.isEmpty()) {
throw new NoSuchElementException("queue is empty");
}
return out.peek();
}
public int size() {
return in.size() + out.size();
}
/** Total number of single-element moves from {@code in} to {@code out} so far. */
public long transferCount() {
return transfers;
}
/** The two stacks, top first, for traces. */
public String snapshot() {
return "in=" + in + " out=" + out;
}
private void pourIfNeeded() {
if (out.isEmpty()) { // only when out is empty: pouring onto a non-empty out would put new items ahead of old ones
while (!in.isEmpty()) {
out.push(in.pop());
transfers++;
}
}
}
}
@@ -0,0 +1,37 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.Deque;
/** Problem 2: evaluate a Reverse Polish Notation expression. Division truncates toward zero, as Java's does. */
public final class Rpn {
private Rpn() {}
public static int evaluate(String[] tokens) {
Deque<Integer> operands = new ArrayDeque<>();
for (String token : tokens) {
switch (token) {
case "+", "-", "*", "/" -> {
int right = operands.pop(); // the second operand is on top
int left = operands.pop();
operands.push(apply(token, left, right));
}
default -> operands.push(Integer.parseInt(token));
}
}
if (operands.size() != 1) {
throw new IllegalArgumentException("malformed expression: " + operands.size() + " values left");
}
return operands.pop();
}
static int apply(String operator, int left, int right) {
return switch (operator) {
case "+" -> left + right;
case "-" -> left - right;
case "*" -> left * right;
case "/" -> left / right;
default -> throw new IllegalArgumentException("unknown operator " + operator);
};
}
}
@@ -0,0 +1,50 @@
package com.ankurm.interviewlab.stackqueue;
import java.util.ArrayDeque;
import java.util.NoSuchElementException;
import java.util.Queue;
/**
* Problem 10: a LIFO stack built from one FIFO queue. After each push, every older item is rotated behind the new
* one, so the newest item sits at the front of the queue, which is exactly where pop and top read from.
*/
public final class StackViaQueue<T> {
private final Queue<T> queue = new ArrayDeque<>();
private long rotations;
public void push(T item) {
queue.offer(item);
for (int i = queue.size() - 1; i > 0; i--) {
queue.offer(queue.poll());
rotations++;
}
}
public T pop() {
if (queue.isEmpty()) {
throw new NoSuchElementException("stack is empty");
}
return queue.poll();
}
public T top() {
if (queue.isEmpty()) {
throw new NoSuchElementException("stack is empty");
}
return queue.peek();
}
public int size() {
return queue.size();
}
/** Total number of single-element rotations so far. */
public long rotationCount() {
return rotations;
}
/** The queue, front first (the front is the top of the stack), for traces. */
public String snapshot() {
return "queue(front=top)=" + queue;
}
}
@@ -0,0 +1,85 @@
package com.ankurm.interviewlab.stackqueue;
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.stackqueue.Complexity.Counts;
import com.ankurm.interviewlab.stackqueue.support.Transcript;
import java.util.Random;
import org.junit.jupiter.api.Test;
/** Counted work, not timed work: the numbers do not depend on the machine. */
class ComplexityTest {
@Test
void counts() {
Transcript t = Transcript.start("09-operation-counts.txt", "Operation counts (counted, not timed)");
Random rnd = new Random(7L);
t.line("nextGreater on a strictly decreasing array (the worst case for the scan-right version)");
t.line(String.format("%8s %18s %18s %14s", "n", "scan-right compares", "stack compares", "stack push+pop"));
for (int n : new int[] {1_000, 10_000, 100_000}) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = n - i;
}
long brute = Complexity.bruteForceComparisons(a);
int[] answers = new int[n];
Counts stack = Complexity.monotonicNextGreater(a, answers);
assertArrayEquals(MonotonicStack.nextGreater(a), answers);
assertEquals((long) n * (n - 1) / 2, brute);
assertTrue(stack.stackOps() <= 2L * n);
t.line(String.format("%8d %18d %18d %14d", n, brute, stack.comparisons(), stack.stackOps()));
}
t.blank();
t.line("nextGreater on random values (n = 100000): every index is pushed once and popped at most once");
int n = 100_000;
int[] r = new int[n];
for (int i = 0; i < n; i++) {
r[i] = rnd.nextInt(1_000_000);
}
int[] answers = new int[n];
Counts stack = Complexity.monotonicNextGreater(r, answers);
assertArrayEquals(MonotonicStack.nextGreater(r), answers);
assertEquals(n, stack.pushes());
assertTrue(stack.pops() <= n);
assertTrue(stack.comparisons() <= 2L * n);
t.line("pushes = " + stack.pushes() + ", pops = " + stack.pops() + ", comparisons = " + stack.comparisons()
+ " (bound: comparisons <= 2n = " + 2L * n + ")");
t.blank();
t.line("QueueViaStacks: n enqueues followed by n dequeues");
t.line(String.format("%8s %22s %24s", "n", "total element moves", "moves in the 1st dequeue"));
for (int size : new int[] {1_000, 10_000, 100_000}) {
QueueViaStacks<Integer> q = new QueueViaStacks<>();
for (int i = 0; i < size; i++) {
q.enqueue(i);
}
q.dequeue();
long firstDequeue = q.transferCount();
for (int i = 1; i < size; i++) {
q.dequeue();
}
assertEquals(size, firstDequeue);
assertEquals(size, q.transferCount());
t.line(String.format("%8d %22d %24d", size, q.transferCount(), firstDequeue));
}
t.line("one dequeue can cost n moves, but each element moves exactly once, so n enqueues + n dequeues cost 3n steps in all");
t.blank();
t.line("StackViaQueue: n pushes");
t.line(String.format("%8s %20s %14s", "n", "total rotations", "n(n-1)/2"));
for (int size : new int[] {100, 1_000, 5_000}) {
StackViaQueue<Integer> s = new StackViaQueue<>();
for (int i = 0; i < size; i++) {
s.push(i);
}
long expected = (long) size * (size - 1) / 2;
assertEquals(expected, s.rotationCount());
t.line(String.format("%8d %20d %14d", size, s.rotationCount(), expected));
}
t.flush();
}
}
@@ -0,0 +1,152 @@
package com.ankurm.interviewlab.stackqueue;
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.assertTrue;
import com.ankurm.interviewlab.stackqueue.DryRuns.Trace;
import com.ankurm.interviewlab.stackqueue.support.Transcript;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
/** Writes the step-by-step traces and asserts each instrumented copy agrees with the real method. */
class DryRunTest {
@Test
void balancedParenthesesTrace() {
Transcript t = Transcript.start("01-balanced-parentheses-trace.txt", "Balanced brackets: every step of four inputs");
for (String input : new String[] {"{[()]}", "([)]", "())", "(()"}) {
Trace<Boolean> trace = DryRuns.balanced(input);
assertEquals(Parentheses.isBalanced(input), trace.result());
t.line("input: " + input + " firstMismatch = " + Parentheses.firstMismatch(input));
trace.lines().forEach(l -> t.line(" " + l));
t.blank();
}
assertTrue(Parentheses.isBalanced("{[()]}"));
assertFalse(Parentheses.isBalanced("([)]"));
assertEquals(2, Parentheses.firstMismatch("([)]"));
assertEquals(2, Parentheses.firstMismatch("())"));
assertEquals(0, Parentheses.firstMismatch("(()"));
t.flush();
}
@Test
void rpnTrace() {
Transcript t = Transcript.start("02-rpn-trace.txt", "Reverse Polish Notation: stack after every token");
String[][] cases = {{"2", "1", "+", "3", "*"}, {"4", "13", "5", "/", "+"}};
int[] expected = {9, 6};
for (int c = 0; c < cases.length; c++) {
Trace<Integer> trace = DryRuns.rpn(cases[c]);
assertEquals(expected[c], trace.result());
assertEquals(expected[c], Rpn.evaluate(cases[c]));
t.line("tokens: " + Arrays.toString(cases[c]) + " result = " + trace.result());
trace.lines().forEach(l -> t.line(" " + l));
t.blank();
}
t.flush();
}
@Test
void minStackTrace() {
Transcript t = Transcript.start("03-min-stack-trace.txt", "Min stack: both internal stacks after every operation");
String[] ops = {"push 5", "push 3", "push 7", "push 3", "getMin", "pop", "getMin", "pop", "getMin", "pop", "getMin"};
Trace<List<Integer>> trace = DryRuns.minStack(ops);
assertEquals(List.of(3, 3, 3, 5), trace.result());
// the same operations on both real implementations
for (IntMinStack real : new IntMinStack[] {new MinStack(), new EncodedMinStack()}) {
java.util.ArrayList<Integer> seen = new java.util.ArrayList<>();
for (String op : ops) {
if (op.startsWith("push ")) {
real.push(Integer.parseInt(op.substring(5)));
} else if (op.equals("pop")) {
real.pop();
} else {
seen.add(real.getMin());
}
}
assertEquals(trace.result(), seen);
}
trace.lines().forEach(t::line);
t.blank().line("getMin results: " + trace.result() + " (identical for MinStack and EncodedMinStack)");
t.flush();
}
@Test
void monotonicStackTrace() {
Transcript t = Transcript.start("04-monotonic-stack-trace.txt", "Next greater element: the stack of pending indices, step by step");
int[] a = {2, 1, 2, 4, 3};
Trace<int[]> trace = DryRuns.nextGreater(a);
assertArrayEquals(new int[] {4, 2, 4, -1, -1}, trace.result());
assertArrayEquals(MonotonicStack.nextGreater(a), trace.result());
t.line("input " + Arrays.toString(a));
trace.lines().forEach(l -> t.line(" " + l));
t.line("result " + Arrays.toString(trace.result())).blank();
int[] temps = {73, 74, 75, 71, 69, 72, 76, 73};
int[] wait = MonotonicStack.dailyTemperatures(temps);
assertArrayEquals(new int[] {1, 1, 4, 2, 1, 1, 0, 0}, wait);
t.line("dailyTemperatures " + Arrays.toString(temps));
t.line(" = " + Arrays.toString(wait)).blank();
int[] circle = {1, 2, 1};
assertArrayEquals(new int[] {2, -1, 2}, MonotonicStack.nextGreaterCircular(circle));
t.line("nextGreaterCircular " + Arrays.toString(circle) + " = " + Arrays.toString(MonotonicStack.nextGreaterCircular(circle)));
assertArrayEquals(new int[] {-1, 3, -1}, MonotonicStack.nextGreaterSubset(new int[] {4, 1, 2}, new int[] {1, 3, 4, 2}));
t.line("nextGreaterSubset [4, 1, 2] within [1, 3, 4, 2] = "
+ Arrays.toString(MonotonicStack.nextGreaterSubset(new int[] {4, 1, 2}, new int[] {1, 3, 4, 2})));
t.flush();
}
@Test
void histogramTrace() {
Transcript t = Transcript.start("05-histogram-trace.txt", "Largest rectangle in a histogram: pops, widths and areas");
int[] h = {2, 1, 5, 6, 2, 3};
Trace<Integer> trace = DryRuns.histogram(h);
assertEquals(10, trace.result());
assertEquals(10, MonotonicStack.largestRectangle(h));
t.line("heights " + Arrays.toString(h));
trace.lines().forEach(l -> t.line(" " + l));
t.line("largest rectangle = " + trace.result());
t.flush();
}
@Test
void queueAndStackConversionTrace() {
Transcript t = Transcript.start("06-queue-stack-conversions-trace.txt", "Queue from two stacks, and stack from one queue");
QueueViaStacks<Integer> q = new QueueViaStacks<>();
t.line("QueueViaStacks (stacks shown top first)");
q.enqueue(1);
t.line(" enqueue(1) " + q.snapshot());
q.enqueue(2);
t.line(" enqueue(2) " + q.snapshot());
q.enqueue(3);
t.line(" enqueue(3) " + q.snapshot());
int a = q.dequeue();
t.line(" dequeue() = " + a + " " + q.snapshot() + " transfers so far: " + q.transferCount());
q.enqueue(4);
t.line(" enqueue(4) " + q.snapshot());
int b = q.dequeue();
int c = q.dequeue();
int d = q.dequeue();
t.line(" dequeue() x3 = " + b + ", " + c + ", " + d + " " + q.snapshot() + " transfers so far: " + q.transferCount());
assertEquals(List.of(1, 2, 3, 4), List.of(a, b, c, d));
assertEquals(4, q.transferCount());
t.blank();
StackViaQueue<Integer> s = new StackViaQueue<>();
t.line("StackViaQueue");
for (int x = 1; x <= 3; x++) {
s.push(x);
t.line(" push(" + x + ") " + s.snapshot() + " rotations so far: " + s.rotationCount());
}
int p1 = s.pop();
int p2 = s.pop();
t.line(" pop() x2 = " + p1 + ", " + p2 + " " + s.snapshot());
assertEquals(3, p1);
assertEquals(2, p2);
assertEquals(3, s.rotationCount());
t.flush();
}
}
@@ -0,0 +1,51 @@
package com.ankurm.interviewlab.stackqueue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.ankurm.interviewlab.stackqueue.support.Transcript;
import java.util.List;
import org.junit.jupiter.api.Test;
/** Stack versus ArrayDeque, from reflection and from running both. */
class JdkStackTest {
@Test
void stackVersusArrayDeque() throws Exception {
Transcript t = Transcript.start("10-stack-vs-arraydeque.txt", "java.util.Stack versus ArrayDeque used as a stack");
assertEquals(List.of(3, 2, 1), JdkStackDemo.stackPopOrder());
assertEquals(List.of(1, 2, 3), JdkStackDemo.queuePollOrder());
t.line("ArrayDeque as a stack: push(1), push(2), push(3), then pop until empty -> " + JdkStackDemo.stackPopOrder());
t.line("ArrayDeque as a queue: offer(1), offer(2), offer(3), then poll until empty -> " + JdkStackDemo.queuePollOrder());
t.blank();
assertEquals(List.of(1, 2, 3), JdkStackDemo.legacyStackOrder());
assertEquals(List.of(3, 2, 1), JdkStackDemo.dequeOrder());
t.line("push(1), push(2), push(3), then iterate:");
t.line(" java.util.Stack -> " + JdkStackDemo.legacyStackOrder() + " (bottom to top)");
t.line(" ArrayDeque -> " + JdkStackDemo.dequeOrder() + " (top to bottom)");
t.blank();
assertEquals("java.util.Vector", JdkStackDemo.legacySuperclass());
assertTrue(JdkStackDemo.legacyPopIsSynchronized());
assertFalse(JdkStackDemo.dequePopIsSynchronized());
t.line("Stack.class.getSuperclass() = " + JdkStackDemo.legacySuperclass());
t.line("Stack.pop() is synchronized = " + JdkStackDemo.legacyPopIsSynchronized());
t.line("ArrayDeque.pop() is synchronized = " + JdkStackDemo.dequePopIsSynchronized());
t.blank();
assertEquals("accepted", JdkStackDemo.pushNullOnLegacy());
assertEquals("java.lang.NullPointerException", JdkStackDemo.pushNullOnDeque());
t.line("push(null) on Stack = " + JdkStackDemo.pushNullOnLegacy());
t.line("push(null) on ArrayDeque = " + JdkStackDemo.pushNullOnDeque());
t.blank();
assertEquals(2, JdkStackDemo.peekAfterPushes());
assertEquals(1, JdkStackDemo.peekAfterOffers());
t.line("ArrayDeque: push(1), push(2), peek() = " + JdkStackDemo.peekAfterPushes() + " (push is addFirst)");
t.line("ArrayDeque: offer(1), offer(2), peek() = " + JdkStackDemo.peekAfterOffers() + " (offer is addLast)");
t.flush();
}
}
@@ -0,0 +1,121 @@
package com.ankurm.interviewlab.stackqueue;
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.stackqueue.support.Transcript;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
/** Eight classic mistakes, each reproduced next to the correct answer. */
class PitfallsTest {
@Test
void pitfalls() {
Transcript t = Transcript.start("11-pitfalls.txt", "Eight classic mistakes, each reproduced");
assertTrue(Pitfalls.balancedCountsOnly("([)]"));
assertFalse(Parentheses.isBalanced("([)]"));
t.line("1. balanced brackets by counting each kind, with no stack");
t.line(" input \"([)]\": counts-only = " + Pitfalls.balancedCountsOnly("([)]") + " stack = " + Parentheses.isBalanced("([)]"));
t.line(" every count ends at zero, but the ] arrives while the ( is not the innermost open bracket");
t.blank();
t.line("2. min stack that compares boxed Integers with ==");
for (int[] values : new int[][] {{10, 20, 5}, {1000, 2000, 500}}) {
Pitfalls.BoxedEqualsMinStack broken = new Pitfalls.BoxedEqualsMinStack();
MinStack right = new MinStack();
for (int v : values) {
broken.push(v);
right.push(v);
}
broken.pop();
right.pop();
t.line(" push " + Arrays.toString(values) + ", pop, getMin(): broken = " + broken.getMin() + " correct = " + right.getMin());
}
t.line(" Integer.valueOf caches -128..127, so small values share one object and == happens to work");
t.blank();
Pitfalls.StrictMinStack strict = new Pitfalls.StrictMinStack();
strict.push(3);
strict.push(3);
strict.pop();
String failure;
try {
strict.getMin();
failure = "no exception";
} catch (NullPointerException e) {
failure = "NullPointerException: " + e.getMessage();
}
assertTrue(failure.startsWith("NullPointerException"));
MinStack good = new MinStack();
good.push(3);
good.push(3);
good.pop();
assertEquals(3, good.getMin());
t.line("3. min stack that pushes onto the min stack only on a strictly smaller value");
t.line(" push 3, push 3, pop, getMin(): strict -> " + failure);
t.line(" correct -> " + good.getMin() + " (the stack still holds one 3)");
t.blank();
Pitfalls.IntDiffMinStack overflow = new Pitfalls.IntDiffMinStack();
EncodedMinStack encoded = new EncodedMinStack();
for (int v : new int[] {Integer.MIN_VALUE, Integer.MAX_VALUE}) {
overflow.push(v);
encoded.push(v);
}
assertEquals(Integer.MIN_VALUE, overflow.top());
assertEquals(Integer.MAX_VALUE, encoded.top());
t.line("4. encoded min stack with int differences instead of long");
t.line(" push Integer.MIN_VALUE, push Integer.MAX_VALUE, top(): int diffs = " + overflow.top() + " long diffs = " + encoded.top());
t.line(" MAX_VALUE - MIN_VALUE does not fit in an int, wraps to -1, and -1 looks like \"new minimum\"");
t.blank();
int[] dup = {2, 2, 3};
assertArrayEquals(new int[] {2, 3, -1}, Pitfalls.nextGreaterPopsEqual(dup));
assertArrayEquals(new int[] {3, 3, -1}, MonotonicStack.nextGreater(dup));
t.line("5. next greater element popping on <= instead of <");
t.line(" input " + Arrays.toString(dup) + ": buggy = " + Arrays.toString(Pitfalls.nextGreaterPopsEqual(dup))
+ " correct = " + Arrays.toString(MonotonicStack.nextGreater(dup)));
t.line(" the first 2 is told its next greater element is 2");
t.blank();
int[] bars = {2, 4};
assertEquals(0, Pitfalls.largestRectangleNoSentinel(bars));
assertEquals(4, MonotonicStack.largestRectangle(bars));
t.line("6. largest rectangle without the final height-0 bar");
t.line(" input " + Arrays.toString(bars) + ": buggy = " + Pitfalls.largestRectangleNoSentinel(bars)
+ " correct = " + MonotonicStack.largestRectangle(bars));
t.line(" nothing ever pops the bars that are still on the stack when the loop ends");
t.blank();
Pitfalls.PourAlwaysQueue broken = new Pitfalls.PourAlwaysQueue();
QueueViaStacks<Integer> right = new QueueViaStacks<>();
broken.enqueue(1);
broken.enqueue(2);
right.enqueue(1);
right.enqueue(2);
int b1 = broken.dequeue();
int r1 = right.dequeue();
broken.enqueue(3);
right.enqueue(3);
int b2 = broken.dequeue();
int r2 = right.dequeue();
assertEquals(1, b1);
assertEquals(1, r1);
assertEquals(3, b2);
assertEquals(2, r2);
t.line("7. queue from two stacks that pours on every dequeue");
t.line(" enqueue 1, 2; dequeue; enqueue 3; dequeue: first = " + b1 + " (ok), second: broken = " + b2 + " correct = " + r2);
t.line(" the 3 was poured on top of the 2 that was still waiting in the output stack");
t.blank();
assertThrows(java.util.NoSuchElementException.class, () -> new QueueViaStacks<Integer>().dequeue());
assertThrows(java.util.NoSuchElementException.class, () -> new StackViaQueue<Integer>().pop());
t.line("8. popping an empty structure: both conversions throw NoSuchElementException, as ArrayDeque.pop does");
t.flush();
}
}
@@ -0,0 +1,183 @@
package com.ankurm.interviewlab.stackqueue;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.ankurm.interviewlab.stackqueue.support.Transcript;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.Test;
/** Every stack solution against its slow reference, on seeded random inputs. */
class RandomAgreementTest {
private static final int CASES = 5_000;
@Test
void agreement() {
Random rnd = new Random(20261112L);
Transcript t = Transcript.start("08-random-agreement.txt",
"Random agreement with brute force (seed 20261112, " + CASES + " cases per row)");
for (int c = 0; c < CASES; c++) {
int[] a = randomArray(rnd, rnd.nextInt(13), 6); // small value range forces many duplicates
assertArrayEquals(BruteForce.nextGreater(a), MonotonicStack.nextGreater(a));
assertArrayEquals(BruteForce.dailyTemperatures(a), MonotonicStack.dailyTemperatures(a));
assertArrayEquals(BruteForce.nextGreaterCircular(a), MonotonicStack.nextGreaterCircular(a));
assertEquals(BruteForce.largestRectangle(a), MonotonicStack.largestRectangle(a));
}
t.line(String.format("%-52s %d cases, 0 mismatches", "nextGreater vs scan-right", CASES));
t.line(String.format("%-52s %d cases, 0 mismatches", "dailyTemperatures vs scan-right", CASES));
t.line(String.format("%-52s %d cases, 0 mismatches", "nextGreaterCircular vs wrap-around scan", CASES));
t.line(String.format("%-52s %d cases, 0 mismatches", "largestRectangle vs widen-each-bar", CASES));
String alphabet = "()[]{}ab";
for (int c = 0; c < CASES; c++) {
StringBuilder sb = new StringBuilder();
int len = rnd.nextInt(13);
for (int i = 0; i < len; i++) {
sb.append(alphabet.charAt(rnd.nextInt(alphabet.length())));
}
String s = sb.toString();
assertEquals(BruteForce.isBalanced(s), Parentheses.isBalanced(s), s);
assertEquals(Parentheses.isBalanced(s), Parentheses.firstMismatch(s) == -1, s);
}
t.line(String.format("%-52s %d cases, 0 mismatches", "isBalanced vs delete-adjacent-pairs", CASES));
for (int c = 0; c < CASES; c++) {
String encoded = randomEncoded(rnd, 3);
assertEquals(BruteForce.decode(encoded), DecodeString.decode(encoded), encoded);
}
t.line(String.format("%-52s %d cases, 0 mismatches", "decode vs recursive descent", CASES));
for (int c = 0; c < CASES; c++) {
Expr e = randomExpr(rnd, 4);
List<String> tokens = new ArrayList<>();
e.toRpn(tokens);
assertEquals(e.eval(), Rpn.evaluate(tokens.toArray(new String[0])), tokens.toString());
}
t.line(String.format("%-52s %d cases, 0 mismatches", "evaluate (RPN) vs expression-tree evaluation", CASES));
int minStackOps = 0;
for (int c = 0; c < CASES; c++) {
IntMinStack[] impls = {new MinStack(), new EncodedMinStack()};
List<Integer> reference = new ArrayList<>();
for (int step = 0; step < 30; step++) {
if (reference.isEmpty() || rnd.nextInt(3) > 0) {
int x = rnd.nextBoolean() ? rnd.nextInt(2000) - 1000 : rnd.nextInt();
reference.add(x);
for (IntMinStack s : impls) {
s.push(x);
}
} else {
int expected = reference.remove(reference.size() - 1);
for (IntMinStack s : impls) {
assertEquals(expected, s.pop());
}
}
minStackOps++;
if (!reference.isEmpty()) {
int min = reference.stream().mapToInt(Integer::intValue).min().getAsInt();
int top = reference.get(reference.size() - 1);
for (IntMinStack s : impls) {
assertEquals(min, s.getMin());
assertEquals(top, s.top());
assertEquals(reference.size(), s.size());
}
}
}
}
t.line(String.format("%-52s %d sequences (%d operations), 0 mismatches", "MinStack and EncodedMinStack vs recompute-the-min", CASES, minStackOps));
long queueOps = 0;
for (int c = 0; c < CASES; c++) {
QueueViaStacks<Integer> q = new QueueViaStacks<>();
StackViaQueue<Integer> st = new StackViaQueue<>();
Deque<Integer> refQueue = new ArrayDeque<>();
Deque<Integer> refStack = new ArrayDeque<>();
for (int step = 0; step < 30; step++) {
if (refQueue.isEmpty() || rnd.nextInt(5) < 3) {
int x = rnd.nextInt(100);
q.enqueue(x);
refQueue.offer(x);
st.push(x);
refStack.push(x);
} else {
assertEquals(refQueue.poll(), q.dequeue());
assertEquals(refStack.pop(), st.pop());
}
queueOps++;
assertEquals(refQueue.size(), q.size());
assertEquals(refStack.size(), st.size());
}
}
t.line(String.format("%-52s %d sequences (%d operations), 0 mismatches", "QueueViaStacks and StackViaQueue vs ArrayDeque", CASES, queueOps));
t.flush();
}
private static int[] randomArray(Random rnd, int length, int bound) {
int[] a = new int[length];
for (int i = 0; i < length; i++) {
a[i] = rnd.nextInt(bound);
}
return a;
}
/** A valid encoded string such as 2[a3[bc]]d, built by chance. */
private static String randomEncoded(Random rnd, int depth) {
StringBuilder sb = new StringBuilder();
int parts = 1 + rnd.nextInt(3);
for (int p = 0; p < parts; p++) {
if (depth > 0 && rnd.nextInt(3) == 0) {
sb.append(1 + rnd.nextInt(3)).append('[').append(randomEncoded(rnd, depth - 1)).append(']');
} else {
int letters = 1 + rnd.nextInt(2);
for (int i = 0; i < letters; i++) {
sb.append((char) ('a' + rnd.nextInt(3)));
}
}
}
return sb.toString();
}
private interface Expr {
int eval();
void toRpn(List<String> out);
}
private static Expr randomExpr(Random rnd, int depth) {
if (depth == 0 || rnd.nextInt(4) == 0) {
int v = rnd.nextInt(19) - 9;
return new Expr() {
public int eval() {
return v;
}
public void toRpn(List<String> out) {
out.add(Integer.toString(v));
}
};
}
Expr left = randomExpr(rnd, depth - 1);
Expr right = randomExpr(rnd, depth - 1);
String op = new String[] {"+", "-", "*", "/"}[rnd.nextInt(4)];
if (op.equals("/") && right.eval() == 0) {
op = "+"; // never divide by zero
}
final String chosen = op;
return new Expr() {
public int eval() {
return Rpn.apply(chosen, left.eval(), right.eval());
}
public void toRpn(List<String> out) {
left.toRpn(out);
right.toRpn(out);
out.add(chosen);
}
};
}
}
@@ -0,0 +1,93 @@
package com.ankurm.interviewlab.stackqueue;
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.stackqueue.support.Transcript;
import java.util.Arrays;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.Test;
/** One headline example per problem, asserted and written to output/07. */
class SolutionsTest {
@Test
void allTenProblems() {
Transcript t = Transcript.start("07-all-ten-results.txt", "All ten problems on their headline examples");
assertTrue(Parentheses.isBalanced("{[()]}"));
assertFalse(Parentheses.isBalanced("([)]"));
t.line(" 1. isBalanced(\"{[()]}\") = " + Parentheses.isBalanced("{[()]}") + "; isBalanced(\"([)]\") = " + Parentheses.isBalanced("([)]"));
String[] rpn = {"2", "1", "+", "3", "*"};
assertEquals(9, Rpn.evaluate(rpn));
assertEquals(6, Rpn.evaluate(new String[] {"4", "13", "5", "/", "+"}));
assertEquals(-2, Rpn.evaluate(new String[] {"7", "-3", "/"}));
t.line(" 2. evaluate(" + Arrays.toString(rpn) + ") = " + Rpn.evaluate(rpn) + "; 7 / -3 truncates toward zero: "
+ Rpn.evaluate(new String[] {"7", "-3", "/"}));
MinStack ms = new MinStack();
EncodedMinStack es = new EncodedMinStack();
for (int x : new int[] {-2, 0, -3}) {
ms.push(x);
es.push(x);
}
assertEquals(-3, ms.getMin());
assertEquals(-3, es.getMin());
ms.pop();
es.pop();
assertEquals(0, ms.top());
assertEquals(0, es.top());
assertEquals(-2, ms.getMin());
assertEquals(-2, es.getMin());
t.line(" 3. push -2, 0, -3: getMin = -3; pop; top = " + ms.top() + ", getMin = " + ms.getMin() + " (both implementations agree)");
assertThrows(NoSuchElementException.class, () -> new MinStack().getMin());
assertThrows(NoSuchElementException.class, () -> new EncodedMinStack().pop());
int[] nums = {1, 3, 4, 2};
assertArrayEquals(new int[] {3, 4, -1, -1}, MonotonicStack.nextGreater(nums));
t.line(" 4. nextGreater(" + Arrays.toString(nums) + ") = " + Arrays.toString(MonotonicStack.nextGreater(nums)));
int[] temps = {73, 74, 75, 71, 69, 72, 76, 73};
assertArrayEquals(new int[] {1, 1, 4, 2, 1, 1, 0, 0}, MonotonicStack.dailyTemperatures(temps));
t.line(" 5. dailyTemperatures(" + Arrays.toString(temps) + ") = " + Arrays.toString(MonotonicStack.dailyTemperatures(temps)));
int[] circle = {1, 2, 3, 4, 3};
assertArrayEquals(new int[] {2, 3, 4, -1, 4}, MonotonicStack.nextGreaterCircular(circle));
t.line(" 6. nextGreaterCircular(" + Arrays.toString(circle) + ") = " + Arrays.toString(MonotonicStack.nextGreaterCircular(circle)));
int[] bars = {2, 1, 5, 6, 2, 3};
assertEquals(10, MonotonicStack.largestRectangle(bars));
assertEquals(4, MonotonicStack.largestRectangle(new int[] {2, 4}));
t.line(" 7. largestRectangle(" + Arrays.toString(bars) + ") = " + MonotonicStack.largestRectangle(bars));
assertEquals("abcbcabcbcabcbc", DecodeString.decode("3[a2[bc]]"));
assertEquals("accaccacc", DecodeString.decode("3[a2[c]]"));
assertEquals("abcabccdcdcdef", DecodeString.decode("2[abc]3[cd]ef"));
t.line(" 8. decode(\"3[a2[bc]]\") = \"" + DecodeString.decode("3[a2[bc]]") + "\"; decode(\"2[abc]3[cd]ef\") = \""
+ DecodeString.decode("2[abc]3[cd]ef") + "\"");
QueueViaStacks<String> q = new QueueViaStacks<>();
q.enqueue("a");
q.enqueue("b");
q.enqueue("c");
assertEquals("a", q.dequeue());
q.enqueue("d");
assertEquals("b", q.dequeue());
assertEquals("c", q.peek());
t.line(" 9. enqueue a, b, c; dequeue = a; enqueue d; dequeue = b; peek = " + q.peek() + "; size = " + q.size());
StackViaQueue<String> s = new StackViaQueue<>();
s.push("a");
s.push("b");
s.push("c");
assertEquals("c", s.top());
assertEquals("c", s.pop());
assertEquals("b", s.pop());
t.line("10. push a, b, c; top = c; pop = c; pop = b; size = " + s.size());
t.flush();
}
}
@@ -0,0 +1,43 @@
package com.ankurm.interviewlab.stackqueue.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);
}
}
}