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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
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,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();
|
||||
}
|
||||
}
|
||||
+183
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user