Array vs Linked List: The Tradeoff Every Interview Expects You to Know

May 27, 202616 min read
dsaalgorithmsinterview-prepdata-structures
Array vs Linked List: The Tradeoff Every Interview Expects You to Know
TL;DR
  • Array stores elements contiguously with O(1) random access, O(n) mid-insert, and excellent cache locality
  • Linked list O(1) insertion is conditional on already having a pointer to the node; without one, you pay O(n) to find it first
  • Cache locality often matters more than operation count; arrays beat linked lists in most benchmarks, even for theoretically slower operations
  • Dynamic arrays give O(1) amortized append with 1.5x to 2x growth factors, matching linked list tail insertion
  • LRU cache is the canonical linked list win: a hash map provides the pointer, the doubly linked list provides O(1) structural modification
  • Default to array in interviews; switch to linked list only when you hold direct pointers and need O(1) node manipulation without random access

You know the array vs linked list comparison. You can define both, draw them, maybe even implement them from scratch. But when an interviewer asks "why would you use a linked list here instead of an array?" and you answer with "because insertion is O(1)," you've already lost a point. That answer is incomplete, and in most real scenarios, it's flat out wrong.

The real tradeoff is about where you pay. Arrays pay when they move data. Linked lists pay when they find it. Every operation, every design decision between the two comes down to that single asymmetry. Not Big-O classes. Payment location.

Below: how both structures sit in memory, what each operation actually costs, where the textbook complexity hides a lie, and when you should pick one over the other.

Linked lists: what is my purpose? You will be used in coding interviews forever. Linked lists in the wild are basically endangered. Interviews are their nature preserve.

What Is an Array, Really?

An array is a contiguous block of memory where each element occupies a fixed-size slot. If the first element lives at address 0x1000 and each element is 4 bytes, the fifth element lives at 0x1010. No searching, no traversal. One multiplication and one addition. Your CPU does this faster than you can blink. Actually, faster than the electrical signal travels from your brain to your eyelid.

Array contiguous memory layout showing O(1) index access via address arithmetic Arrays in memory: everything in a row, like a good spreadsheet.

That's why random access is O(1). The address of any element is a simple arithmetic expression: base + index * element_size. The CPU computes it in a single cycle.

But contiguity cuts both ways. To insert an element at position 2, every element from index 2 onward must shift right by one slot. Deleting at position 2 means every element after it shifts left. The data physically moves in memory. It's like inserting a person into the middle of a crowded bench. Everyone has to scoot.

What Is a Linked List, Really?

A linked list is a chain of nodes scattered across memory like socks after laundry day. Each node holds two things: a value and a pointer to the next node. A doubly linked list adds a pointer to the previous node too.

Linked list nodes scattered across non-sequential memory addresses connected by pointers Linked list nodes in memory: scattered everywhere, held together by promises (pointers).

Notice the addresses. They're not sequential. Node A is at 0x4A20, Node B at 0x7F08, Node C at 0x2B40. The nodes can live anywhere in the heap. The only thread connecting them is the pointer each node carries.

To reach the kth element, you must follow k pointers from the head. There is no shortcut. No arithmetic trick. You walk the chain, one node at a time, like following a treasure map where each clue only tells you where the next clue is.

The Complexity Table Everyone Memorizes (and Nobody Questions)

The table you'll find in every textbook, tattooed on the inside of every CS student's eyelids:

OperationArraySingly Linked List
Access by indexO(1)O(n)
Search (unsorted)O(n)O(n)
Insert at beginningO(n)O(1)
Insert at endO(1) amortized*O(1) with tail pointer
Insert at position kO(n)O(1)**
Delete at beginningO(n)O(1)
Delete at position kO(n)O(1)**

*Dynamic arrays double their capacity when full, making append O(1) amortized.

**This is the asterisk that changes everything.

The Asterisk That Changes Everything

That O(1) for linked list insertion is only true if you already have a pointer to the node before the insertion point. In practice, you almost never do. This is like saying "moving apartments is free" while quietly ignoring the six months you spent finding one.

Think about it. If someone says "insert 25 after the node with value 20," you first need to find that node. Finding it means traversing from the head: O(n). Then the pointer rewiring is O(1). The total? O(n).

