Course contents

Big-O cheat sheet for collections

Big-O costs for Java collection operations, amortised ArrayList growth, expected vs worst-case hashing, constants, memory overhead and interview-safe phrasing.

FreeMust know for interviewsLesson14 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a heat map, not a scoreboard
  2. The main table
  3. Amortised O(1): why ArrayList append is not magic
  4. Expected versus worst case for hashing
  5. Why Big-O hides constants
  6. Memory overhead ballparks
  7. Advanced: collection-specific caveats
  8. How to quote complexity in an interview

🧮 What is a Big-O cheat sheet for collections?

A Big-O cheat sheet is a map from a collection operation to how its cost grows as the collection grows. For example, ArrayList.get(500_000) is still O(1), while ArrayList.contains(x) may scan 500,000 elements and is O(n). The sheet is useful only if you say the caveats: expected, amortised, worst-case, ordered iteration and memory overhead.

For this lesson, we'll focus on the operations interviewers ask you to compare: get, add, remove, contains and iteration for the main Java 21 collections. We'll also cover amortised ArrayList growth, expected versus worst-case hashing, why Big-O hides constants, memory per element ballparks and how to quote complexity without sounding overconfident. This lesson covers the checklist item Time Complexity of Java Collections.

Below the line (out of scope)

The 30-second answer

ArrayList gives O(1) indexed get, amortised O(1) append and O(n) middle insert or remove because it shifts elements. LinkedList gives O(1) add or remove only when you already hold the node through an iterator; finding that position is O(n), and pointer chasing makes it slower than its Big-O suggests. Hash collections are expected O(1) for contains, get, put and remove, with Java 8+ treeified bins capping collision-heavy bins at O(log n) in the usual comparable-key case, but resizes are still occasional O(n). Tree collections are O(log n) and buy you sorted order and range queries. PriorityQueue is O(1) peek, O(log n) offer or poll, and not sorted when iterated. In interviews, say average, amortised and worst-case instead of just saying O(1).

What you’ll learn

  • Read a complexity table for get, add, remove, contains and iteration across the main Java collections
  • Explain amortised O(1) for ArrayList.add and why Java grows the backing array by about 1.5x
  • Separate expected O(1) hashing from Java 8+ tree-bin worst cases and occasional resize costs
  • Describe why Big-O hides constants: cache locality, pointer chasing, comparator cost and equals cost
  • Quote realistic memory overhead numbers for HashMap.Node, LinkedList.Node and array-backed collections
  • Give interview answers without overclaiming: say average, amortised, worst-case and iteration order precisely

Interview radar

Asked very often: ArrayList vs LinkedList, HashSet.contains vs TreeSet.contains, and why PriorityQueue iteration is not sorted. Senior rounds probe: Amortised growth, hash flooding, tree bins since Java 8, cache locality and presizing. Production angle: A table that says O(1) still hides a 1-million-element copy, a bad hashCode, or 48 bytes of node overhead per entry.

01Picture it first: a heat map, not a scoreboard

Think of the collections table as a heat map for where the work happens. Green cells mean the operation touches one or a few locations. Orange cells mean the operation walks a path or height, like a tree or heap. Red cells mean the operation may scan or shift a number of elements proportional to the collection size.

The diagram below compresses the operations you should keep in your head. It is not a promise that every green cell is equally cheap.

Fig. 1

A heat-map grid compares Java collections by get, add, remove, contains and iteration, showing array-backed lists as cheap for indexed get and append, hash collections as expected constant-time for membership, tree collections as logarithmic, priority queues as heap-ordered and linked structures as costly for random access

A heat-map grid compares Java collections by get, add, remove, contains and iteration, showing array-backed lists as cheap for indexed get and append, hash collections as expected constant-time for membership, tree collections as logarithmic, priority queues as heap-ordered and linked structures as costly for random access

Notice the pattern. Arrays win when the index is known and memory is contiguous. Hash tables win when equality lookup matters and hash distribution is reasonable. Trees win when you need sorted navigation. Heaps win when you only need the next best item, not a fully sorted collection.

02The main table

