Arrays and in-place manipulation
An array is a block of equal-sized slots side by side in memory, so a[i] is one multiplication away: O(1). Appending to a dynamic array is O(1) amortized; inserting or deleting in the middle is O(n) because everything after it shifts. In-place techniques (a read pointer and a write pointer, swaps, reversals) rearrange an array with O(1) extra space.
The idea in plain words
Section titled “The idea in plain words”Picture a row of numbered lockers, all the same width. To reach locker 700 you don’t walk past 699 others: you compute start + 700 × width and go there. That’s why indexing is constant time, and why scanning left to right is the fastest loop a CPU runs (each memory fetch brings the next few slots along for free).
The price is rigidity:
- Inserting in the middle means sliding every later locker’s contents one step to make room. O(n).
- Growing means the block may not have room to extend, so a dynamic array (Python
list, JSArray, C#List<T>) allocates a bigger block and copies everything across. Done right, that copy is rare enough to average out to O(1) per append.
In-place means you rearrange the data using the array itself as your workspace, with only a few index variables on the side. Interviewers love it because it forces you to keep a precise invariant about which part of the array means what.
See it
Section titled “See it”The most reusable in-place pattern: r reads every element, w marks where the next kept element goes. Everything left of w is finished output. Change the values or the number to remove.
When to reach for it
Section titled “When to reach for it”| The problem says… | Why this fits |
|---|---|
| “in place”, “O(1) extra space”, “modify the input” | Read/write pointers or swaps use the array as its own buffer. |
| “remove / filter / dedupe while keeping order” | Read/write compaction: one pass, stable. |
| “partition”, “move all X to the end”, “group into three kinds” | Swap-based partitioning (Lomuto, Hoare, Dutch flag). |
| “rotate / reverse / rearrange” | Reversals and cyclic swaps, O(1) space. |
index arithmetic: “position i maps to…”, values in 1..n |
The array can be its own hash table (mark visited by sign or by swapping into place). |
| random access by position, fixed size | A plain array beats any linked structure. |
How it works
Section titled “How it works”Read/write compaction
Section titled “Read/write compaction”- Set
w = 0. Invariant:a[0..w-1]is the output so far. - Move
racross every element. - If
a[r]should be kept, copy it toa[w]and advancew. Otherwise skip it. - Return
was the new length. In a fixed-size array the tail is garbage; in Python/JS you can truncate.
def remove_all(a: list[int], val: int) -> int: """Remove every `val` in place, keeping order. Returns the new length w; a[:w] is the result.""" w = 0 # 1. invariant: a[0:w] holds the kept values seen so far for r in range(len(a)): # 2. r reads every element exactly once if a[r] != val: a[w] = a[r] # 3. keep it: copy down into the first free slot w += 1 return w
def reverse_range(a: list[int], lo: int, hi: int) -> None: """Reverse a[lo..hi] (inclusive) in place with two pointers meeting in the middle.""" while lo < hi: a[lo], a[hi] = a[hi], a[lo] lo += 1 hi -= 1
if __name__ == "__main__": a = [3, 2, 2, 3, 4, 2, 5, 2] w = remove_all(a, 2) del a[w:] # 4. Python lists can shrink; in fixed arrays you just return w assert a == [3, 3, 4, 5] assert remove_all([], 1) == 0 b = [1, 2, 3, 4, 5] reverse_range(b, 1, 3) assert b == [1, 4, 3, 2, 5] print("ok")import assert from "node:assert/strict";
/** Remove every `val` in place, keeping order. Returns the new length w; a.slice(0, w) is the result. */export function removeAll(a: number[], val: number): number { let w = 0; // 1. invariant: a[0..w-1] holds the kept values seen so far for (let r = 0; r < a.length; r++) { // 2. r reads every element exactly once if (a[r] !== val) a[w++] = a[r]; // 3. keep it: copy down into the first free slot } return w;}
/** Reverse a[lo..hi] (inclusive) in place with two pointers meeting in the middle. */export function reverseRange(a: number[], lo: number, hi: number): void { for (; lo < hi; lo++, hi--) [a[lo], a[hi]] = [a[hi], a[lo]];}
const a = [3, 2, 2, 3, 4, 2, 5, 2];const w = removeAll(a, 2);a.length = w; // 4. JS arrays can be truncated by assigning lengthassert.deepEqual(a, [3, 3, 4, 5]);assert.equal(removeAll([], 1), 0);const b = [1, 2, 3, 4, 5];reverseRange(b, 1, 3);assert.deepEqual(b, [1, 4, 3, 2, 5]);console.log("ok");// Remove every `val` in place, keeping order. Returns the new length w; a[..w] is the result.static int RemoveAll(int[] a, int val){ int w = 0; // 1. invariant: a[0..w-1] holds the kept values seen so far for (int r = 0; r < a.Length; r++) // 2. r reads every element exactly once if (a[r] != val) a[w++] = a[r]; // 3. keep it: copy down into the first free slot return w;}
// Reverse a[lo..hi] (inclusive) in place with two pointers meeting in the middle.static void ReverseRange(int[] a, int lo, int hi){ for (; lo < hi; lo++, hi--) (a[lo], a[hi]) = (a[hi], a[lo]); // tuple swap}
int[] a = [3, 2, 2, 3, 4, 2, 5, 2];int w = RemoveAll(a, 2);int[] kept = a[..w]; // 4. arrays are fixed size: take a range (a copy) or a Spanif (!kept.SequenceEqual([3, 3, 4, 5])) throw new Exception("bad compact");if (RemoveAll([], 1) != 0) throw new Exception("expected 0");int[] b = [1, 2, 3, 4, 5];ReverseRange(b, 1, 3);if (!b.SequenceEqual([1, 4, 3, 2, 5])) throw new Exception("bad reverse");Console.WriteLine("ok");Three zones with swaps (Dutch national flag)
Section titled “Three zones with swaps (Dutch national flag)”Dijkstra’s problem: sort an array of three kinds of values in one pass. Keep three boundaries and an “unknown” zone that shrinks by one each step.
lo= end of the “less” zone,mid= next unknown,hi= start of the “greater” zone (from the right).- If
a[mid]is small, swap it into the left zone; advanceloandmid. - If it’s big, swap it with
a[hi]and shrinkhi. Don’t advancemid: the value you just pulled in hasn’t been looked at. - If it’s equal, just advance
mid.
def three_way_partition(a: list[int], pivot: int) -> None: """Rearrange in place into [< pivot | == pivot | > pivot] in one pass (Dijkstra's Dutch flag).""" lo, mid, hi = 0, 0, len(a) - 1 # 1. invariant: a[:lo] < pivot, a[lo:mid] == pivot, a[mid:hi+1] unknown, a[hi+1:] > pivot while mid <= hi: if a[mid] < pivot: a[lo], a[mid] = a[mid], a[lo] # 2. small: swap into the left zone, both advance lo += 1 mid += 1 elif a[mid] > pivot: a[mid], a[hi] = a[hi], a[mid] # 3. big: swap to the right; don't advance mid, hi -= 1 # the value that came back is still unknown else: mid += 1 # 4. equal: already in the middle zone
if __name__ == "__main__": a = [2, 0, 2, 1, 1, 0] three_way_partition(a, 1) assert a == [0, 0, 1, 1, 2, 2] b = [5, 1, 9, 5, 3, 7, 5] three_way_partition(b, 5) assert all(x < 5 for x in b[:2]) and b[2:5] == [5, 5, 5] and all(x > 5 for x in b[5:]) empty: list[int] = [] three_way_partition(empty, 0) print("ok")import assert from "node:assert/strict";
/** Rearrange in place into [< pivot | == pivot | > pivot] in one pass (Dijkstra's Dutch flag). */export function threeWayPartition(a: number[], pivot: number): void { let lo = 0, mid = 0, hi = a.length - 1; // 1. invariant: a[0..lo-1] < pivot, a[lo..mid-1] == pivot, a[mid..hi] unknown, a[hi+1..] > pivot while (mid <= hi) { if (a[mid] < pivot) { [a[lo], a[mid]] = [a[mid], a[lo]]; // 2. small: swap into the left zone, both advance lo++; mid++; } else if (a[mid] > pivot) { [a[mid], a[hi]] = [a[hi], a[mid]]; // 3. big: swap right; mid stays, the incoming value is unknown hi--; } else { mid++; // 4. equal: already in the middle zone } }}
const a = [2, 0, 2, 1, 1, 0];threeWayPartition(a, 1);assert.deepEqual(a, [0, 0, 1, 1, 2, 2]);const b = [5, 1, 9, 5, 3, 7, 5];threeWayPartition(b, 5);assert.ok(b.slice(0, 2).every((x) => x < 5) && b.slice(5).every((x) => x > 5));assert.deepEqual(b.slice(2, 5), [5, 5, 5]);threeWayPartition([], 0);console.log("ok");// Rearrange in place into [< pivot | == pivot | > pivot] in one pass (Dijkstra's Dutch flag).static void ThreeWayPartition(int[] a, int pivot){ int lo = 0, mid = 0, hi = a.Length - 1; // 1. invariant: a[..lo] < pivot, a[lo..mid] == pivot, a[mid..hi+1] unknown, a[(hi+1)..] > pivot while (mid <= hi) { if (a[mid] < pivot) { (a[lo], a[mid]) = (a[mid], a[lo]); // 2. small: swap into the left zone, both advance lo++; mid++; } else if (a[mid] > pivot) { (a[mid], a[hi]) = (a[hi], a[mid]); // 3. big: swap right; mid stays, incoming value is unknown hi--; } else mid++; // 4. equal: already in the middle zone }}
int[] a = [2, 0, 2, 1, 1, 0];ThreeWayPartition(a, 1);if (!a.SequenceEqual([0, 0, 1, 1, 2, 2])) throw new Exception("bad partition");int[] b = [5, 1, 9, 5, 3, 7, 5];ThreeWayPartition(b, 5);if (!(b[..2].All(x => x < 5) && b[2..5].All(x => x == 5) && b[5..].All(x => x > 5))) throw new Exception("bad zones");ThreeWayPartition([], 0);Console.WriteLine("ok");Arrays in each language
Section titled “Arrays in each language”| Python | TypeScript / JS | C# | |
|---|---|---|---|
| dynamic array | list |
Array (T[]) |
List<T> |
| fixed array | array.array, NumPy |
typed arrays (Int32Array), fixed length |
T[] (fixed length) |
| growth when full | ≈ ×1.125 plus a constant | V8: ≈ ×1.5 plus 16 | ×2 (starts at 4) |
slice a[i:j] |
copies | slice copies |
a[i..j] on arrays copies; AsSpan(i, len) is a view |
| insert/remove at front | insert(0,x) / pop(0): O(n) |
unshift / shift: O(n) |
Insert(0,x) / RemoveAt(0): O(n) |
| swap | a[i], a[j] = a[j], a[i] |
[a[i], a[j]] = [a[j], a[i]] |
(a[i], a[j]) = (a[j], a[i]) |
What it costs
Section titled “What it costs”| Operation | Cost | Why |
|---|---|---|
a[i] read / write |
O(1) | Address arithmetic. |
| append (dynamic) | O(1) amortized, O(n) worst | Occasional resize copies everything. |
| insert / delete at index i | O(n − i) | Shift the tail. |
| search unsorted | O(n) | No structure to exploit. Sorted → binary search, O(log n). |
| slice / copy of k items | O(k) | A new block plus a copy, in all three languages. |
Why append is O(1) amortized: with doubling, the copies over n appends are 1 + 2 + 4 + … + n/2 < n. Add the n writes and it’s under 3n total, so under 3 per append. A constant increment (+4 slots) instead gives n²/8 copies: quadratic. Try both:
Variations you’ll meet
Section titled “Variations you’ll meet”- Two pointers from both ends. Reverse, palindromic checks, pair sums on sorted data. See two pointers.
- Array as a hash table. When values are in
1..n, put valuevat indexv-1(cyclic sort), or flipa[v-1]negative to mark “seen”. O(1) space, mutates input: say so. - Prefix sums / difference arrays. Precompute running totals for O(1) range queries. See prefix sums.
- 2-D arrays. Rows of arrays, or one flat array with
r * cols + c. See matrices. - Circular indexing.
(i + k) % nturns a fixed array into a ring. See queues. - Copy vs view. NumPy slices and C#
Span<T>are views (no copy); Python list slices and JSsliceare copies.
Pitfalls
Section titled “Pitfalls”- Modifying a list while iterating it with
for x in a: a.remove(x)skips elements. Use a write pointer or build a new list. - Off-by-one on boundaries. Decide inclusive vs exclusive (
[lo, hi]vs[lo, hi)) and keep it everywhere. - Hidden O(n) calls inside loops:
list.pop(0),arr.shift(),list.insert(0, x),x in list,indexOf, slicing. A loop of these is O(n²). - Aliasing.
b = acopies the reference, not the data. Python[[0] * m] * ncreates n references to the same row. - Mutating the caller’s input when the spec didn’t allow it. Ask; copy if unsure.
- Integer overflow in C# (
intsums,(lo + hi) / 2). Python ints don’t overflow; JS numbers lose precision past 2^53.
In the interview
Section titled “In the interview”- Ask: “Can I modify the input?” and “Do I need to preserve order?” Those two answers pick the technique.
- State the invariant for each pointer: “Everything before
wis kept output; everything betweenwandris garbage I can overwrite.” - Count your passes and extra space precisely: “one pass, O(1) extra” beats “linear”.
- Follow-ups: “now do it stably”, “now without extra space”, “what if it doesn’t fit in memory?” (process in chunks, external sort).
Check yourself
Section titled “Check yourself”Practice on your own
Section titled “Practice on your own”Classic textbook exercises, not gym questions:
- Remove duplicates from a sorted array in place; then allow each value at most twice.
- Move all zeros to the end, keeping the order of the other values.
- Find the one missing number in an array of
0..n(sum, XOR, or cyclic placement). - Merge two sorted arrays where the first has enough spare room at the end (fill from the back).
Go deeper
Section titled “Go deeper”- Python docs, Time complexity of list operations and CPython’s
listobject.cgrowth comment. - Microsoft Learn,
List<T>remarks on capacity, andSpan<T>. - Bentley, Programming Pearls, column 2 (“Aha! Algorithms”): in-place rotation and the reversal trick.
- Dijkstra, A Discipline of Programming, ch. 14: the Dutch national flag problem and invariant-driven loops.