InterviewLegend

Arrays, Strings & Hashing

Alex Chen26 min readUpdated
In this lesson
  • Flag rolling-window spend with a per-user deque and a running sum
  • Count ledger runs that net to a target with a prefix-sum hash map
  • Narrate, absorb the escalating follow-up, and test money and duplicate edges

The norm reported again and again for the Block coding round is plain: optimize for done. You solve the problem live in CoderPad, sitting next to the interviewer. You can Google an API you forgot. Getting the first half working beats running out of time on a clever fragment that compiles into nothing. Look past the payments surface on most of what shows up, and you are left with an array walked once with a hash map on the side.

The substitution is consistent. A stream of card charges instead of an integer array. A double-entry ledger instead of a list of numbers. A batch of payment requests instead of a string of characters. Underneath sits the same medium you already drill. What wins the round is choosing the right structure, saying why, and handling the money edges nobody asked about yet.

The Block angle
A working solution starts the conversation here. Block grades the pairing as heavily as the answer. Do you narrate the trade-off? Does your code survive the next requirement? Do you write a test for a duplicate charge and a rounding edge? The opposite failure costs you too: burying a medium problem under shiny libraries and verbose tests. So get to running code, talk through why the deque drops expired entries in constant time, then probe your own solution with the cases a payments backend sees in production. Silence reads as luck, and over-engineering reads as missing the point.

How Block frames arrays and hashing

The single most reported phone-screen archetype is a transaction stream with a rolling time window. Flag a user when their spend crosses a threshold in the last N seconds. Detect fraud over a moving window. Group or aggregate a feed of charges. The tooling is a sliding window: a deque plus a hash map. The grade leans on whether you can justify the deque. Expired charges leave from the front in constant time, so you never re-sum the window.

Onsite, the array work tilts toward money correctness. Reconcile a ledger that has to balance to zero. Count the date ranges that net to a settlement figure. Dedup a stream of payment requests so a retry never double-charges. Beneath the Square and Cash App labels, these are prefix-sum and hash-set problems. Once the first part runs, the interviewer layers a second requirement on top, then watches whether you refactor cleanly or start over.

  • The signature screen: Rolling-window spend flag. A stream of charges per account, flag the moment a user crosses a spend limit inside the trailing window. Deque per user evicts aged charges from the front; a running sum avoids rescanning. Justifying the deque is graded as much as the code running.
  • The onsite tilt: Ledger and receipt correctness. Count contiguous entries that net to a target, find a balanced batch, total an order with refunds. Prefix sum plus a hash map of counts. The edges win it: zero-sum spans, negative movements, the empty ledger.
  • The hash-set move: Idempotent money movement. A payment request arrives more than once under at-least-once delivery. A seen-keys set makes the retry a no-op so the account is charged exactly once. Square's real idempotency_key API is the production form of this exact set.
  • What gets scored: Narration, layering, tests. Working code on CoderPad, the reason for your structure said aloud, a graceful response when the requirement changes, and tests over money, rounding, duplicates, and invalid states. Not the single most-optimal line.

Every problem in this family is an array swept once while a hash structure remembers something about what you have already seen. The rolling-window flag remembers each user's live charges in a deque. The ledger reconciliation remembers how many times each prefix sum has occurred. The idempotent settle remembers which request keys are spent. Pick what the hash structure should hold, and the single pass falls out.

Asked at Block

Three builds in the order Block tends to ask them: the rolling-window flag from the phone screen, then the ledger reconciliation, then the idempotent settle. Write each in the editor and run your own cases, especially the ones the prompt does not spell out, before opening the walkthrough.

1

Rolling-Window Spend Flag: Deque + Hash Map

MediumSignature problem
Problem
A risk service receives a time-ordered stream of card charges. Each charge is [time, user, amount] with time a non-decreasing integer (seconds), user an account id, and amount a positive integer (cents).

Flag a charge when, after applying it, that user's total spend inside the trailing window strictly exceeds limit. A charge belongs to the current charge's window for that user when current_time - time < window, so a charge exactly window seconds or more in the past has aged out and no longer counts.

Return the list of indices (into charges, in stream order) of the charges that trip the flag. Each user has an independent window and an independent running total.

Input: charges = [[1,"A",30],[2,"A",40],[3,"B",100],[5,"A",35]], window = 3, limit = 60

Output: [1,2]

At t=2, A has 30+40=70 in the last 3s, over 60, flag index 1. At t=3, B alone is 100, flag index 2. At t=5, A's charges from t=1 and t=2 have aged out, leaving only 35, under the limit.

