E ExamMaster

CS Core & Software Engineering · Data Structures & Algorithms

Arrays and Strings

Contiguous array storage, string manipulation, prefix sums and in place techniques.

Eight concepts on contiguous arrays and strings — random access, shifting costs, amortized growth, prefix sums, Kadane, immutable concatenation, and in-place two-pointer work. Interview-heavy, and every cost claim here is one you can recompute on a tiny input.

  • CS Core & Software Engineering
  • Easy level
  • 8 concepts
  • 5 practice questions

1Contiguous storage and random access

An array stores its elements in one contiguous block of memory. The address of index i is base plus i times the element width, so a read or write at a known index is O(1) — constant work, independent of how long the array is. That is random access: you do not walk from the front.

The same layout is why an unsorted scan still costs O(n): knowing the address of every cell does not tell you which cell holds the value you want, so you may have to look at all of them. Contiguity buys index arithmetic, not free search.

Figure. Five equal cells in one row. Index i lands on the i-th cell by arithmetic; nothing walks the cells in between.

How an index becomes an address

  1. Base addressThe array occupies one unbroken block; the runtime knows where index 0 begins.
  2. Scale the indexMultiply i by the fixed byte width of one element — every cell is the same size.
  3. Add oncebase + i \times width is the cell. One multiply and one add is O(1), whether i is 3 or 3 million.
What contiguity buys — and what it does not
OperationCostWhy
Read / write a[i]O(1)Address = base + i × width
Scan for a value (unsorted)O(n)Every cell may hold it
Walk index 0, 1, 2, … in orderO(n) totaln cells, one O(1) step each
In a contiguous array of one million integers, reading a[500000] is
  1. O(1), because the address is base + 500000 × width
  2. O(n), because you must skip the first half of the array
  3. O(\log n), because the middle of a million-element array is found by halving

Index arithmetic jumps straight to the cell. Skipping from the front would be a linked-list walk; binary search is for a sorted lookup of a value, not for reading a known index.

2Insert and delete force a shift

Contiguous layout makes middle inserts and deletes expensive. To insert at index i in an n-element array you must move every element from i through n-1 one slot to the right first — otherwise you overwrite a live value. That shift touches n-i cells, so the cost is O(n). Deletion is the same walk in reverse: close the gap by sliding the tail left.

Appending at the end of a dynamic array that still has spare capacity is different — no live cell has to move, so that write is O(1) amortized once growth is accounted for. The O(n) bill is for an arbitrary index, not for every mutation.

Figure. Six live values A–F with a hole opened at index 2. The four-cell tail C, D, E, F must each move one slot right before the new value can land — that slide is the O(n) cost (n-i = 4 writes).

Insert at index i

  1. Need a holeCell i is already occupied. Nothing can land there until the tail moves.
  2. Shift the tailCopy a[n−1] → a[n], then a[n−2] → a[n−1], …, down to a[i] → a[i+1] — n-i writes.
  3. Write the new valuea[i] is free; store the insert. One write after the shift.

How many cells move?

An array holds six values in cells 0…5. You insert a new value at index 2. How many existing elements must shift, and what is the asymptotic cost in n?

  • Tail length = n − i = 6 − 24 cells
  • Each of those cells is written once during the shift4 writes
  • For general n and fixed i, writes scale as n − iO(n)

Pro tip. Inserting at index 0 shifts the whole array; inserting at index n (append, capacity free) shifts nothing. Same API, opposite bills.

Deleting a[0] from an n-element contiguous array costs
  1. O(1), because only one cell is cleared
  2. O(n), because the remaining n−1 cells must slide left to close the gap
  3. O(\log n), because the hole is closed by a binary search of free slots

Clearing a[0] leaves a hole. Contiguous layout demands a packed block, so every later element moves one slot left — n−1 writes.

3Dynamic arrays and amortized append

A dynamic array (Python list, Java ArrayList, C++ vector) keeps a capacity larger than its size. Appends into spare capacity are a single write. When size hits capacity the structure allocates a new block — typically twice as large — copies every live element across, and only then appends. That resize is O(n) when it happens.

Across n appends starting from empty, the doubling copies total 1+2+4+\cdots+n = O(n) element moves. Spread over n appends that is O(1) amortized per append: a few appends pay a large copy, most pay almost nothing, and the average stays constant.

Figure. Resize copy counts for eight appends with doubling. The three growth steps sum to 7 — less than one full extra pass over the final array.

