ASAPUtils Logo ASAPUtils
Data structure · Week 1

Arrays

What an array actually is in memory, why appending is amortised O(1) but inserting is O(n), and the handful of signals that tell you the answer is a single pass rather than a nested loop.

Watch it run

Step through it. Then hide the page and predict the next frame before pressing →. Predicting is the part that builds the skill; watching alone does not.

1. The physical picture

An array is one contiguous block of memory. Element i lives at base + i × elementSize, which is a single multiply-and-add — that’s the entire reason indexing is O(1).

A dynamic array (vector<int>, a JS array) is that block plus a capacity. When you append past the capacity, it allocates a bigger block (usually double), copies everything over, and frees the old one. That copy is O(n), but it happens so rarely that the average append is O(1). That’s amortised cost, and it’s why push_back is considered free.

2. The invariant

Elements occupy positions 0 .. size-1 with no gaps, in the order you put them.

Everything expensive about arrays follows from maintaining “no gaps”: insert or delete in the middle and everything after it has to shift.

3. Operations and complexity

OperationCostWhy
Read/write a[i]O(1)Pointer arithmetic
Append at the endO(1) amortisedOccasional doubling, spread out
Insert/delete at the endO(1)Nothing shifts
Insert/delete in the middleO(n)Everything after shifts
Search (unsorted)O(n)Must look at each
Search (sorted)O(log n)Binary search
SortO(n log n)

4. C++ and JavaScript

vector<int> v;                       // dynamic array
v.push_back(5);                      // amortised O(1)
v.size(); v.empty(); v.back();
vector<vector<int>> g(R, vector<int>(C, 0));   // 2-D, all zeros
sort(v.begin(), v.end());
reverse(v.begin(), v.end());
const v = [];
v.push(5);                           // amortised O(1)
v.length;
const g = Array.from({ length: R }, () => Array(C).fill(0));
v.sort((a, b) => a - b);             // ALWAYS pass a comparator
v.reverse();

Two traps worth burning in: [10, 9, 1].sort() gives [1, 10, 9] in JavaScript because the default sort is lexicographic; and Array(R).fill(Array(C).fill(0)) gives you R references to the same row, so writing one cell writes them all.

5. How you travel it

  • Forward walk — for (i = 0; i < n; i++). The default.
  • Backward walk — when writing in place and you don’t want to clobber unread data.
  • Two ends inward — l = 0, r = n-1, the two-pointer pattern.
  • Two passes — left-to-right then right-to-left, which is how Product of Array Except Self avoids division.
  • Fast and slow — one index moves every step, the other only when a condition holds. This is the “write pointer” idea behind every in-place removal problem.

6. It’s the answer when…

  • You need order, or you need to index by position.
  • Keys are small dense integers — a int freq[26] frequency array beats a hash map for lowercase letters, on both speed and readability.
  • The problem says “in place” or “O(1) extra space”.
  • You’re about to sort, because sorting unlocks two pointers and binary search.

7. The three classic mistakes

  1. Shifting in a loop. Deleting k elements one at a time from the middle is O(n·k). Build the result with a write pointer instead, in one pass.
  2. Off-by-one at the boundaries. n-1 vs n, and < vs <=. Write the invariant (see loops and index arithmetic) instead of guessing.
  3. Assuming the input is sorted. Read the constraints. If it isn’t and you need order, sorting costs O(n log n) — and destroys the original indices, which matters when the answer is indices rather than values.

8. What to drill

Reverse in place, rotate by k, remove duplicates from a sorted array in place, move zeroes to the end, and merge two sorted arrays. All O(1) space, all with a written invariant. They take an hour and they de-risk the entire rest of the quarter.

Problems that drill this

Related Topics