Input: charges = [[1,"A",50],[2,"A",10],[10,"A",55]], window = 5, limit = 55

Output: [1]

At t=2, A has 60 in window, over 55, flag index 1. At t=10 the earlier two charges are gone and 55 does not strictly exceed 55, so no flag.

How this uses the pattern

Keep one deque of (time, amount) per user plus that user's running sum. On each charge, push it and add to the sum, then pop from the front while the front charge has aged out of the window, subtracting as you go. After eviction, compare the running sum to the limit. The deque gives O(1) front eviction, which is why you never re-sum the window.

Rolling-Window Spend Flag: Deque + Hash Map

Loading...
Click "Run" to execute your code

Test Cases

Input: [[1,"A",30],[2,"A",40],[3,"B",100],[5,"A",35]], 3, 60Expected: [1, 2]
Input: [[1,"A",50],[2,"A",10],[10,"A",55]], 5, 55Expected: [1]
Input: [[1,"A",40],[2,"B",40],[3,"A",30],[4,"B",30]], 10, 60Expected: [2, 3]
Input: [], 3, 60Expected: []
The Block angle
This is the Block phone screen in its most common shape. The grade rides on whether you reach for a deque and can say why. Charges expire in time order, so the oldest always leaves from the front in constant time, and a running sum means you never rescan. The per-user hash map is the second tell, since a global window would let one account's spend mask another's. Block escalates this live, usually to "now make the window slide by count instead of time" or "flag only when the limit is crossed, not on every charge after." Keep your eviction and your flag check separate and clean enough to bend.
Take it further
The natural second part is a per-merchant velocity rule layered on top of the per-user one. Now a charge is suspicious only if the same user crosses their spend limit AND hits more than K distinct merchants inside the window. Keep the deque holding (time, amount, merchant). Next to the running sum, keep a small hash map of merchant to live count for that user. Increment on push, decrement on eviction, and drop a merchant when its count hits zero. The flag condition becomes the sum check and the map's size, both already maintained in one pass. (Discussed in the solution.)
The Block angle
The next problem trades the moving window for a fixed target, and the hash map starts counting prefixes instead of holding charges. Block's onsite array work leans on reconciliation: a double-entry ledger has to balance, and a recurring ask is how many contiguous runs of entries net to a given figure. The brute force checks every span in quadratic time. The move they are listening for is a prefix sum with a hash map of how often each running total has appeared, which turns "find every balanced span" into one pass. It is the same single-sweep-plus-memory shape as the window flag, pointed at a different question.
2

Ledger Reconciliation: Prefix Sum + Hash Map

MediumCommonly asked
Problem
A double-entry ledger is given as movements, a list of signed integers in posting order, where each value is the net effect of one entry in cents (credits positive, debits negative).

Reconciliation asks: how many contiguous spans of entries net to exactly target? A span is a run movements[i..j] for i <= j, and a span that nets to target is a self-contained balanced batch (use target = 0 for a batch that fully cancels out). Spans that overlap are counted separately.

Return the count of contiguous spans summing to target.

Input: movements = [10,-10,30,-20,-10], target = 0

Output: 4

The zero-netting spans are [10,-10], [30,-20,-10], [10,-10,30,-20,-10], and the run from index 1 to 3 (-10,30,-20). Four in total.

Input: movements = [100,-50,-50,100], target = 100

Output: 3

The single entry at index 0, the single entry at index 3, and the whole array all net to 100.

How this uses the pattern

Walk a running prefix sum. A span ending at the current entry nets to target exactly when some earlier prefix equals current_prefix minus target. Keep a hash map from prefix value to how many indices produced it, seeded with {0: 1} for spans that start at index 0. Add its count to the answer before recording the current prefix.

Ledger Reconciliation: Prefix Sum + Hash Map

Loading...
Click "Run" to execute your code

Test Cases