# "O(1) insertion" requires a reference you usually don't have def insert_after(node, value): new_node = Node(value) new_node.next = node.next node.next = new_node # This part is O(1). But how did you get `node`?

Compare this to arrays. Inserting at position k in an array is O(n) because you shift elements. But you found position k in O(1) because you computed its address.

The linked list's O(1) insertion is payment for an O(n) search. The array's O(n) insertion is payment for an O(1) lookup. You always pay n somewhere. The question is where.

The only scenario where linked list insertion is genuinely O(1) end-to-end is inserting at the head or tail (with a tail pointer). And for inserting at the end, dynamic arrays achieve the same O(1) amortized.

How Dynamic Arrays Actually Work

Most languages don't give you raw fixed-size arrays. Python's list, Java's ArrayList, C++'s std::vector, and JavaScript's Array are all dynamic arrays. They manage resizing for you, like a hotel that quietly builds a new wing whenever the old one fills up.

A dynamic array starts with some initial capacity. When you append and the array is full, it allocates a new block (typically 2x the old size), copies everything over, and frees the old block.

Dynamic array resizing: allocate 2x capacity, copy elements, then append cheaply Dynamic arrays: expensive move once, cheap rent for a while.

The copy costs O(n) when it happens. But it happens so rarely that the average cost per append is O(1). This is amortized analysis. The total cost of n appends is O(n), so each append averages O(1).

The growth factor matters. Java's ArrayList grows by 1.5x. CPython's list uses roughly 1.125x (via new_size + (new_size >> 3) + 6). C++'s std::vector typically uses 2x. A factor of 2 wastes up to half the allocated memory. A factor of 1.5 wastes less but copies more frequently. Both give O(1) amortized. It's a choose-your-own-waste adventure.

Memory: The Cost You Don't See in Big-O

Big-O tells you how many steps an operation takes. It says nothing about how much memory each element consumes. This is where the comparison gets spicy.

Memory overhead comparison: array 4 bytes per int32, singly linked list ~16 bytes, doubly linked list ~32 bytes Linked list overhead visualized. Those pointers add up fast.

Array: Each element uses exactly the bytes needed for the value. An array of 1,000 32-bit integers uses 4,000 bytes. Period.

Singly Linked List: Each node stores the value plus one pointer. On a 64-bit system, that pointer is 8 bytes. So each node holding a 4-byte integer actually uses 12 bytes (plus padding and allocator overhead, often rounding to 16 or even 24 bytes). For 1,000 integers, that's 16,000 to 24,000 bytes.

Doubly Linked List: Two pointers per node. A 4-byte value plus 16 bytes of pointers, plus alignment and allocator metadata. Each node easily reaches 32 bytes. For 1,000 integers: 32,000 bytes.

That's 4x to 8x the memory of an array, storing the same data. Your linked list is basically paying rent for a two-bedroom apartment to store a single sock.

StructureBytes per int32 element (64-bit system)
Array4
Singly linked list~16-24
Doubly linked list~32

Dynamic arrays waste some space on unused capacity (up to 2x with a doubling strategy). Even in the worst case, that's 8,000 bytes for 1,000 integers, still far less than a linked list.

Why Memory Layout Destroys the Complexity Table

This is the fact that flips the textbook on its head: on modern hardware, a cache miss costs 50 to 100 times more than an arithmetic operation. The complexity table counts operations. It says nothing about cache misses. It's like rating restaurants by menu size and ignoring the food.

Your CPU doesn't fetch one byte from RAM at a time. It fetches a 64-byte cache line. When you read array[0], the CPU loads 64 bytes starting at that address. If each element is 4 bytes, you just got elements 0 through 15 for free. When you read array[1] through array[15], they're already in cache. Zero wait. It's a buy-one-get-fifteen-free deal, and arrays trigger it on every single access.

Cache line comparison: arrays load 16 integers per fetch while linked lists waste most of each cache line Arrays get 16 elements per cache fetch. Linked lists get one element and a lot of empty calories.

Arrays exploit this brutally. Traversing an array is a sequential scan through contiguous memory. The hardware prefetcher detects the pattern and starts loading the next cache line before you even ask for it. Your CPU is basically reading ahead like an overachiever.