Read this table as Java 21, normal usage, n elements. For maps, get, add, remove and contains mean key operations: get(key), put(key, value), remove(key) and containsKey(key). For sets, get is not applicable.

Collection get add / put remove contains Iteration What to say out loud
ArrayList O(1) by index amortised O(1) at end, O(n) at index O(n) shift O(n) O(n), cache-friendly default list for indexed reads and appends
LinkedList O(n) O(1) at ends or iterator position, O(n) by index O(1) at iterator position, O(n) by value/index O(n) O(n), pointer chasing rarely the right default list
ArrayDeque no indexed get amortised O(1) at both ends O(1) at ends, O(n) by value O(n) O(n) default stack/queue, no null elements
HashMap / HashSet expected O(1), tree-bin O(log n) expected O(1), resize O(n) occasionally expected O(1) expected O(1) O(capacity + size) for map/set table walk average O(1), worst-case caveats
LinkedHashMap / LinkedHashSet expected O(1) expected O(1) expected O(1) expected O(1) O(n) in link order hash table plus insertion/access order
TreeMap / TreeSet O(log n) O(log n) O(log n) O(log n) O(n) sorted sorted keys, range queries
PriorityQueue O(1) peek, no random get O(log n) offer O(log n) poll, O(n) remove object O(n) O(n), not sorted heap, not a sorted list
EnumMap / EnumSet O(1) O(1) O(1) O(1) O(enum constants) or O(n) compact when keys are enum constants
CopyOnWriteArrayList O(1) O(n) copy O(n) copy O(n) O(n) snapshot many readers, rare writes
ConcurrentHashMap expected O(1) expected O(1) expected O(1) expected O(1) O(n) weakly consistent shared map default, no null
ConcurrentSkipListMap O(log n) O(log n) O(log n) O(log n) O(n) sorted, weakly consistent concurrent sorted map

Two details are easy to miss. HashMap iteration is not pure O(size): it scans the bucket array as well as entries, so a very sparse map costs more to iterate than its size suggests. LinkedHashMap iteration follows the doubly linked list, so it is O(size), which can make it nicer for iteration-heavy caches.

Try it yourself first

Before reading on, choose the collection and complexity for each case: a FIFO job queue, a lookup by user ID, a leaderboard sorted by score, and a listener list that is read thousands of times per write.

Queue<Job> jobs = new ArrayDeque<>();                 // offer/poll at ends: amortised O(1)
Map<String, User> usersById = new HashMap<>();        // get by key: expected O(1)
NavigableMap<Integer, String> leaderboard = new TreeMap<>(); // nearest scores: O(log n)
List<Listener> listeners = new CopyOnWriteArrayList<>();     // iteration cheap, writes copy

Notice three things:

  • The table gives you the cost of the operation, not the best type by itself.
  • PriorityQueue would be right for "give me the next highest score", but TreeMap is better for "closest score above and below".
  • CopyOnWriteArrayList is terrible for frequent writes and excellent for mostly-read listener lists because each iterator sees a stable array snapshot.

03Amortised O(1): why ArrayList append is not magic

An ArrayList is an Object[] plus a size. A no-argument new ArrayList<>() starts with a shared empty array; the first append allocates capacity 10. Once the array is full, Java grows it to old + (old >> 1), roughly 1.5x, and copies the old references into the new array.

That means a single append can be O(n), because the grow step copies n references. Across a long run of appends, those copies are spread over many cheap inserts, so the average cost per append is O(1). That is what amortised means.

List<Integer> ids = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
    ids.add(i);       // most calls write one slot; a few calls copy the whole backing array
}

If the size is known, presize it. new ArrayList<>(1_000_000) allocates enough slots up front and avoids the growth copies. The same idea applies to hash maps: since Java 19, HashMap.newHashMap(expectedMappings) sizes the table for the number of mappings you expect, not just a raw capacity.

Performance angle

With compressed ordinary object pointers, an ArrayList reference slot is 4 bytes. Copying 1,000,000 references is about 4 MB of memory traffic, which is still usually cheaper than chasing 1,000,000 linked nodes scattered across the heap. Big-O says both are O(n); the CPU sees very different work.

04Expected versus worst case for hashing

