skip to content

Arrays & Strings

Learn contiguous-memory structures from the ground up: how arrays are laid out and indexed, how dynamic arrays grow, how matrices are stored and traversed, and how strings are represented and manipulated across languages. Interviewers probe this layer constantly because almost every coding problem starts from an array or a string, and your reasoning about costs here reveals whether you understand memory, not just APIs.

part ofData structures & algorithmsoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

A dynamic array's size falls from a million to twelve — why doesn't its memory footprint fall too?

level: juniorimportance: must knowfreq 66%

answer

  1. two numbers, not one
  2. which number does an erase change?
  3. the block is a single allocation
  4. capacity is a high-water mark
  5. giving it back means another copy

basics

~20 s

A dynamic array tracks two separate numbers: size, the elements in use, and capacity, the slots allocated. Removing elements lowers size only. The underlying buffer stays as large as it ever grew until something explicitly shrinks it.

solid answer

~50 s

A dynamic array is a fixed block of storage plus a `size` counter. `capacity` is how many slots the block holds; `size` is how many are logically in use. Erasing elements decrements `size` and leaves `capacity` alone, so capacity behaves as a high-water mark: it records the largest the structure ever got, not how big it is now. Nothing about erasing frees a byte — the block is a single allocation, and you cannot return the tail of it piecemeal. Handing the memory back means allocating a smaller block, copying the surviving elements into it, and releasing the old one, which is an O(n) operation someone has to ask for. In implementations that store handles rather than values inline, the slots past the logical size can also keep removed items alive until they are overwritten.

go deeper

for a junior

Be ready to state the two numbers by name and say which one an erase changes. If you can say 'size went to twelve, capacity stayed at a million, and the block is one allocation', you have answered the screening version.

for a middle

Explain why partial return is impossible: the storage is a single contiguous allocation, so reclaiming means a new smaller block plus a copy. Mention that abandoned slots can keep removed items alive if they are not overwritten.

for a senior

Show that you would measure before acting — retained bytes per instance times instance count, over how long. Be ready to say that reclaiming costs an O(n) copy, briefly raises peak memory, and invalidates anything holding a position into the old storage.

for a principal

Own the policy question: whether long-lived structures in your services should ever return their peak footprint, what that discipline costs in code and review attention, and whether a scheduled rebuild at a quiet moment beats per-operation cleverness.