Linked lists get none of this. Node A is at 0x4A20. Node B is at 0x7F08. Those addresses are in completely different cache lines. Every node = node.next is a potential cache miss. The prefetcher can't predict where the next node lives because it depends on a pointer value that hasn't been loaded yet. This is pointer chasing, and it serializes memory access into a sad, single-file line.

Bjarne Stroustrup (creator of C++) demonstrated this in a famous benchmark. He sorted 500,000 integers using both std::vector and std::list. The vector found the insertion point by scanning (O(n)) and shifted elements (O(n)). The list found the insertion point by traversal (O(n)) and rewired pointers (O(1)). The vector was faster. Sequential memory movement is something the CPU handles at near-peak throughput. The list traversal was a chain of dependent cache misses, each one waiting for the previous one to resolve before it even knew where to look.

The practical result: for collections under a few thousand elements, array operations that are theoretically O(n) often beat linked list operations that are theoretically O(1). The constant factor hidden inside that O(1) includes a cache miss penalty of 80 to 100 nanoseconds per hop. Big-O lied to you. Well, it lied by omission.

When Linked Lists Actually Win

If arrays are faster in practice, why do linked lists exist? Because a few situations make their structural properties irreplaceable. Linked lists are specialists, not generalists. And in their niche, nothing else comes close.

You Have a Direct Pointer and Need O(1) Removal

An LRU cache is the textbook example, and honestly the reason linked lists still have a job. You store nodes in a doubly linked list and keep a hash map from keys to node pointers. When you access a key, you look up the node in O(1), unlink it from wherever it sits in O(1) (pointer rewiring, no shifting), and move it to the front in O(1).

LRU cache architecture: hash map for O(1) lookup connected to doubly linked list for O(1) structural modification The hash map finds the node. The linked list moves it. Together, they're unstoppable.

Try doing that with an array. You'd find the element in O(1) via the hash map, but moving it to the front means shifting every element between its position and the front. That's O(n).

# LRU cache: hash map + doubly linked list # Access: O(1) lookup + O(1) move-to-front # Evict: O(1) remove from tail def access(key): node = hashmap[key] # O(1) unlink(node) # O(1) pointer surgery push_front(node) # O(1)

This is the one case everyone should know. The hash map gives you the pointer. The linked list gives you O(1) structural modification. Together, they achieve something no single array-based structure can.

Frequent Insertion and Deletion in the Middle, With Maintained References

If your program holds references (pointers) to specific nodes and frequently inserts or deletes around them, linked lists avoid the cascading shifts that arrays require. Operating system schedulers, for example, maintain process queues where processes are added and removed constantly, and the OS holds direct handles to each process's node.

The Linux kernel uses intrusive linked lists extensively. The list_head structure contains only prev and next pointers, embedded directly into the struct it chains. The container_of macro recovers the enclosing struct from the list node's address. This lets the kernel maintain process lists, I/O queues, and timer wheels with O(1) insertion and removal, no external allocation, and zero overhead beyond two pointers per entry.

You Cannot Afford a Worst-Case O(n) Pause

Dynamic arrays are O(1) amortized for appends, but every so often they allocate and copy. That single O(n) spike can be catastrophic in a real-time system. A linked list append is O(1) every single time. No exceptions. No surprise resize bills.

Real-time audio processing, embedded control loops, and certain networking paths cannot tolerate jitter. In those contexts, predictable per-operation cost matters more than a better average. Consistency beats speed when a single hiccup means audible glitches or dropped packets.

You Need Efficient Merging or Splitting

Merging two linked lists is O(1). Point the tail of the first at the head of the second. Done. No copies, no allocations. Splitting a linked list at a known node is also O(1).

Merging two arrays means allocating a new buffer and copying both. That's O(n + m). In algorithms like merge sort on linked lists, this property makes the linked list version elegant and allocation-free.

When Arrays Win (Which Is Most of the Time)

In most application-level code, arrays (or dynamic arrays) win. It's not even close:

  • Random access is the primary operation. Binary search, indexing by position, anything that jumps around the data.
  • You iterate frequently. Sequential traversal is where arrays dominate, thanks to cache locality. Your CPU's prefetcher basically rolls out a red carpet.
  • Memory matters. Arrays use 4 to 8 times less memory per element than linked lists.
  • You append at the end. Dynamic arrays handle this in O(1) amortized, same as a linked list with a tail pointer.
  • You need to sort. Quicksort and heapsort rely on random access. Merge sort works on linked lists, but a cache-friendly array-based merge sort is faster in practice.