Hash tables earn expected O(1) by spreading keys across buckets. HashMap stores an array of buckets, hashes the key, masks the hash to pick a bucket, then compares stored hash, == and equals inside that bucket. With a good hash distribution and a load factor around 0.75, buckets stay short.

The honest answer has four parts:

  1. Normal get, put, remove and containsKey are expected O(1).
  2. A resize is O(n), but happens occasionally and is amortised across inserts.
  3. Since Java 8, a bucket that grows past the treeification thresholds can become a red-black tree, so a treeified bin is O(log k) for k entries in that bin.
  4. A bad hashCode, mutable key or expensive equals can still dominate your runtime.
record BadKey(int id) {
    @Override public int hashCode() { return 42; }
}

Map<BadKey, String> orders = new HashMap<>();
for (int i = 0; i < 20_000; i++) {
    orders.put(new BadKey(i), "order-" + i);
}

System.out.println(orders.get(new BadKey(19_999))); // works, but collisions do the hard work

This example is intentionally hostile: every key lands in the same bucket. Java 8+ tree bins are a defence against that shape of input, but the interview-safe phrase is still "expected O(1), with tree-bin worst-case protection since Java 8". We cover the thresholds and resize split in Hashing, collisions, treeification and load factor.

Where candidates slip

Many candidates say "HashMap is O(1)" and stop. That sounds memorised. Say "expected O(1), occasional O(n) resize, and Java 8+ tree bins protect collision-heavy buckets with O(log n) lookup in the treeified case." That one sentence is much stronger and still honest.

05Why Big-O hides constants

Big-O ignores constant factors, allocation, cache locality, branch prediction and the cost of user code such as equals, hashCode and compare. In real Java, those constants decide many close calls.

ArrayList and LinkedList are the classic trap. Removing from the middle of a LinkedList is O(1) only if you already have the ListIterator positioned at that node. If all you have is an index or a value, the list must first walk node by node, which is O(n). Each node is a separate object, so that walk is also cache-unfriendly.

List<Integer> array = new ArrayList<>(List.of(10, 20, 30, 40));
array.add(2, 25);          // shifts 30 and 40 with System.arraycopy

List<Integer> linked = new LinkedList<>(List.of(10, 20, 30, 40));
ListIterator<Integer> it = linked.listIterator(2);
it.add(25);                // O(1) after the iterator is already at index 2

The default recommendation is practical: start with ArrayList for lists unless you have a measured reason not to. Use ArrayDeque for stack and queue behavior. Use LinkedList only when you specifically benefit from iterator-position inserts/removes or need a Deque that permits null, which is rare.

In the real world

A common production mistake is replacing an ArrayList with a LinkedList because "we remove from the middle". If the code finds the item with indexOf, contains or a for-each scan first, it already paid O(n), then adds pointer chasing and per-node allocation. In many services the ArrayList version wins because System.arraycopy is a tight native memory move and the array is contiguous.

06Memory overhead ballparks

Complexity tells you growth rate; memory tells you whether the structure fits in cache or in the heap. These numbers vary with JVM, object alignment, compressed oops and field layout, so quote them as ballparks, not laws.

Structure piece Approximate overhead on a 64-bit HotSpot JVM with compressed oops Why it matters
ArrayList object about 24 bytes, plus backing array stores size and a reference to Object[]
Object[] slot 4 bytes per reference compact and cache-friendly
LinkedList.Node about 24 bytes plus the element object item, next, prev, object header
HashMap.Node about 32-48 bytes plus key and value objects int hash, key ref, value ref, next ref, header and padding
LinkedHashMap.Entry roughly one HashMap.Node plus two references before/after links for order
TreeMap.Entry roughly 40-56 bytes key, value, left, right, parent, color and padding
EnumSet one or more long words up to 64 enum constants fit in one long

This is why a million-entry HashMap is not just a million keys and values. It also has a bucket array and a node object per entry. If the keys and values are small, the overhead can be the majority of the memory footprint.

Performance angle

A LinkedList<Integer> with 1,000,000 elements has about 24 MB of node objects before counting the 1,000,000 Integer objects. An ArrayList<Integer> has about 4 MB of reference slots plus the same Integer objects. If the integers are cached or shared, the list storage difference is still about 20 MB.

