diff --git a/README.md b/README.md index cf7bf34..ca16a62 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ One module per article. This repository currently holds: | [`sliding-window`](sliding-window/) | Sliding Window and Two Pointers in Java: 12 Interview Problems Solved | this commit | | [`dynamic-programming`](dynamic-programming/) | Dynamic Programming in Java for Beginners: Memoization vs Tabulation (10 Classic Problems) | this commit | | [`fresher-qa`](fresher-qa/) | Top 50 Java Interview Questions for Freshers (2026) with Simple Answers | this commit | +| [`stack-queue`](stack-queue/) | Stack and Queue Problems in Java: Balanced Parentheses, Next Greater Element, Min Stack | this commit | ## `collections` module diff --git a/stack-queue/README.md b/stack-queue/README.md new file mode 100644 index 0000000..eb418a4 --- /dev/null +++ b/stack-queue/README.md @@ -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 +``` diff --git a/stack-queue/output/01-balanced-parentheses-trace.txt b/stack-queue/output/01-balanced-parentheses-trace.txt new file mode 100644 index 0000000..0a7e77a --- /dev/null +++ b/stack-queue/output/01-balanced-parentheses-trace.txt @@ -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 + diff --git a/stack-queue/output/02-rpn-trace.txt b/stack-queue/output/02-rpn-trace.txt new file mode 100644 index 0000000..422c80f --- /dev/null +++ b/stack-queue/output/02-rpn-trace.txt @@ -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] + diff --git a/stack-queue/output/03-min-stack-trace.txt b/stack-queue/output/03-min-stack-trace.txt new file mode 100644 index 0000000..3dfc169 --- /dev/null +++ b/stack-queue/output/03-min-stack-trace.txt @@ -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) diff --git a/stack-queue/output/04-monotonic-stack-trace.txt b/stack-queue/output/04-monotonic-stack-trace.txt new file mode 100644 index 0000000..57791d2 --- /dev/null +++ b/stack-queue/output/04-monotonic-stack-trace.txt @@ -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] diff --git a/stack-queue/output/05-histogram-trace.txt b/stack-queue/output/05-histogram-trace.txt new file mode 100644 index 0000000..5d08a7c --- /dev/null +++ b/stack-queue/output/05-histogram-trace.txt @@ -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 diff --git a/stack-queue/output/06-queue-stack-conversions-trace.txt b/stack-queue/output/06-queue-stack-conversions-trace.txt new file mode 100644 index 0000000..7a793a4 --- /dev/null +++ b/stack-queue/output/06-queue-stack-conversions-trace.txt @@ -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] diff --git a/stack-queue/output/07-all-ten-results.txt b/stack-queue/output/07-all-ten-results.txt new file mode 100644 index 0000000..86b5c39 --- /dev/null +++ b/stack-queue/output/07-all-ten-results.txt @@ -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 diff --git a/stack-queue/output/08-random-agreement.txt b/stack-queue/output/08-random-agreement.txt new file mode 100644 index 0000000..279da34 --- /dev/null +++ b/stack-queue/output/08-random-agreement.txt @@ -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 diff --git a/stack-queue/output/09-operation-counts.txt b/stack-queue/output/09-operation-counts.txt new file mode 100644 index 0000000..e0fced1 --- /dev/null +++ b/stack-queue/output/09-operation-counts.txt @@ -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 diff --git a/stack-queue/output/10-stack-vs-arraydeque.txt b/stack-queue/output/10-stack-vs-arraydeque.txt new file mode 100644 index 0000000..b3ea399 --- /dev/null +++ b/stack-queue/output/10-stack-vs-arraydeque.txt @@ -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) diff --git a/stack-queue/output/11-pitfalls.txt b/stack-queue/output/11-pitfalls.txt new file mode 100644 index 0000000..dd0c89d --- /dev/null +++ b/stack-queue/output/11-pitfalls.txt @@ -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 diff --git a/stack-queue/pom.xml b/stack-queue/pom.xml new file mode 100644 index 0000000..07a8f10 --- /dev/null +++ b/stack-queue/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + com.ankurm.interviewlab + stack-queue + 1.0.0 + jar + + java-interview-lab :: stack-queue + + 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. + + + + 25 + UTF-8 + 6.1.3 + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.platform + junit-platform-launcher + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.0 + + -Xmx512m -Duser.timezone=UTC -Dfile.encoding=UTF-8 + + + + + diff --git a/stack-queue/scripts/run-all.sh b/stack-queue/scripts/run-all.sh new file mode 100755 index 0000000..38df9a6 --- /dev/null +++ b/stack-queue/scripts/run-all.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Regenerates every file in output/. Needs JDK 25 and Maven. The first Maven run must be online. +# Every file is written by a JUnit test that asserts the same numbers it prints, so the figures +# in the article are assertions: they fail the build the moment they stop being true. +set -euo pipefail +cd "$(dirname "$0")/.." +mkdir -p output +mvn -B -q -T1 test +echo "done:"; ls -1 output diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/BruteForce.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/BruteForce.java new file mode 100644 index 0000000..9ad95ef --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/BruteForce.java @@ -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(); + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Complexity.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Complexity.java new file mode 100644 index 0000000..532f5da --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Complexity.java @@ -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 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; + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DecodeString.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DecodeString.java new file mode 100644 index 0000000..3db93fd --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DecodeString.java @@ -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 parked = new ArrayDeque<>(); + Deque 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(); + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DryRuns.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DryRuns.java new file mode 100644 index 0000000..25c94e9 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/DryRuns.java @@ -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(List 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 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 balanced(String s) { + List lines = new ArrayList<>(); + Deque 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 rpn(String[] tokens) { + List lines = new ArrayList<>(); + Deque 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> minStack(String... ops) { + List lines = new ArrayList<>(); + List minsSeen = new ArrayList<>(); + Deque values = new ArrayDeque<>(); + Deque 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 nextGreater(int[] a) { + List lines = new ArrayList<>(); + int[] result = new int[a.length]; + Arrays.fill(result, -1); + Deque 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 histogram(int[] heights) { + List lines = new ArrayList<>(); + int n = heights.length; + int best = 0; + Deque 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); + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/EncodedMinStack.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/EncodedMinStack.java new file mode 100644 index 0000000..bc9b819 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/EncodedMinStack.java @@ -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 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"); + } + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/IntMinStack.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/IntMinStack.java new file mode 100644 index 0000000..74649fc --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/IntMinStack.java @@ -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(); +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/JdkStackDemo.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/JdkStackDemo.java new file mode 100644 index 0000000..92f00c0 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/JdkStackDemo.java @@ -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 stackPopOrder() { + Deque stack = new ArrayDeque<>(); + stack.push(1); + stack.push(2); + stack.push(3); + List 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 queuePollOrder() { + Queue queue = new ArrayDeque<>(); + queue.offer(1); + queue.offer(2); + queue.offer(3); + List 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 legacyStackOrder() { + Stack 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 dequeOrder() { + Deque 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().push(null); + return "accepted"; + } catch (RuntimeException e) { + return e.getClass().getName(); + } + } + + public static String pushNullOnDeque() { + try { + new ArrayDeque().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 d = new ArrayDeque<>(); + d.push(1); + d.push(2); + return d.peek(); + } + + public static int peekAfterOffers() { + Deque d = new ArrayDeque<>(); + d.offer(1); + d.offer(2); + return d.peek(); + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MinStack.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MinStack.java new file mode 100644 index 0000000..d41846c --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MinStack.java @@ -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 values = new ArrayDeque<>(); + private final Deque 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"); + } + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MonotonicStack.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MonotonicStack.java new file mode 100644 index 0000000..8043d59 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/MonotonicStack.java @@ -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 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 nextOf = new HashMap<>(); + Deque 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 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 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 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; + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Parentheses.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Parentheses.java new file mode 100644 index 0000000..c0c18b3 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Parentheses.java @@ -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 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 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 == ']'); + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Pitfalls.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Pitfalls.java new file mode 100644 index 0000000..382c32e --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Pitfalls.java @@ -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 values = new ArrayDeque<>(); + private final Deque 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 values = new ArrayDeque<>(); + private final Deque 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 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 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 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 in = new ArrayDeque<>(); + private final Deque 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(); + } + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/QueueViaStacks.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/QueueViaStacks.java new file mode 100644 index 0000000..fb93416 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/QueueViaStacks.java @@ -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 { + private final Deque in = new ArrayDeque<>(); + private final Deque 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++; + } + } + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Rpn.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Rpn.java new file mode 100644 index 0000000..67a1e53 --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/Rpn.java @@ -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 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); + }; + } +} diff --git a/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/StackViaQueue.java b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/StackViaQueue.java new file mode 100644 index 0000000..f94200b --- /dev/null +++ b/stack-queue/src/main/java/com/ankurm/interviewlab/stackqueue/StackViaQueue.java @@ -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 { + private final Queue 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; + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/ComplexityTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/ComplexityTest.java new file mode 100644 index 0000000..9272325 --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/ComplexityTest.java @@ -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 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 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(); + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/DryRunTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/DryRunTest.java new file mode 100644 index 0000000..be6299f --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/DryRunTest.java @@ -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 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 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> 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 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 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 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 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 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(); + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/JdkStackTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/JdkStackTest.java new file mode 100644 index 0000000..6fb9b34 --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/JdkStackTest.java @@ -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(); + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/PitfallsTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/PitfallsTest.java new file mode 100644 index 0000000..3533b69 --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/PitfallsTest.java @@ -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 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().dequeue()); + assertThrows(java.util.NoSuchElementException.class, () -> new StackViaQueue().pop()); + t.line("8. popping an empty structure: both conversions throw NoSuchElementException, as ArrayDeque.pop does"); + t.flush(); + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/RandomAgreementTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/RandomAgreementTest.java new file mode 100644 index 0000000..2d59920 --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/RandomAgreementTest.java @@ -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 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 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 q = new QueueViaStacks<>(); + StackViaQueue st = new StackViaQueue<>(); + Deque refQueue = new ArrayDeque<>(); + Deque 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 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 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 out) { + left.toRpn(out); + right.toRpn(out); + out.add(chosen); + } + }; + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/SolutionsTest.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/SolutionsTest.java new file mode 100644 index 0000000..b7d2ead --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/SolutionsTest.java @@ -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 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 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(); + } +} diff --git a/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/support/Transcript.java b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/support/Transcript.java new file mode 100644 index 0000000..e66922e --- /dev/null +++ b/stack-queue/src/test/java/com/ankurm/interviewlab/stackqueue/support/Transcript.java @@ -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 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); + } + } +}