## Two numbers, not one A dynamic (growable) array is built on a fixed-size block of contiguous storage. Because that block cannot be extended in place, the structure keeps two independent quantities: - **size** — how many elements are logically present. This is what iteration length, bounds checks and the "is it empty" test use. - **capacity** — how many element slots the currently allocated block can hold. This is a property of the *allocation*, not of the data. The invariant is `0 <= size <= capacity`. Everything surprising about memory in a growable array follows from the fact that these two numbers move independently: appends push `size` up and only occasionally push `capacity` up; erases push `size` down and, under the most common policies, never push `capacity` down at all. ## Why erasing frees nothing The block is one allocation. When your work queue drains from 1,048,576 entries to 12, the structure sets `size = 12` and possibly clears the abandoned slots — but the allocation is still one object of one size, and there is no way to return "the last 1,048,564 slots" of it to the allocator. Allocators hand out and take back whole blocks. So `capacity` is a **high-water mark**. It remembers the largest the structure ever became, for as long as the structure lives. That is a deliberate design choice, not an oversight: it makes the erase path cheap and predictable (no allocation, no copy, no invalidation), and it means a container that repeatedly fills and drains does its allocation work once instead of on every cycle. ## What it costs you Two distinct costs hide behind a retained capacity: 1. **Retained bytes.** A long-lived structure that once held a million entries holds the footprint of a million entries forever. Multiply by the number of such structures and the number of running instances and the number is often real. 2. **Retained *elements*.** If the storage holds handles/references rather than the values inline, the slots beyond `size` may still contain the old references. Logically the elements are gone; physically something still points at them, so they cannot be reclaimed. Careful implementations null out or destroy the abandoned slots on erase precisely to avoid this; naive hand-rolled ones do not. This is why "my structure is empty but memory did not drop" sometimes has *two* separate causes at once. ## Giving the memory back There is exactly one mechanism: allocate a smaller block, copy the live elements into it, release the old block. That is O(n) time, and while both blocks are live it *temporarily raises* peak memory rather than lowering it. It also moves every element, so any reference or position captured into the old block becomes meaningless. In other words, shrinking has the same cost profile and the same invalidation consequences as growing. That is why it is generally an explicit request rather than something that happens on every erase. | operation | changes size | changes capacity | moves elements | |---|---|---|---| | append within capacity | yes | no | no | | append past capacity | yes | yes (up) | yes | | erase | yes | no | shifts, if erasing mid-array | | clear | yes (to 0) | no | no | | explicit shrink | no | yes (down) | yes | ## Degrees of freedom across ecosystems The policy is genuinely a choice, and mainstream runtimes chose differently. Growable arrays in C++ and Rust never hand a byte back on their own — reducing the buffer is an explicit, and in one case non-binding, request. The list type in mainstream Python builds does the opposite and reallocates downward once the size falls well below the allocated slots. Managed runtimes such as those behind Java and C# sit with the first group: the backing block persists at its high-water mark until you ask for a trimmed copy. If you carry an assumption about which behaviour is "normal" from one ecosystem to another, you will be wrong half the time — which is exactly why interviewers ask this as a concept question rather than an API question. ## What to say in the room Name the two numbers, say that erase touches only one of them, say that the block is a single allocation so partial return is impossible, and say that reclaiming means a fresh smaller allocation plus a copy plus invalidation. That is the whole answer, and it generalises to every growable buffer you will ever meet, including string builders and byte buffers.

  • Does clearing every element release the memory?
    No. Clear sets size to zero and leaves capacity untouched — the block is exactly as large as before. Worse, in storage that holds handles rather than inline values, the abandoned slots can keep the removed items alive unless the implementation overwrites them. Clearing is a cheap logical reset, not a memory operation.
  • If capacity never shrinks, is memory use unbounded over the process lifetime?
    No — it is bounded by the peak size the structure ever reached, not by the total number of elements ever inserted. A container that cycles between 10 and 100 entries settles near a capacity for 100. The pathological case is a long-lived container that sees one rare spike and then idles at that footprint for months.
  • How do you actually give the memory back?
    Allocate a smaller block, copy the survivors, release the old one — either through an explicit shrink operation where one exists, or by building a fresh structure sized to the survivors and dropping the old. Both are O(n), both briefly hold two blocks at once, and both move every element, so anything holding a position into the old storage is invalidated.

A rented warehouse: you can empty every shelf in an afternoon, but you keep paying for the same floor space until you sign a smaller lease and physically move the remaining stock.

saying these in an interview costs you the question

  • Removing elements immediately frees the memory
  • Size and capacity are the same number
  • Shrinking the buffer is free because it only reduces memory
  • Clearing the container releases every element it held
  • Capacity always equals the current element count

context

open as a page

Why does a growable array track both a size and a capacity, and what happens when they are equal?

level: juniorimportance: must knowfreq 82%

basics

~20 s

Size is how many elements are stored; capacity is how many slots the underlying block can hold. Appending when size equals capacity forces allocating a larger block, copying every stored element into it, and releasing the old one.

open as a page

After in-place compaction with a write pointer, why must the caller be given a returned length?

level: juniorimportance: must knowfreq 74%

basics

~10 s

In-place compaction never shrinks the array, so survivors occupy only a prefix and stale values still sit in the tail. The returned write count is the only signal of how many entries are valid.

open as a page

Why does an in-place array reversal need only n/2 swaps rather than n?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Each swap places two elements at once, so n/2 swaps fix all n positions. Looping over every index instead swaps each pair twice, which undoes the reversal and hands back the original array.

open as a page

Why does a flat pixel buffer index as y*width+x, and what breaks if you write x*width+y?

level: juniorimportance: must knowfreq 65%

basics

~20 s