Input: [10,-10,30,-20,-10], 0Expected: 4
Input: [100,-50,-50,100], 100Expected: 3
Input: [1,2,3], 3Expected: 2
Input: [], 0Expected: 0
The Block angle
Block casts the canonical prefix-sum-and-hash-map count as ledger reconciliation, and it fits the domain. A double-entry ledger really does have to balance, so "which runs of postings net to zero" is an honest reconciliation question. The signal is that you avoid the quadratic double loop by remembering prefix sums in a hash map of counts. The seed entry {0: 1} is where most candidates stumble, since without it you miss every balanced span that starts at the very first entry. Expect a follow-up layered on top, commonly "now return the longest balanced span," which swaps the count map for a map of earliest index per prefix.
Take it further
The usual extension asks for the longest balanced span, not the number of them. Keep the running prefix sum, but change the hash map to store the earliest index at which each prefix value appeared. When the current prefix minus target has been seen before, the span from just after that earliest index to here nets to target, and its length is the current index minus that stored index. Track the maximum such length. Record a prefix only the first time you see it, so the stored index stays the earliest and the span stays the longest. (Discussed in the solution.)
The Block angle
The last build is the smallest and the most payments-honest: charge a request exactly once even when it arrives twice. Under at-least-once delivery a client retries a payment it never heard back on, and the ledger must not double-charge. Square ships an idempotency_key on its public API for exactly this, and the in-memory version is a hash set of keys already applied. It is the same remember-what-you-have-seen instinct as the first two problems, stripped down to membership, and Block likes it because the naive answer silently charges a customer twice.
3

Idempotent Settle: Hash-Set Dedup

MediumCommonly asked
Problem
Payment requests arrive as requests, a list of [key, amount]. The key is the client's idempotency key for that payment and amount is a non-negative integer (cents). Because delivery is at-least-once, the same key can appear more than once: those repeats are retries of one logical payment.

Apply each request in order. The first time a key is seen, charge its amount. Any later request with a key already applied is a retry and must be a no-op, so the account is charged exactly once per key.

Return the total amount charged.

Input: requests = [["a",10],["b",5],["a",10],["c",7]]

Output: 22

Keys a, b, c each charge once for 10+5+7=22. The second ["a",10] is a retry and is ignored.

Input: requests = [["a",10],["a",999]]

Output: 10

The key a is already applied, so the retry is a no-op regardless of its amount. Only the first charge of 10 counts.

How this uses the pattern

Keep a set of keys already applied. For each request, if the key is in the set, skip it. Otherwise add the key and add its amount to the total. Membership in a hash set is O(1), so the whole pass is linear.

Idempotent Settle: Hash-Set Dedup

Loading...
Click "Run" to execute your code

Test Cases