What doubling buys

  1. Spare slotIf size < capacity, write at a[size] and bump size — one O(1) step.
  2. GrowIf full, allocate ~2× capacity and copy the old block over — O(\text{capacity}) once.
  3. AmortizeGeometric copies sum to O(n) over n appends, so the average per append is O(1).

Eight appends from capacity 1

Start with capacity 1 and size 0. Append eight elements, doubling capacity whenever the array is full. How many element copies does resizing perform in total, and what is that cost per append?

  • Resizes copy 1, then 2, then 4 live cells (before appends 2, 3 and 5)1 + 2 + 4 = 7 copies
  • Appends performed8
  • Copies per append = 7 / 80.875 → O(1) amortized

Pro tip. The worst single append is still O(n) — amortized is an average, not a guarantee for every call. Real-time code that cannot hitch still needs a reserved capacity.

n appends into a doubling dynamic array cost, in total,
  1. O(n), so each append is O(1) amortized
  2. O(n^2), because every append copies the whole array
  3. O(1), because capacity is infinite

Only the doubling steps copy, and those sizes form a geometric series summing to O(n). Copying on every append would be the naive fixed-growth or always-realloc-by-one strategy.

4Prefix sums for range queries

When you will answer many queries of the form "sum of a[i..j]" on a static array, precompute a prefix array P with P[0]=0 and P[k]=P[k-1]+a[k-1]. Building P is one O(n) pass. Each range then collapses to a subtraction: \text{sum}(i,j)=P[j+1]-P[i], which is O(1).

That beats rescanning the slice in O(j-i+1) per query whenever queries outnumber updates. If the array keeps changing, a plain prefix array goes stale — then you need a different structure. For the interview pattern "many range sums, array frozen," prefixes are the default move.

Figure. Each prefix node adds one array value. The bracketed query is the difference of two prefix nodes — no rescan of the slice.

Build, then query

  1. SeedSet P[0]=0 so every range formula stays uniform, including ranges that start at 0.
  2. AccumulateFor k=1..n, set P[k]=P[k-1]+a[k-1]. Each cell is the sum of a strict prefix.
  3. Subtractsum(i,j) = P[j+1] − P[i] drops the head before i and keeps through j.

One range after a prefix build

Given a = [2, 3, −1, 4], build P and answer sum(1, 3) — the sum of indices 1 through 3.

  • P[0]=0; then +2, +3, −1, +4P = [0, 2, 5, 4, 8]
  • sum(1,3) = P[3+1] − P[1] = P[4] − P[1]8 − 2 = 6
  • Check: a[1]+a[2]+a[3] = 3+(−1)+46

Pro tip. Prefix sums turn repeated O(n) range scans into O(1) lookups after one O(n) build — essential when queries vastly outnumber updates.

With prefix P built as above, sum(i,j) equals
  1. P[j+1]-P[i]
  2. P[j]-P[i]
  3. P[j+1]+P[i]

P[j+1] is the sum through index j; P[i] is the sum through index i−1. Subtract to keep a[i..j]. Off-by-one on the right endpoint is the usual bug.

5Kadane's maximum subarray

The maximum-sum contiguous subarray can be found in one O(n) pass. At each index track current: the best sum of a subarray that ends here. The recurrence is current = max(a[i], current + a[i]) — either start fresh at i, or extend the streak that ended at i−1. A parallel best remembers the largest current seen. Space is O(1) beyond the input.

The trap is restarting: when current + a[i] loses to a[i] alone, the old prefix is abandoned. That is not "skipping" an element inside a kept subarray — the subarray must stay contiguous. Checking all O(n^2) endpoint pairs works but is the wrong first move in an interview.

Figure. Winning window [4, −1, 2, 1]. Cells outside the bracket are contiguous neighbours that Kadane considered and rejected by reset or by a lower best.

One pass

  1. SeedSet current = best = a[0]. The answer is at least the first element when the array is non-empty.
  2. Extend or resetAt each next value, current = max(value, current + value).
  3. Recordbest = max(best, current) after every update. Return best at the end.

Kadane, one pass

def max_subarray(nums):
    current = best = nums[0]
    for value in nums[1:]:
        current = max(value, current + value)
        best = max(best, current)
    return best

Maximum subarray (Kadane)

Given nums = [−2, 1, −3, 4, −1, 2, 1, −5, 4], find the contiguous subarray with the largest sum.

  • After index 1 (value 1): current = max(1, −2+1)current = 1, best = 1
  • After index 3 (value 4): reset — current = max(4, −2+4)current = 4, best = 4
  • Extend through [4, −1, 2, 1]: 4 → 3 → 5 → 6current = 6, best = 6
  • Remaining values never beat 6answer 6 on subarray [4, −1, 2, 1]

