2. Arrays & Dynamic Arrays

How each language allocates, grows, copies, and mutates arrays/lists — fixed vs. dynamic sizing, aliasing traps, and sort-mutation gotchas.

Arrays are the substrate of almost every DSA interview, but the tool each language hands you is subtly different: Java gives you a genuinely fixed-size int[] and a dynamic ArrayList; Go splits the same idea into arrays [n]T (values, fixed size) and slices []T (dynamic headers over a backing array); Python's list and JS's Array are always dynamic and store references rather than raw values. This section covers the mechanics of allocating, growing, copying, and mutating these structures correctly and efficiently — not the algorithms you run over them.

See roadmap: Arrays & Hashing

Language Verdict: Pros, Cons & Recommendation

Python

4/5
  • list slicing, comprehensions, and sorted() are the most concise way to build and copy arrays
  • sorted() defaults to correct numeric/lexical ordering — no default-comparator surprise like JS
  • List comprehensions naturally sidestep the row-aliasing bug ([[0]*m for _ in range(n)])
  • array module available when you need a packed, fixed-type buffer
  • No true fixed-size array — list is always dynamic and stores pointers, not inline values
  • [[0]*m] * n silently aliases every row to the same list — a classic, easy-to-miss DP-grid bug
  • Growth factor and resize threshold are CPython implementation details, not spec-guaranteed

Java

5/5
  • int[] is a real fixed-size, contiguous, unboxed array — fastest option, no aliasing risk
  • new int[n][m] allocates independent rows automatically — immune to the multi-dim aliasing bug entirely
  • ArrayList's 1.5x growth factor is documented; pre-sizing with new ArrayList<>(n) skips resize copies
  • Arrays/Collections utility classes cover copy, sort, and range operations in one call each
  • ArrayList<Integer> boxes every element, adding real pointer-chasing overhead vs. int[]
  • list.remove(1) (by index) vs. list.remove(Integer.valueOf(1)) (by value) overload ambiguity is a genuine footgun
  • No built-in enumerate — indexed iteration means manually tracking i in a classic for loop

Go

5/5
  • Slices store unboxed int/byte contiguously — Java-int[] locality with Python-list growth via append
  • make([]T, n) / make([]T, n, cap) pre-size in one call; copy/append/slices.Clone cover the usual copies
  • Arrays [n]T are real values (assignment copies) when a fixed size is part of the type
  • for i, v := range s is a built-in enumerate with no extra import
  • Slicing shares the backing array — b := a[1:3]; b[0] = 99 mutates a, the classic aliasing bug Python/JS slice-copy avoid
  • append may or may not reallocate; if cap has room, later writes alias the original — a silent cousin of the slice-share bug
  • No negative indexes or pop(i) helper — mid-slice delete is a manual append(s[:i], s[i+1:]...)

JavaScript

3/5
  • TypedArrays (Int32Array, etc.) give real fixed-size, contiguous, unboxed storage when needed
  • Array.from({length}, fn) and spread give clean, aliasing-safe ways to build 2D grids
  • Richest set of indexed-iteration options (for, .forEach, .entries())
  • .slice()/.splice() cover copy and mid-array insert/remove in one call each
  • Array.prototype.sort() defaults to lexicographic ordering — [10, 2, 1].sort() gives [1, 10, 2]
  • push's growth strategy isn't specified by ECMA-262 at all — no guaranteed cost model or pre-sizing
  • Array(n).fill(obj) aliases every slot to the same object reference — the same DP-grid trap as Python
  • .sort() mutates in place *and* returns the same reference, easy to mistake for a non-mutating copy
Recommendation: Python remains the default for speed of writing. Go slices are the nicest array tool of the four when you want unboxed ints without Java's int[] vs ArrayList split — but slicing aliases the backing array, so copy (append([]T(nil), s...) / slices.Clone) when you need independence. In Python and JS, always build 2D grids with a fresh-row-per-iteration idiom, and never trust JS's default .sort() without an explicit comparator.

Coding Mechanics, Side by Side

Fixed-size arrays vs. dynamic arrays

Must-know

Java and Go both have a real fixed-size array type (int[], [n]T); Python and JS are dynamic-only, with narrower typed alternatives available. Go's everyday tool is still the slice ([]T), not the array.

# No true fixed-size array type for general use. nums = [1, 2, 3] # list: always dynamic, heterogeneous-capable nums.append(4) # grows automatically # array module: fixed-TYPE (not fixed-size), packed C-style buffer from array import array typed = array('i', [1, 2, 3]) # 'i' = C int; still resizable via append