A flat row-major buffer stores each row as width consecutive slots, so the cell at column x, row y lives at ywidth+x. Writing xwidth+y addresses the transposed cell: harmless-looking on square images, corrupting or overrunning everything else.

open as a page

Why does transposing a square grid then reversing each row rotate it a quarter turn clockwise?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Transposing mirrors the grid across its main diagonal, which puts every element in its correct destination row but with the columns running backwards. Reversing each row fixes that order. Two mirrors at 45 degrees to each other compose into one quarter turn.

open as a page

Why does building a string by repeated concatenation in a loop cost O(n^2) in total?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Each concatenation on an immutable string allocates a brand-new string and copies every character accumulated so far. Copying 1 + 2 + ... + n characters sums to about n^2/2, so the loop is quadratic, not linear.

open as a page

Why can a 10-character name occupy 34 bytes when stored as UTF-8?

level: juniorimportance: must knowfreq 70%

basics

~10 s

UTF-8 is variable-width: one code point takes one to four bytes. Unaccented Latin letters cost one byte, accented letters two, most emoji four. So ten characters can weigh anywhere from ten to forty bytes.

open as a page

What does it mean for a string to be immutable, and what does immutability buy you?

level: juniorimportance: must knowfreq 74%

basics

~20 s

Immutable means an existing string's characters never change: operations that look like edits return a new string. Because the value cannot change under anyone holding it, it can be shared without defensive copies, hashed once, and read concurrently.

open as a page

Why does summing a contiguous array of sensor readings beat summing the same values through scattered references, when both are O(n)?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Memory moves in fixed-size blocks, so one fetch of a contiguous array delivers many neighbouring readings at once and the hardware can run ahead and fetch the next block early. Scattered references pay a separate, unpredictable trip per value.

open as a page

Why does an array give O(1) access to any index — what address computation makes element 5,000,000 one step away?

level: juniorimportance: must knowfreq 85%

basics

~20 s

An array stores equal-size elements contiguously, so the address of element i is base + i × element-size. That is one multiply and one add no matter how large i is, so element 5,000,000 costs the same as element 3.

open as a page

Why is naive substring search O(n*m) in the worst case yet near O(n) on ordinary text?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Naive search retries the pattern at every offset, so each of the roughly n offsets can cost up to m comparisons. On ordinary text most offsets mismatch within a symbol or two, so the scan behaves near-linearly.

open as a page

Why check palindrome symmetry with two inward-walking indices instead of reversing the text?

level: juniorimportance: must knowfreq 82%

basics

~20 s

Walking one index from each end inward compares mirrored characters directly, uses O(1) extra space, and can stop at the first mismatch. Reversing first is also O(n) time but allocates a whole second copy of the input.

open as a page

Why does growing a buffer by a fixed 4096 slots per overflow make n appends quadratic?

level: middleimportance: must knowfreq 66%

basics

~20 s

A fixed chunk means one reallocation every 4096 appends, and each of those copies the entire prefix stored so far. Total copy work therefore grows with n squared. A bigger chunk lowers the constant but never changes the quadratic order.

open as a page

In a write-pointer compaction loop, what invariant guarantees the write never destroys unread data?

level: middleimportance: must knowfreq 66%

basics

~20 s

The write index advances only on a keep while the read index advances every step, so write never overtakes read. Every slot written was therefore already read, and the region below write holds exactly the survivors so far, in order.

open as a page

How does the three-reversal trick rotate an array by k positions in place?

level: middleimportance: must knowfreq 66%

basics

~20 s

Reverse the whole array, then its first k elements and its last n-k. The full reverse brings the last k entries to the front but backwards; the two partial reverses repair each block's order, using no extra array.

open as a page

Why can truncating text at a fixed UTF-16 code-unit index emit an invalid character?

level: middleimportance: must knowfreq 58%

basics

~20 s

Code points beyond the Basic Multilingual Plane are stored as two UTF-16 code units, a surrogate pair. Cutting between them leaves an unpaired surrogate, which is not a valid character and renders or serializes as a replacement symbol.

open as a page

Two strings hold identical characters — why can an identity comparison still report them as different?