Pro tip. Kadane runs in O(n) time and O(1) space; resetting current to the element when the running sum goes cold is the key insight — do not reach for all O(n^2) subarrays first.

Coding lab. Kadane on the nine-number row runs in the app, with checks on your output.

Kadane's recurrence current = max(a[i], current + a[i]) enforces that the subarray is
  1. Contiguous — a reset starts a new block ending at i, never a hole
  2. A subsequence — negative elements in the middle may be skipped
  3. Sorted — the algorithm only works if a is non-decreasing

Extending adds a[i] onto a block that already ended at i−1; resetting starts at i. Either way the kept indices are consecutive. Skipping a negative in the middle would be a subsequence problem with a different algorithm.

6Immutable strings and concatenation cost

In languages like Java and Python, strings are immutable: any concatenation builds a new string rather than editing the old one in place. A loop that does result = result + next_chunk therefore copies the whole result on every iteration. If the final length is n and you grow by one character at a time, the copies cost 1+2+\cdots+n = O(n^2).

The fix is to accumulate chunks in a mutable buffer — a list in Python, a StringBuilder in Java — and join once at the end. That keeps total character movement O(n). Immutability is a safety property; the quadratic bill is what you pay for ignoring it in a hot loop.

Figure. Immutable strings copy on each append — 1+2+\cdots+n becomes quadratic; use a builder or join.

Why the loop is quadratic

  1. Step kresult already holds k−1 characters; appending one more allocates a fresh string of length k and copies all k characters into it.
  2. Sum the copiesLengths 1 through n sum to n(n+1)/2 character writes — O(n^2).
  3. Buffer oncePush chunks into a list/builder, then join: each character is written a constant number of times, O(n) total.

Five single-character appends

Start from the empty string and build "abcde" by five rounds of result = result + next_char. How many character copies does that perform, versus appending into a list and joining once?

  • Copies per concat: lengths 1 + 2 + 3 + 4 + 515 character writes
  • Closed form n(n+1)/2 with n=515
  • List append five chars, then one join of length 55 writes → O(n)

Pro tip. Avoid building strings by repeated concatenation in a loop; accumulate in a list or StringBuilder and join once.

Building an n-character string by repeated result = result + c in an immutable-string language is
  1. O(n^2) character copies
  2. O(n) character copies, same as a builder
  3. O(1) per append with no hidden copies

Each append rewrites the entire prefix. The triangular sum 1+\cdots+n is quadratic. A builder avoids that by mutating a buffer and joining once.

7In-place two-pointer reverse

Two indices walking toward each other let you reverse or partition an array using O(1) extra memory instead of allocating a second array. For reverse: left starts at 0, right at n−1; swap a[left] with a[right], then step inward until they meet. Each element moves at most once, so time is O(n) and the auxiliary space is two integers.

The same left/right skeleton partitions around a pivot (swap misfits inward) and rotates via reversals. Prefer it when the prompt constrains extra memory — copying into a new array is correct but spends O(n) space the in-place walk does not need.

Figure. First swap exchanges the ends. The next swap will meet at the middle pair; no second array appears.

Reverse in place

  1. Place the fingersleft = 0, right = n−1.
  2. SwapExchange a[left] and a[right].
  3. Step inleft += 1, right −= 1; repeat while left < right.

Reverse [1, 2, 3, 4]

Reverse a = [1, 2, 3, 4] in place with two pointers. Show the array after each swap.

  • Start left=0, right=3 on [1, 2, 3, 4][1, 2, 3, 4]
  • Swap a[0]↔a[3]; step to left=1, right=2[4, 2, 3, 1]
  • Swap a[1]↔a[2]; left=2, right=1 → stop[4, 3, 2, 1]

Pro tip. Reverse or rotate arrays in place with two pointers to avoid O(n) extra memory — the output overwrites the input.

Reversing an n-element array with two pointers uses extra space
  1. O(1) — only the two indices (and a temporary for the swap)
  2. O(n) — a second array is required for a correct reverse
  3. O(\log n) — the pointers binary-search the midpoint

Swaps happen inside the original block. A second array also works but spends linear auxiliary memory the two-pointer walk does not need.

8Which array technique fits

Interview prompts on arrays and strings usually collapse to one of four moves. Many static range-sum queries → prefix sums. Maximum contiguous sum → Kadane. Reverse, rotate, or partition under a memory cap → two pointers in place. Building a string in a loop → buffer then join, never repeated concatenation.