Input: [[["a",10],["b",5],["a",10],["c",7]]]Expected: 22
Input: [[["a",10],["a",999]]]Expected: 10
Input: [[]]Expected: 0
Input: [[["x",0],["y",8],["x",0],["x",0]]]Expected: 8
The Block angle
This is the most literally Block problem in the set, since idempotency_key is a real field on Square's payments API and "do not double-charge on a retry" is a production invariant, not a puzzle. The hash set is all of it. The grade is whether you reach for it immediately and reason about the failure it prevents. At-least-once delivery makes a retry expected, so the account state has to depend on distinct keys, whatever the arrival count. The second requirement Block tends to add is awkward: a retry that reuses a key but carries a different amount. Reject the conflict instead of silently keeping the first.
Take it further
The realistic second part is a key reused with a conflicting amount. A well-behaved retry repeats both the key and the amount. Same key with a different amount means a client bug or a tampered replay. Store the applied amount per key in a hash map instead of a bare set. On a repeat with a matching amount, no-op as before; on a repeat with a mismatched amount, reject it (Square's real API answers a body-mismatch with a 4xx rather than charging or overwriting). Surface those rejected keys separately so the conflict is visible instead of swallowed. (Discussed in the solution.)
The Block angle
Before you call any of these done, test the money edges yourself, because that is where the round is won. The cases that earn credit are predictable: money, rounding, duplicate records, invalid states, all written without being asked. So once the window flag runs, feed it a user who spends right up to the limit, a charge that ages out the same second a new one lands, an empty stream. Reconcile a ledger that never balances and one that is all zeros. Settle a batch where every request is a retry. Then stop, because burying a medium problem in excess tests and shiny libraries costs you the other way. Cover the cases that matter, narrate why, and leave it clean.
4

Fraud Velocity Check: Distinct Devices in a Rolling Window

MediumSignature problem
Problem
A risk service reads a batch of account events in time order. Event i arrives as three parallel columns: times[i] is a non-decreasing integer timestamp in seconds, accounts[i] is the account id, and devices[i] is the device fingerprint the event came from.

Flag event i when, after applying it, the number of distinct devices that account has used inside the trailing window is at least threshold. An earlier event of the same account is still live for the current one when times[i] - times[j] < window, so an event exactly window seconds or more in the past has aged out and stops counting.

Return the list of indices of the flagged events in stream order. Each account owns its own window and its own device count, and repeat traffic from a device already inside the window does not raise that account's distinct total.

Input: times = [1,2,3,9], accounts = ["A","A","A","A"], devices = ["d1","d2","d1","d3"], window = 5, threshold = 2

Output: [1,2]

At t=2 account A is live on d1 and d2, so two distinct devices trips index 1. At t=3 the repeat of d1 holds the count at two, which trips index 2 as well. By t=9 the first three events have aged out and only d3 is live, leaving one device.

Input: times = [1,2,3,4,20], accounts = ["A","A","A","A","A"], devices = ["d1","d2","d2","d3","d4"], window = 5, threshold = 3

Output: [3]

The second d2 adds no new device, so A sits at two distinct devices until index 3 brings d3 and reaches three. At t=20 every earlier event has expired and only d4 is live.

How this uses the pattern

Per account, keep a deque of (time, device) alongside a map from device to how many live events used it. On each event, push onto the back and increment that device. Then pop from the front while the front has aged out, decrementing its device and deleting the key once it reaches zero. The size of that map is the distinct count, so one comparison decides the flag.

Fraud Velocity Check: Distinct Devices in a Rolling Window

Loading...
Click "Run" to execute your code

Test Cases

Input: [1,2,3,9], ["A","A","A","A"], ["d1","d2","d1","d3"], 5, 2Expected: [1, 2]
Input: [1,2,3,4,20], ["A","A","A","A","A"], ["d1","d2","d2","d3","d4"], 5, 3Expected: [3]
Input: [1,2,3,4], ["A","A","A","A"], ["d1","d1","d1","d1"], 10, 2Expected: []
Input: [1,1,2,2,3], ["A","B","A","B","A"], ["d1","d2","d1","d2","d9"], 10, 2Expected: [4]
Input: [0,5], ["A","A"], ["d1","d2"], 5, 2Expected: []
Input: [0,5], ["A","A"], ["d1","d2"], 6, 2Expected: [1]
Input: [], [], [], 5, 2Expected: []
The Block angle
Cash App scores a transfer in real time against a continuous customer risk rating, and the features behind that score get computed off the live event stream instead of read from a warehouse. Velocity counts are the cheap ones, something like how many distinct devices or distinct recipients an account has touched in the last few minutes. An account someone has taken over tends to surface there first, since whoever is draining it signs in from hardware the owner has never held. Swapping a running sum for a distinct count moves the work into the bookkeeping. Expiring an entry means decrementing a per-device counter and forgetting the key once it empties, so a stale key cannot inflate the count.
Take it further
The layer Block tends to add here is a window measured in events instead of seconds, plus a high-water mark per account. Hold only each account's last k events, so eviction fires when the deque grows past k and the timestamp stops mattering, while the device counting stays exactly as it was. Then track the largest distinct-device count each account's window ever reached, so a risk review can rank accounts without replaying the stream. Both changes live in the same loop. (Discussed in the solution.)
5

Daily Payout Rollup: Group a Transaction Stream by Seller

MediumFrequently asked
Problem
A settlement feed arrives as records, a list of rows in feed order. Each row is [recordId, seller, date, kind, amountCents].

- recordId is the feed's id for that row. Delivery is at-least-once, so the same recordId can arrive more than once, and a repeat is a redelivery of one row rather than new money.
- seller is the seller the money belongs to, and date is the settlement date as YYYY-MM-DD, already converted to that seller's local day.
- kind is one of "charge", "refund", "fee", and amountCents is a non-negative integer.

Roll the feed up into one net figure per (seller, date) bucket, where a charge adds and a refund or a fee subtracts. Every bucket holding at least one row appears in the output, including a bucket that comes out at 0 or below.

Return the buckets as a list of [seller, date, netCents], sorted by seller ascending and then by date ascending.

Input: records = [["r1","acme","2026-03-01","charge",5000],["r2","acme","2026-03-01","fee",150],["r3","acme","2026-03-01","refund",1200],["r4","bodega","2026-03-01","charge",900],["r5","acme","2026-03-02","charge",2500]]

Output: [["acme","2026-03-01",3650],["acme","2026-03-02",2500],["bodega","2026-03-01",900]]

acme nets 5000 - 150 - 1200 = 3650 on March 1 and 2500 on March 2. bodega has a single charge on March 1. Seller sorts first, so both acme days precede bodega.

Input: records = [["r1","acme","2026-03-01","charge",5000],["r1","acme","2026-03-01","charge",5000],["r2","acme","2026-03-02","refund",5000]]

Output: [["acme","2026-03-01",5000],["acme","2026-03-02",-5000]]

The second r1 is a redelivery, so March 1 still settles 5000. The refund carries its own date, which puts it on March 2 and leaves that day at -5000 instead of cancelling March 1.

Input: records = [["r9","kiosk","2026-01-31","fee",300]]

Output: [["kiosk","2026-01-31",-300]]

A day of fees with no sales behind them settles negative, which is a real payout the seller owes back rather than an error to clamp at zero.

How this uses the pattern

One pass with a hash map keyed by the (seller, date) pair, plus a set of recordIds already folded in. Skip a row whose id is in the set, otherwise record the id and add the amount with a sign picked from kind. Flatten the map into rows at the end and sort them, since a hash map hands back its keys in whatever order it likes.

Daily Payout Rollup: Group a Transaction Stream by Seller

Loading...
Click "Run" to execute your code

Test Cases

Input: [[["r1","acme","2026-03-01","charge",5000],["r2","acme","2026-03-01","fee",150],["r3","acme","2026-03-01","refund",1200],["r4","bodega","2026-03-01","charge",900],["r5","acme","2026-03-02","charge",2500]]]Expected: [["acme","2026-03-01",3650],["acme","2026-03-02",2500],["bodega","2026-03-01",900]]
Input: [[["r1","acme","2026-03-01","charge",5000],["r1","acme","2026-03-01","charge",5000],["r2","acme","2026-03-02","refund",5000]]]Expected: [["acme","2026-03-01",5000],["acme","2026-03-02",-5000]]
Input: [[["r9","kiosk","2026-01-31","fee",300]]]Expected: [["kiosk","2026-01-31",-300]]
Input: [[]]Expected: []
Input: [[["a1","zebra","2026-02-01","charge",1000],["a2","zebra","2026-02-01","refund",1000],["a3","acorn","2026-12-31","charge",10],["a4","acorn","2026-02-01","charge",20]]]Expected: [["acorn","2026-02-01",20],["acorn","2026-12-31",10],["zebra","2026-02-01",0]]
Input: [[["d1","mesa","2026-05-10","charge",700],["d2","mesa","2026-05-10","refund",250],["d2","mesa","2026-05-10","refund",250],["d3","mesa","2026-05-11","fee",100],["d1","mesa","2026-05-10","charge",700]]]Expected: [["mesa","2026-05-10",450],["mesa","2026-05-11",-100]]
The Block angle
Square deposits a seller's day as one payout, and that payout is the day's charges with the same day's refunds and processing fees taken back out. Bucketing a feed that way takes a few lines, which is why the round moves almost immediately to the edges around it. A refund posted the morning after the sale settles on the morning after, and back-dating it onto the sale's day puts two deposits out by the same amount. Redelivered rows have to fold in once. A day whose refunds outrun its charges settles below zero, where clamping it hides money the seller owes back. Sorting earns its keep too, because a payout file gets diffed against the ledger downstream.
Take it further
The second part tends to take the feed away from you. The rows now stream in without end and slightly out of order, so holding every bucket until the input runs out stops being an option. Track the newest date seen as a watermark and keep only the dates still close enough to be open. Once the watermark passes a date by more than the lateness you allow, emit that date's buckets and drop them. Scope the seen-ids set per date so it leaves with its bucket instead of growing forever. A row that turns up for a closed date goes to a late queue, so a deposited payout never quietly reopens. The grouping arithmetic survives untouched; what gets added is a moment where each bucket closes. (Discussed in the solution.)

Key Takeaways

  • ✓Block frames arrays and hashing as money movement: a charge stream instead of an integer array, a double-entry ledger instead of a list of numbers, a batch of payment requests instead of a string. The algorithm underneath is the recognizable medium you already practice. Read past the framing and reach for the structure.
  • ✓The signature is a single sweep with a hash structure holding what you've seen. A per-user deque of live charges for the rolling-window flag. A prefix-sum count map for ledger reconciliation. A set of applied keys for idempotent settle. Decide what the hash structure remembers and the one pass falls out.
  • ✓It is a pairing round, so talk. You can Google an API, getting to working code beats a clever fragment, and they grade narration and your response to the requirement they stack on as much as the answer. Say why the deque evicts in O(1), then take the follow-up without rewriting from scratch.
  • ✓Test the money edges the prompt did not mention, since Block rewards covering rounding, duplicates, and invalid states unprompted, then stop before over-engineering. A retry that double-charges or a ledger span miscounted by one is the failure these problems are built to expose.
Had your Block interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Block interview guide

Know what Block asks before you walk in.

13 coding · 5 system design · 14 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.