Skip to main content

Choosing the Right Data Structure

A comprehensive guide to selecting the optimal data structure for your use case.

Quick Decision Tree

Need key-value mapping?
├─ Yes → Need sorted order?
│ ├─ Yes → SortedMap
│ └─ No → Need bidirectional lookup?
│ ├─ Yes → BiDirectionalMap
│ └─ No → JavaScript Map (native)

└─ No → Need ordered collection?
├─ Priority-based → PriorityQueue / BinaryHeap
├─ FIFO (Queue) → Queue
├─ LIFO (Stack) → Array or Deque
├─ Both ends → Deque
├─ Sorted unique values → RedBlackTree
├─ String prefix search → Trie
├─ Sequential access → LinkedList / DoublyLinkedList
└─ LRU caching → LRUCache

By Use Case

Caching

RequirementBest ChoiceWhy
Fixed-size cacheLRUCacheAutomatic eviction of least used
Simple cacheJavaScript MapNative, fast, no size limit
With TTLLRUCache (with ttl option)Built-in expiration
Multi-level cacheLRUCache + MapLayer fast and slow storage

Example:

const cache = new LRUCache<string, Data>({
capacity: 100,
ttl: 300000, // 5 minutes
});

Collections

Linear Collections

NeedBest ChoiceTime Complexity
Add/remove at frontLinkedListO(1)
Add/remove at endLinkedList, ArrayO(1)
Add/remove at both endsDoublyLinkedList or DequeO(1)
Random access by indexArrayO(1)
Sequential iterationLinkedList, DoublyLinkedListO(n)
Reverse iterationDoublyLinkedListO(n)

When to use LinkedList vs Array:

// ✅ Use LinkedList when:
// - Frequent insertions/deletions at ends
// - Size changes frequently
// - Don't need random access
const taskQueue = new LinkedList<Task>();

// ✅ Use Array when:
// - Need random access (arr[i])
// - Size relatively stable
// - Frequent index-based operations
const items = [1, 2, 3];

Queue Operations

PatternBest ChoiceOperations
FIFO (First-In-First-Out)Queueenqueue(), dequeue()
LIFO (Last-In-First-Out)Array (as stack)push(), pop()
Priority-basedPriorityQueueenqueue(), dequeue() by priority
Both endsDequeaddFirst(), addLast(), removeFirst(), removeLast()

Example scenarios:

// Task processing (FIFO)
const tasks = new Queue<Task>();

// Undo/Redo (LIFO)
const undoStack: Action[] = [];

// Event handling by priority
const events = new PriorityQueue<Event>({
comparator: (a, b) => a.priority - b.priority,
});

// Sliding window
const window = new Deque<number>();

Maps and Lookups

RequirementBest ChoiceLookup Time
Key→Value lookupJavaScript MapO(1) avg
Key→Value AND Value→KeyBiDirectionalMapO(1)
Sorted by keySortedMapO(log n)
Min/Max key accessSortedMapO(log n)
Prefix-based string lookupTrieO(m), m=key length

Comparison:

// Standard map
const map = new Map<string, number>();
map.set('a', 1);
map.get('a'); // O(1)

// Bidirectional map
const biMap = new BiDirectionalMap<string, number>();
biMap.set('a', 1);
biMap.get('a'); // O(1) - get value by key
biMap.getKey(1); // O(1) - get key by value

// Sorted map
const sorted = new SortedMap<number, string>();
sorted.set(3, 'three');
sorted.set(1, 'one');
sorted.firstKey(); // 1 - O(log n)
for (const [k, v] of sorted) {
// Iterates in sorted order
}

// Trie for strings
const trie = new Trie<number>();
trie.insert('apple', 1);
trie.getAllWithPrefix('app'); // ['apple'] - O(p+n)

Sets and Unique Values

RequirementBest ChoiceNotes
Unique values, unsortedJavaScript SetNative, fast
Unique values, sortedRedBlackTreeO(log n) operations
Min/Max accessRedBlackTreeO(log n)
Duplicate checkingSet or RedBlackTreeO(1) or O(log n)
// Unsorted unique values
const uniqueIds = new Set<string>();

// Sorted unique values
const sortedScores = new RedBlackTree<number>();
sortedScores.insert(85);
sortedScores.insert(92);
sortedScores.min(); // 85
sortedScores.max(); // 92

Tree Operations

NeedBest ChoiceOperations
Sorted collectionRedBlackTreeinsert(), remove(), contains() all O(log n)
Priority queuePriorityQueue / BinaryHeapEfficient min/max access
String searchTriePrefix matching, autocomplete

Priority and Heap Operations

Use CaseBest ChoiceWhy
Task schedulingPriorityQueueQueue-like API with priorities
K largest/smallestBinaryHeap (MinHeap/MaxHeap)Efficient top-k algorithms
Median trackingMinHeap + MaxHeapTwo-heap approach
Dijkstra's algorithmPriorityQueueNeed min-priority extraction
// Task scheduling
const scheduler = new PriorityQueue<Task>({
comparator: (a, b) => b.priority - a.priority,
});