These are not interchangeable. Prefix sums do not find a maximum subarray; Kadane does not answer arbitrary later range queries without rescanning; two pointers reverse does not make range sums O(1). Pick by the question asked, not by the structure you saw last.

Read the prompt shape in the Prompt → first move table — many frozen range sums → Prefix sums; max contiguous sum → Kadane; in-place reverse/partition → two pointers; string building → buffer then join.

Prompt → first move
You are asked for…Reach for…Cost to remember
Many sum(i,j) on a frozen arrayPrefix sumsBuild O(n), query O(1)
Maximum contiguous subarray sumKadaneO(n) time, O(1) space
Reverse / partition with tight extra memoryTwo pointers in placeO(n) time, O(1) extra space
Build a string by repeated appendsList / StringBuilder, join onceO(n) instead of O(n^2)
"Return the largest sum of any contiguous slice" on an unsorted array of ints. First move?
  1. Kadane in one O(n) pass
  2. Build a prefix array and answer sum(0,n−1)
  3. Two-pointer reverse, then read a[0]

Maximum contiguous sum is Kadane's problem. A single full-array prefix total is one range, not the maximum over all ranges. Reverse changes order, not the maximum subarray sum.

Notes

  • Array Access: Elements sit in contiguous memory, so random access by index is O(1), but inserting/deleting in the middle is O(n) due to shifting.
  • Prefix Sums: Precomputing a running-sum array lets any range sum be answered in O(1) after O(n) preprocessing.
  • Kadane's Algorithm: Finds the maximum-sum contiguous subarray in a single O(n) pass by tracking the best sum ending at each index.
  • String Immutability: In languages like Java/Python, strings are immutable, so naive concatenation in a loop is O(n^2); use a builder/list to make it O(n).
  • In-Place Techniques: Two-pointer swaps let you reverse or partition an array using O(1) extra space instead of an auxiliary array.

Formulas

  • Random access time: O(1); search in an unsorted array: O(n).
  • Insertion/deletion at arbitrary index: O(n); at the end of a dynamic array (amortized): O(1).
  • Prefix-sum range query: build O(n), query O(1), where \text{sum}(i,j)=P[j+1]-P[i].
  • Kadane's max subarray: time O(n), space O(1).
  • Dynamic array resize: amortized append O(1) via doubling, giving total O(n) for n appends.

Exam traps & shortcuts

  • When asked for a range-sum or subarray-sum many times, precompute prefix sums to answer each query in O(1).
  • For 'maximum subarray sum,' immediately reach for Kadane's algorithm rather than checking all O(n^2) subarrays.
  • Reverse or rotate arrays in place with two pointers to avoid O(n) extra memory.
  • Avoid building strings by repeated concatenation in a loop; accumulate in a list/StringBuilder and join once.

Reference tables

Every line here is restated from a concept above — use it as a scan sheet, not as a second argument.

Complexity cheatsheet
Operation / techniqueTimeExtra space
Random access a[i]O(1)
Insert / delete at arbitrary indexO(n)O(1) if shifting in place
n appends with doublingO(n) total (O(1) amortized each)O(n) capacity
Prefix build + range sum queryO(n) build, O(1) queryO(n) for P
Kadane max subarrayO(n)O(1)
Immutable concat in a loopO(n^2)O(n) for the strings
In-place two-pointer reverseO(n)O(1)

The identities the ledgers keep using.

Formula anchors
IdentityForm
Prefix range sum\mathrm{sum}(i,j)=P[j+1]-P[i] with P[0]=0
Kadane step\mathrm{current}=\max(a[i],\,\mathrm{current}+a[i])
Doubling copy total1+2+\cdots+n=O(n) over n appends
Immutable concat copies1+2+\cdots+n=n(n+1)/2

Recap

Read only this the night before an arrays round.

Access
Contiguous ⇒ a[i] is O(1). Unsorted search is still O(n).
Mutate
Middle insert/delete shifts the tail — O(n). End append with spare capacity is O(1).
Grow
Doubling makes n appends O(n) total, O(1) amortized — individual resizes still hitch.
Prefix
Many frozen range sums: build P in O(n), answer with P[j+1]-P[i].
Kadane
Max contiguous sum in one pass; reset when a fresh start beats extending. Contiguous, not subsequence.
Strings
Immutable + concat-in-a-loop = O(n^2). Buffer, then join once.
In place
Two pointers reverse or partition in O(1) extra memory.

Practise Arrays and Strings

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.