07Advanced: collection-specific caveats

Lists and deques

ArrayList.contains is O(n), because it must compare elements until it finds a match. ArrayDeque.contains is also O(n). If your hot path asks "have I seen this?", a HashSet is usually the companion structure you want.

ArrayDeque is a circular array. Adding or removing at either end is amortised O(1), and it rejects null so that poll() returning null can mean "empty". For stack code, prefer ArrayDeque over the legacy synchronized Stack.

Maps and sets

HashSet is backed by a HashMap, LinkedHashSet by a LinkedHashMap, and TreeSet by a TreeMap. So the set costs mirror the map costs. The extra thing to remember is iteration order: none for HashSet, insertion order for LinkedHashSet, sorted order for TreeSet.

EnumMap and EnumSet are special because enum constants have dense ordinals. An EnumMap stores values in an array indexed by ordinal; an EnumSet stores bits in one or more longs. If your key type is an enum, they are usually both faster and smaller than hash-based alternatives.

Priority queues

A PriorityQueue is a binary heap in an array. peek() is O(1) because the minimum element is at the root. offer() and poll() are O(log n) because the element bubbles up or down the heap height.

The iterator is the gotcha: it walks the heap array, not sorted order. To consume in priority order, repeatedly call poll(), or copy the queue first if you need to preserve it.

Concurrent collections

ConcurrentHashMap has expected O(1) reads and updates, but its iterators are weakly consistent: they may reflect some concurrent updates and never throw ConcurrentModificationException. It also rejects null keys and values so get(k) == null always means absent.

ConcurrentSkipListMap is the concurrent sorted-map option. It uses a skip-list structure, so core operations are O(log n), and it gives you NavigableMap methods such as floorEntry, ceilingEntry and subMap under concurrent access.

CopyOnWriteArrayList is the opposite trade-off. Reads and iteration use a stable array and need no locks, while every write copies the entire array. I use it for listener lists and small registry-style collections, not for request streams.

08How to quote complexity in an interview

When I ask collection complexity, I'm listening for careful language. You do not need to recite every cell. I want to hear that you know the engine underneath the collection and can tie the cost to that engine.

A good answer follows this pattern: name the collection, name the backing structure, say average/amortised/worst-case, then mention the constant that matters. For example: "ArrayList.add(e) is amortised O(1) because it usually writes the next array slot, but a grow copies the array. HashMap.get is expected O(1), with tree-bin protection for heavy collisions since Java 8, and the real cost includes hashCode and equals."

In the interview

"I don't quote HashMap as plain O(1). I say expected O(1) for get, put and remove with a decent hash, occasional O(n) resize amortised across inserts, and Java 8+ tree bins giving O(log n) protection for collision-heavy buckets. ArrayList is O(1) by index and amortised O(1) append, but middle insert and remove are O(n) shifts. LinkedList only gets O(1) insert or remove if I already have the iterator at the node; otherwise finding the position is O(n), and the cache behavior is poor. Tree collections are O(log n) and buy sorted navigation, while PriorityQueue gives O(1) peek and O(log n) offer or poll but not sorted iteration. If memory matters, I also mention node overhead: linked and hash structures spend dozens of bytes per element, while arrays store references compactly."

On the whiteboard

ArrayList      get O(1), append amortised O(1), middle add/remove O(n)
LinkedList     get/contains O(n), O(1) edit only with iterator at node
ArrayDeque     ends amortised O(1), contains O(n), no nulls
HashMap/Set    expected O(1), resize O(n), tree bins O(log n) since Java 8
LinkedHash*    expected O(1), iteration O(n) in insertion/access order
TreeMap/Set    O(log n), sorted/range operations
PriorityQueue  peek O(1), offer/poll O(log n), remove/contains O(n)
EnumMap/Set    O(1), compact array/bit-vector for enum keys
COWAL          read O(1), write O(n) copy, snapshot iteration
CHM            expected O(1), weakly consistent iteration, no nulls
Big-O caveat   constants matter: cache locality, equals/hashCode/comparator, memory
Practise this topicCollections framework: interview questions & scenarios
Further reading (8)