Python has no fixed-size array primitive — list is always dynamic and stores object references, not raw values. The array module gives you a fixed-type, packed buffer (closer to a C array), but it's still dynamically resizable — it constrains element type, not length. For interviews, list is what you reach for essentially every time.

Growth factor & amortized append cost

Recommended

All four give amortized O(1) append, but the real-world memory overhead and growth strategy differ, and only Java's ArrayList growth factor is specified in source as a stable 1.5x.

import sys lst = [] prev = 0 for i in range(10): lst.append(i) size = sys.getsizeof(lst) if size != prev: print(len(lst), size) # capacity jumps are visible here prev = size

CPython over-allocates on resize, roughly newsize + (newsize >> 3) + (3 if newsize < 9 else 6) — not a clean multiplier, but similar in spirit to ~1.125x growth. This is a CPython implementation detail (not part of the language spec), and it only triggers when the backing buffer is actually full. append is amortized O(1) either way.

Contiguous memory & cache locality

Recommended

Array traversal beats linked-list traversal in all four runtimes despite identical Big-O. Java's int[], Go's []int, and JS's TypedArray give you true contiguous data; Python lists (and boxed Java/JS arrays) are contiguous arrays of pointers.

# A Python list is an array of POINTERS to PyObject, not inline values. lst = [1, 2, 3, 4, 5] # lst's backing buffer holds 5 pointers (8 bytes each on 64-bit); # the actual int objects live scattered on the heap.

Traversing a Python list is still faster than a linked list — the pointer array itself is contiguous, so you get decent locality on the pointers — but every element access dereferences to a separately heap-allocated PyObject (boxed int, string, etc.) that can be anywhere in memory. So a Python list is 'contiguous but indirect': strictly better than a linked list, but it does not get the cache-line benefit of a true primitive array.

The multi-dimensional array aliasing bug

Must-know

One of the most common interview bugs: initializing a 2D grid by replicating a row reference instead of creating fresh rows.

# BUG: all rows are the SAME list object grid = [[0] * 3] * 2 grid[0][0] = 1 print(grid) # [[1, 0, 0], [1, 0, 0]] <- both rows changed! # FIX: a fresh inner list per row grid = [[0] * 3 for _ in range(2)] grid[0][0] = 1 print(grid) # [[1, 0, 0], [0, 0, 0]]

[[0]*m] * n evaluates [0]*m once and replicates the reference n times, so every row is literally the same object — mutating one row mutates all of them. This is one of the most common bugs when initializing a grid for DP or matrix problems. The list-comprehension form calls [0]*m fresh on every iteration, producing n distinct lists.

Slicing & copy semantics

Recommended
arr = [1, 2, 3, 4, 5] sub = arr[1:3] # new list [2, 3], O(k) where k = slice length sub[0] = 99 print(arr) # [1, 2, 3, 4, 5] unchanged -- sub is a new list

Slicing always allocates a new list and copies the references (a shallow copy) in O(k) time/space for a slice of length k. If the elements themselves are mutable objects, mutating an element (not the container) still affects both lists, since only the container was copied.

Sorting in place vs. returning a new array

Must-know

Every language has at least one sort that mutates in place — know which is which, and whether it returns a new collection or the same one, before your interview.

arr = [3, 1, 2] result = arr.sort() # BUG: sorts in place, returns None print(result) # None print(arr) # [1, 2, 3] -- arr itself was mutated new_arr = sorted(arr) # correct: returns a new sorted list

list.sort() mutates in place and returns None — assigning its return value (x = arr.sort()) is a classic bug that silently produces None instead of the sorted list. sorted(iterable) always returns a new list and leaves the original untouched, and it also works on any iterable, not just lists.

Iterating with index access

Optional
for i, val in enumerate(arr): print(i, val) for i, val in enumerate(arr, start=1): # custom start offset print(i, val)

enumerate() is the idiomatic way to get index and value together — cleaner than manual range(len(arr)) indexing, and the start kwarg is handy for 1-indexed output. It returns a lazy iterator, not a list, so it doesn't allocate an extra array.

Removing/inserting mid-array: real cost

Recommended
arr = [1, 2, 3, 4, 5] arr.pop(1) # O(n): shifts every element after index 1 left arr.insert(1, 99) # O(n): shifts every element after index 1 right arr.pop() # O(1): removing from the end is cheap

pop(i) and insert(i, x) are O(n) because CPython physically shifts the underlying buffer (a memmove). pop() with no argument (removes the last element) is O(1) — prefer removing/appending at the end whenever ordering doesn't force you to touch the middle or front.

Further Reading