How to Decide in an Interview

When an interviewer presents a problem, use this mental model:

Default to array. If the problem involves indexing, sorting, or iterating, use an array. Don't overthink it. Arrays are the Honda Civic of data structures. Reliable, efficient, good at almost everything.

Switch to linked list only if you need all three of these properties simultaneously:

  1. You have direct pointers to the nodes you care about (usually via a hash map).
  2. You need O(1) insertion or removal at those pointers.
  3. You don't need random access by index.

If any of those three is missing, the array almost certainly wins.

The LRU cache problem (LeetCode 146) is the canonical example. Two Sum uses a hash map (not a linked list). Merge intervals uses a sorted array. Sliding window problems use arrays or deques. The number of interview problems where a linked list is the optimal choice is small, and they all share that "hash map + linked list" shape.

The Worked Example: Why LRU Demands a Linked List

Let's trace through a concrete LRU cache with capacity 3 and operations: put(1,A), put(2,B), put(3,C), get(1), put(4,D).

Using a doubly linked list + hash map:

put(1,A): List: [1] ← head/tail       Map: {1→node1}
put(2,B): List: [2,1]                  Map: {1→node1, 2→node2}
put(3,C): List: [3,2,1]                Map: {1→node1, 2→node2, 3→node3}
get(1):   Find node1 via map → O(1)
          Unlink node1 from position 3 → O(1) pointer surgery
          Move to front → O(1)
          List: [1,3,2]
put(4,D): Cache full. Evict tail (node2, key=2) → O(1)
          Remove key 2 from map → O(1)
          Insert node4 at front → O(1)
          List: [4,1,3]                Map: {1→node1, 3→node3, 4→node4}

Every operation is O(1). The hash map gives you the pointer. The linked list gives you O(1) structural change at that pointer.

Now try it with an array:

get(1):   Find index of key 1 via map → O(1)
          Move element at index 2 to index 0
          Shift elements 0 and 1 right → O(n)

The shift kills it. With n elements in the cache, every access is O(n). That's the difference. That's why LRU cache is the interview question that gives linked lists a reason to exist.

The Worked Example: Why Binary Search Demands an Array

Now flip the scenario. You have a sorted collection of 1,000,000 integers and need to find whether a target exists.

Using an array:

def binary_search(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = lo + (hi - lo) // 2 if arr[mid] == target: return mid elif arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1

20 comparisons. Each arr[mid] is O(1) random access. Total: O(log n).

Using a linked list: You can't compute mid without traversing from the head. Finding the middle element alone is O(n). Binary search degrades to O(n log n), worse than scanning the entire list linearly. You've managed to make divide-and-conquer slower than brute force. Impressive, in the worst way.

Random access is the foundation of every divide-and-conquer algorithm on sequences. Without it, you lose logarithmic time.

Quick Recap

  • Arrays store elements contiguously. O(1) access by index, O(n) insert/delete in the middle, excellent cache locality, low memory overhead.
  • Linked lists store elements anywhere, connected by pointers. O(n) access, O(1) insert/delete if you already have a pointer to the right spot, poor cache locality, high memory overhead.
  • The O(1) linked list insertion is conditional on having a pointer. Without one, you pay O(n) to find the spot, making the total cost the same as an array.
  • On modern hardware, cache locality often matters more than operation count. Arrays win most benchmarks, even for operations where linked lists have a theoretical edge.
  • Linked lists earn their place when you hold direct references and need O(1) structural modification: LRU caches, OS schedulers, real-time systems, and list merging/splitting.
  • In interviews, default to arrays. Switch to a linked list only when you need hash-map-backed O(1) node manipulation without random access.

Practice the Decision Under Pressure

Knowing the tradeoff intellectually is step one. Articulating it in a live interview, while choosing the right structure for the problem at hand, is step two. SpaceComplexity runs voice-based mock interviews where you explain your data structure choices out loud, with rubric-based feedback on whether your reasoning actually landed.

Further Reading