Compare the performance characteristics of common operations (random access, append, insert/remove in the middle, contains) for arrays and ArrayList. What does ArrayList's add() being 'amortized O(1)' mean?
answer
- get/set by index: O(1) both (array-backed)
- ArrayList add() at end: amortized O(1); resize copies at ~1.5x
- Middle insert/remove: O(n) shift via System.arraycopy
- contains/indexOf: O(n) linear scan -> use HashSet
- Pre-size the ArrayList to avoid resize copies
basics
~20 sBoth give O(1) access by index. Appending to an ArrayList is usually O(1) but occasionally has to grow and copy everything, which is slow; averaged out it's still cheap ('amortized O(1)'). Inserting or removing in the middle is O(n) for both because elements must shift. Searching by value (contains) is O(n).
solid answer
~50 sIndexed access (get/set) is O(1) for both an array and an ArrayList, since ArrayList is array-backed. Appending to an array isn't really an operation (fixed size); for ArrayList, add() at the end is amortized O(1): most adds just write to the next slot, but when the backing array is full it allocates a ~1.5x larger array and copies all elements (O(n)). Because that costly resize happens rarely and its cost is spread across many cheap adds, the average per-add cost stays constant — that's amortized O(1). Inserting or removing in the middle is O(n) for both: subsequent elements must shift via System.arraycopy. Searching by value (contains/indexOf) is O(n) linear scan. So for index-heavy, append-heavy workloads ArrayList is excellent; for frequent middle inserts/removes consider LinkedList or a different structure, and for membership tests use a HashSet.
go deeper
Knows get by index is fast and inserting in the middle is slow; can use add() and get() correctly even without the Big-O vocabulary.
States the Big-O for each operation and can explain amortized O(1) appends and the O(n) middle shift, recommending HashSet for membership.
Reasons about the resize growth factor, pre-sizing, System.arraycopy, cache locality, and picks the right structure (ArrayDeque/HashSet) per access pattern; insists on measuring.
Connects data-structure choice to system-level concerns (GC, memory bandwidth, latency tails from resize copies), and sets guidelines/benchmarks so teams choose collections by access pattern rather than folklore.
## Big-O in one sentence **Big-O notation** describes how an operation's cost grows with the number of elements n. **O(1)** = constant time (independent of n); **O(n)** = linear (proportional to n). We use it to compare operations without measuring exact nanoseconds. Because an `ArrayList` is **backed by an array**, most of its costs mirror an array's. Let's go operation by operation. ## Random access by index — O(1) for both Reading or writing `arr[i]` or `list.get(i)` / `list.set(i, v)` is **constant time**. An array is a contiguous block, so the address of element i is just `base + i * elementSize` — one arithmetic step, no scanning. ArrayList just forwards to its internal array. This is arrays' superpower and why they (and ArrayList) crush a LinkedList, where `get(i)` must walk i nodes (O(n)). ## Appending at the end A raw array has **no append** — its size is fixed. To "grow" it you manually allocate a bigger array and copy (O(n)). ArrayList's `add(e)` (append at end) is **amortized O(1)** — explained below. Most of the time it's a single write to the next free slot. Occasionally it must resize. ### What 'amortized O(1)' means When ArrayList's internal array is **full**, the next `add()` must: 1. allocate a new array about **1.5x** the current capacity, 2. **copy** all existing elements into it (O(n)), 3. then store the new element. That one add is expensive (O(n)). But resizing happens **rarely** — only when capacity is exhausted — and each resize roughly doubles headroom, so the number of resizes grows only logarithmically. If you spread (amortize) the total cost of all those occasional O(n) copies across the many cheap O(1) adds, the **average cost per add is still constant**. That averaged guarantee is what "**amortized O(1)**" means: any single add *might* be O(n), but a long run of adds costs O(1) each on average. (Pre-sizing with `new ArrayList<>(expectedSize)` avoids the resizes entirely.) ## Insert or remove in the middle — O(n) for both To insert at index i (or remove it), every element after i must **shift** one slot over to keep the block contiguous. That shift is done with `System.arraycopy` and touches up to n elements, so it's **O(n)**. This is true for a raw array too (you'd shift manually). Removing the **last** element is O(1) (no shift); removing the **first** is the worst case (shift everything). If your workload is dominated by middle inserts/removes, an `ArrayList` is a poor fit — a `LinkedList` does O(1) insert/remove *once you have the node*, though finding the position is still O(n), and an `ArrayDeque` is better for head/tail churn. ## Search by value (contains / indexOf) — O(n) Neither structure indexes by value, so `contains(x)` / `indexOf(x)` scans linearly until it finds a match — **O(n)**. If you frequently test membership, use a **HashSet** (O(1) average contains) instead of a List/array. ## Summary table | Operation | Array | ArrayList | |---|---|---| | get/set by index | O(1) | O(1) | | append at end | n/a (fixed) | amortized O(1) | | insert/remove middle | O(n) | O(n) | | remove last | O(1) | O(1) | | contains / indexOf | O(n) | O(n) | ## Practical guidance - Index- and append-heavy, iterate-a-lot workloads → **ArrayList** (or array for fixed primitives) is ideal; pre-size it when you know the count. - Heavy head insert/remove → **ArrayDeque** / **LinkedList**. - Frequent membership checks → **HashSet**. Measure before optimizing; for small n the constant factors dominate and ArrayList's cache-friendly contiguous layout usually beats 'theoretically faster' linked structures.
- How can you avoid ArrayList's resize copies when you know the size in advance?Construct it with an initial capacity: new ArrayList<>(expectedSize). The backing array starts big enough, so adds never trigger a grow-and-copy, keeping every add a true O(1) write.
- When would a LinkedList actually beat an ArrayList?Rarely in practice. LinkedList wins only when you do many insertions/removals at the head or via an existing iterator/node, and don't need indexed access. Even then ArrayDeque usually beats it due to cache locality and lower per-node object overhead.
saying these in an interview costs you the question
- Saying middle insertion into an ArrayList is O(1)
- Claiming any single add() is guaranteed O(1) (it can be O(n) on resize)
- Thinking ArrayList contains() is fast (it's O(n))
- Believing LinkedList is generally faster — it's worse for indexed access and cache locality