level: middleimportance: must knowfreq 68%

basics

~20 s

Identity comparison asks whether two references point at one object; content comparison asks whether the characters match. Equal contents can live in two separately allocated strings, so identity is false while content is true. Compare contents unless every value was canonicalized.

open as a page

How can an out-of-bounds array write at index equal to the length silently corrupt neighbouring data instead of failing?

level: middleimportance: must knowfreq 60%

basics

~20 s

The address formula base + i × element-size works for any i, valid or not; index equal to the length lands one element past the array, on whatever is stored next. Unchecked runtimes just write there; checked runtimes compare the index against the stored length first and raise an error.

open as a page

In an array-backed alert feed, why is inserting at index 0 O(n) but appending O(1)?

level: middleimportance: must knowfreq 76%

basics

~20 s

An array's slots are fixed positions, so making room at index 0 means moving every existing element one slot right — n moves. Appending writes into the first free slot and moves nothing, so it costs one write.

open as a page

Why does KMP run in O(n+m) when a naive scan can degrade to O(n*m)?

level: middleimportance: must knowfreq 58%

basics

~20 s

KMP never moves its text pointer backward. On a mismatch it shrinks the matched pattern length using the precomputed table instead of restarting the text one position later, so the scan costs O(n) after an O(m) table build.

open as a page

How does Rabin-Karp's rolling hash update each window in O(1), and what does that save?

level: middleimportance: must knowfreq 70%

basics

~20 s

A rolling hash derives the next window's hash from the current one in constant time: drop the leading symbol's weighted term, shift the rest, add the trailing symbol. Hashing each window from scratch would cost O(m) per position.

open as a page

How do sorting and character counting compare as canonical forms for anagram detection?

level: middleimportance: must knowfreq 74%

basics

~20 s

Sorting both inputs and comparing produces a canonical form in O(L log L); tallying character frequencies produces one in O(L). Counting wins asymptotically, but only if the tally covers the real alphabet rather than a fixed table of 26 lowercase letters.

open as a page

In KMP string matching, what does the failure (prefix) function table store?

level: juniorimportance: should knowfreq 42%

basics

~20 s

For each prefix of the pattern, the failure function stores the length of the longest proper prefix of that prefix which is also a suffix of it. It is derived from the pattern alone and says nothing about the text.

open as a page

Why can a saved reference into a growable array's storage go stale after later appends?

level: middleimportance: should knowfreq 52%

basics

~20 s

An append past capacity allocates a larger block, copies the elements over and releases the old one. A reference captured into the old block then names freed storage. Indices survive the move; handles into the buffer do not.

open as a page

In a three-way partition into below, equal and above a threshold, why does the scan index not advance after the high-side swap?

level: middleimportance: should knowfreq 52%

basics

~20 s

The high-side swap pulls a value out of the still-unexamined region and drops it under the scan index. That value has not been classified yet, so the scan index must stay and inspect it next iteration.

open as a page

Why can a column-order scan of a huge row-major elevation grid run an order of magnitude slower than a row-order scan?

level: middleimportance: should knowfreq 58%

basics

~20 s

Both loops touch the same number of cells, but row-major storage puts a row's cells next to each other. A row-order scan walks memory one slot at a time; a column-order scan jumps a full row width per step, so each access lands in a different region.

open as a page

Reviewing this in-place transpose, what does swapping m[i][j] and m[j][i] over every i and j produce?

level: middleimportance: should knowfreq 52%

basics

~20 s

Nothing changes. Each off-diagonal pair gets swapped twice, once as (i, j) and again as (j, i), so the second swap undoes the first, and diagonal swaps are no-ops. The inner loop must start at j = i + 1.

open as a page

In a spiral-order walk with top, bottom, left and right bounds, why recheck the bounds mid-lap?

level: middleimportance: should knowfreq 58%

basics

~20 s

A lap can exhaust the region halfway through. Once the top row and right column are consumed, what remains may be a single row or column, and without rechecking the bounds the return passes walk it twice.

open as a page

showing 1–30 of 59