// K largest elements
const minHeap = new MinHeap<number>();
for (const num of numbers) {
minHeap.insert(num);
if (minHeap.size > k) minHeap.remove();
}

Performance Comparison

Time Complexity

Data StructureInsertDeleteSearchAccessMin/Max
ArrayO(n)O(n)O(n)O(1)O(n)
LinkedListO(1)*O(n)O(n)O(n)O(n)
DoublyLinkedListO(1)*O(n)O(n)O(n)O(n)
QueueO(1)O(1)O(n)--
DequeO(1)O(1)O(n)O(1)-
PriorityQueueO(log n)O(log n)O(n)O(1)**O(1)**
BinaryHeapO(log n)O(log n)O(n)O(1)**O(1)**
RedBlackTreeO(log n)O(log n)O(log n)-O(log n)
SortedMapO(log n)O(log n)O(log n)-O(log n)
TrieO(m)***O(m)***O(m)***--
BiMapO(1)O(1)O(1)O(1)-
LRUCacheO(1)O(1)-O(1)-

* At ends only ** Peek operation *** m = string length

Space Complexity

Data StructureSpaceNotes
AllO(n)Linear in number of elements
LinkedListO(n)Additional pointer per node
DoublyLinkedListO(n)Two pointers per node
TrieO(ALPHABET_SIZE × N × M)Can be large for long strings
BiMapO(2n)Two internal maps

Common Scenarios

1. Web Browser History

Requirement: Navigate back/forward through visited pages

Best Choice: DoublyLinkedList

const history = new DoublyLinkedList<string>();
history.append('/home');
history.append('/products');
// Navigate with forward/reverse iteration

Alternative: Array with index pointer (simpler but less efficient for modifications)

2. Autocomplete/Search Suggestions

Requirement: Find all words starting with a prefix

Best Choice: Trie

const trie = new Trie<number>(false); // case-insensitive
trie.insert('apple', 100);
trie.insert('application', 85);
trie.getAllWithPrefix('app'); // ['apple', 'application']

Alternative: Array with filter (simpler but O(n) per search)

3. Leaderboard

Requirement: Maintain player scores in rank order

Best Choice: SortedMap

const leaderboard = new SortedMap<number, Player>({
comparator: (a, b) => b - a, // Descending
});

Alternative: Array + sort (simpler but O(n log n) per update)

4. Rate Limiting

Requirement: Track request counts per user with expiration

Best Choice: LRUCache with TTL

const rateLimiter = new LRUCache<string, number>({
capacity: 10000,
ttl: 60000, // 1 minute
});

5. Event Processing

Requirement: Process events in priority order

Best Choice: PriorityQueue

const events = new PriorityQueue<Event>({
comparator: (a, b) => a.priority - b.priority,
});

6. User ID ↔ Username Mapping

Requirement: Look up in both directions

Best Choice: BiDirectionalMap

const users = new BiDirectionalMap<number, string>();
users.set(1, 'alice');
users.get(1); // 'alice'
users.getKey('alice'); // 1

Decision Factors

1. Access Patterns

  • Random access: Array, Map
  • Sequential: LinkedList, Queue
  • Sorted: RedBlackTree, SortedMap
  • Priority: PriorityQueue, BinaryHeap

2. Modification Frequency

  • Frequent inserts/deletes: LinkedList, Heap
  • Mostly reads: Array, Map
  • Balanced: Tree structures

3. Size Constraints

  • Fixed size: LRUCache
  • Dynamic size: Most structures
  • Large datasets: Consider space overhead

4. Performance Requirements

  • Constant time critical: Map, BiMap, LRUCache
  • Logarithmic acceptable: Trees, Heaps
  • Linear acceptable: LinkedList search

Anti-Patterns

❌ Using Array for Frequent Insertions/Deletions

// Bad: O(n) for each shift
const arr = [1, 2, 3];
arr.unshift(0); // Shifts all elements

// Good: O(1)
const list = new LinkedList<number>();
list.prepend(0);

❌ Using LinkedList for Random Access

// Bad: O(n) to find index
list.get(1000);

// Good: O(1)
arr[1000];

❌ Not Using LRUCache for Caching

// Bad: No automatic eviction
const cache = new Map<string, Data>();
if (cache.size > 100) {
// Manual eviction logic needed
}

// Good: Automatic LRU eviction
const cache = new LRUCache<string, Data>({ capacity: 100 });

Summary

Choose based on your primary operation:

  • Caching: LRUCache
  • FIFO/LIFO: Queue / Stack (Array)
  • Priority: PriorityQueue
  • Sorted: RedBlackTree, SortedMap
  • Bidirectional lookup: BiDirectionalMap
  • String prefix: Trie
  • Sequential: LinkedList
  • Both ends: Deque / DoublyLinkedList

When in doubt, start simple (Array, Map) and optimize based on profiling!