Table of Contents
923 words
5 minutes
Design Problems: From Design Patterns to High-Frequency Data Structure Design

Overview#

Data structure design problems require combining existing data structures to implement a new abstraction under specified operations and complexity constraints. The focus is not on writing a single function, but on maintaining the object’s internal state so that every method satisfies its time and space requirements.

Prerequisites

  • Stacks, hash tables, linked lists: the foundational building blocks for high-frequency design problems
  • Complexity constraints: each API must be analyzed individually
  • Invariants: the object’s internal state must remain consistent after every operation

Problem Definition#

Given a set of APIs and complexity requirements, design the class’s internal data structures so that all operations execute correctly and efficiently.

ElementDescription
InputConstructor parameters and a sequence of method calls
OutputEach method’s return value and the object’s state changes
Core constraintOperation complexities such as get, put, push, pop
Typical problemsMin stack, hash set, LRU cache, LFU cache

Core Principle: Step-by-Step Illustration#

Taking the LRU cache as an example, the hash table handles O(1) node lookup, while the doubly linked list handles O(1) movement and eviction:

Map key -> Node head Recently used Older tail

On each get or update of an existing key, move the node to the head of the list; when capacity is exceeded, remove the least recently used node from the tail.


Detailed Algorithm Steps#

Algorithm: LRUCache.put(key, value)
Input: key and value
Output: none
1. If key already exists:
2. Update node value
3. Move node to the head of the linked list
4. Return
5. Create a new node and add it to the Map
6. Insert the new node at the head of the linked list
7. If capacity is exceeded:
8. Delete the tail node of the linked list
9. Delete the corresponding key from the Map

Complexity analysis:

StructureOperationTime complexitySpace complexity
Min stackpush/pop/top/getMinO(1)O(n)
Hash setadd/remove/containsAverage O(1)O(n)
LRU cacheget/putO(1)O(capacity)
LFU cacheget/putO(1) amortizedO(capacity)

TypeScript Implementation#

1. Min Stack#

class MinStack {
private stack: number[] = [];
private minStack: number[] = [];
push(value: number): void {
this.stack.push(value);
const min = this.minStack.length === 0
? value
: Math.min(value, this.minStack[this.minStack.length - 1]);
this.minStack.push(min);
}
pop(): void {
this.stack.pop();
this.minStack.pop();
}
top(): number {
return this.stack[this.stack.length - 1];
}
getMin(): number {
return this.minStack[this.minStack.length - 1];
}
}

2. Hash Set#

class MyHashSet {
private readonly size = 769;
private buckets: number[][] = Array.from({ length: 769 }, () => []);
private hash(key: number): number {
return key % this.size;
}
add(key: number): void {
const bucket = this.buckets[this.hash(key)];
if (!bucket.includes(key)) bucket.push(key);
}
remove(key: number): void {
const bucket = this.buckets[this.hash(key)];
const index = bucket.indexOf(key);
if (index !== -1) bucket.splice(index, 1);
}
contains(key: number): boolean {
return this.buckets[this.hash(key)].includes(key);
}
}

3. LRU Node#

class DLinkedNode {
key: number;
value: number;
prev: DLinkedNode | null = null;
next: DLinkedNode | null = null;
constructor(key: number, value: number) {
this.key = key;
this.value = value;
}
}

4. LRU Cache#

class LRUCache {
private cache = new Map<number, DLinkedNode>();
private head = new DLinkedNode(0, 0);
private tail = new DLinkedNode(0, 0);
constructor(private capacity: number) {
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: number): number {
const node = this.cache.get(key);
if (!node) return -1;
this.moveToHead(node);
return node.value;
}
put(key: number, value: number): void {
const existing = this.cache.get(key);
if (existing) {
existing.value = value;
this.moveToHead(existing);
return;
}
const node = new DLinkedNode(key, value);
this.cache.set(key, node);
this.addToHead(node);
if (this.cache.size > this.capacity) {
const removed = this.removeTail();
this.cache.delete(removed.key);
}
}
private addToHead(node: DLinkedNode): void {
node.prev = this.head;
node.next = this.head.next;
this.head.next!.prev = node;
this.head.next = node;
}
private removeNode(node: DLinkedNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private moveToHead(node: DLinkedNode): void {
this.removeNode(node);
this.addToHead(node);
}
private removeTail(): DLinkedNode {
const node = this.tail.prev!;
this.removeNode(node);
return node;
}
}

5. Simplified LFU Cache Implementation#

interface LFUNode {
key: number;
value: number;
freq: number;
}
class LFUCache {
private minFreq = 0;
private keyToNode = new Map<number, LFUNode>();
private freqToKeys = new Map<number, Set<number>>();
constructor(private capacity: number) {}
get(key: number): number {
const node = this.keyToNode.get(key);
if (!node) return -1;
this.increaseFreq(key);
return node.value;
}
put(key: number, value: number): void {
if (this.capacity === 0) return;
const node = this.keyToNode.get(key);
if (node) {
node.value = value;
this.increaseFreq(key);
return;
}
if (this.keyToNode.size >= this.capacity) {
const evictKey = this.freqToKeys.get(this.minFreq)!.values().next().value;
this.freqToKeys.get(this.minFreq)!.delete(evictKey);
this.keyToNode.delete(evictKey);
}
this.keyToNode.set(key, { key, value, freq: 1 });
if (!this.freqToKeys.has(1)) this.freqToKeys.set(1, new Set());
this.freqToKeys.get(1)!.add(key);
this.minFreq = 1;
}
private increaseFreq(key: number): void {
const node = this.keyToNode.get(key)!;
const oldKeys = this.freqToKeys.get(node.freq)!;
oldKeys.delete(key);
if (oldKeys.size === 0 && node.freq === this.minFreq) {
this.minFreq++;
}
node.freq++;
if (!this.freqToKeys.has(node.freq)) this.freqToKeys.set(node.freq, new Set());
this.freqToKeys.get(node.freq)!.add(key);
}
}

Engineering Optimization: Managing State with Invariants#

Design ProblemCore InvariantAuxiliary Structure
Min StackminStack[i] stores the minimum of the first i levelsAuxiliary stack
Hash SetNo duplicate keys within the same bucketBucket array
LRUHead of the linked list is most recent, tail is oldestMap + doubly linked list
LFUminFreq always points to the minimum access frequencykeyMap + freqMap

When solving design problems, first list the APIs, then list the invariants that must be maintained after each API—this is more reliable than writing code directly.


Applications and Limitations#

Typical Applications#

  • Cache eviction policies: LRU, LFU
  • Enhanced stack/queue structures: min stack, monotonic queue
  • Hash structures: sets, maps, counters
  • Business object state machines and resource pool management

Limitations#

LimitationDescription
High state couplingMultiple structures must be updated in sync
Many edge casesEmpty capacity, duplicate keys, and deleting the tail node must all remain consistent
High implementation complexityO(1) requirements typically require combining multiple structures

Summary#

Explicit API Analyze complexity requirements Choose the underlying structure Define invariants Implement and maintain state synchronization

Key points:

  1. The core of a design problem is to combine existing data structures to create new operation semantics.
  2. Each method must individually satisfy the complexity requirements.
  3. A Map handles fast lookup, while a linked list handles fast reordering—a common combination for cache problems.
  4. Internal invariants matter more than code templates.
Design Problems: From Design Patterns to High-Frequency Data Structure Design
https://www.hehonglei.com/technology/design/
Author
HHLBlog
Published at
2026-04-26
License
CC BY-NC-SA 4.0