Data Structure Designs
Overview
Data structure design problems begin with an interface and a performance contract. Before choosing an implementation, list every operation, its required complexity, and the information it must answer. The final structure is often a combination of standard structures rather than a new structure built from scratch.
The most important concept is the invariant connecting those structures. If an array stores values and a hash map stores their indexes, every swap must update the map. If a map points to nodes in a linked list, removing a node must update both representations. If a heap contains historical entries, a separate map must identify which entry is still current.
Recurring patterns in this post include:
- Hash map plus dense array: the array provides indexing or random access, while the map locates an item in
O(1). Swap-with-last removal keeps the array dense. - Hash map plus doubly linked list: the map locates a node, while the list maintains recency or ordered buckets. Sentinel nodes eliminate most boundary cases.
- Two containers or a circular array: two stacks reverse order lazily, while head, tail, and size indexes implement a bounded queue without shifting elements.
- Buffered or flattened iterators: cache the next item for peeking, or flatten nested input before iteration when eager
O(n)preprocessing is acceptable. - Timestamped or versioned histories: append changes in chronological order and use binary search to retrieve the latest value at or before a requested time.
- Frequency buckets: group values by frequency so the current minimum, maximum, or most recent value at a frequency can be found directly.
- Intervals: represent free space with half-open ranges, allocate from a fitting interval, and merge touching ranges after memory is released.
- Authoritative map plus lazy heap: update the map immediately, push new heap entries, and discard stale entries only when they reach the top.
Most design bugs occur during mutation, when two internal representations briefly disagree. Write each modifying operation as a sequence that restores every invariant before returning. Track logical state such as size explicitly instead of inferring it from stored values, and distinguish worst-case from amortized complexity: lazy stack transfers and stale-heap cleanup may occasionally do extra work, but each old entry is processed only a limited number of times.
Templates
There is no universal data-structure-design template. The following patterns capture the most reusable combinations in this post.
Hash map plus dense array
- Keep values in a dense array and map each value to its current index.
- Insert at the end and record the new index.
- To remove a value, move the final value into its index, update the moved value’s index, and pop the last slot.
- Never mutate the array without making the corresponding map update.
import java.util.*;
class DenseIndexTemplate {
private final List<Integer> values = new ArrayList<>();
private final Map<Integer, Integer> valueToIndex = new HashMap<>();
public boolean add(int value) {
if (valueToIndex.containsKey(value)) {
return false;
}
valueToIndex.put(value, values.size());
values.add(value);
return true;
}
public boolean remove(int value) {
if (!valueToIndex.containsKey(value)) {
return false;
}
int index = valueToIndex.get(value);
int lastIndex = values.size() - 1;
int lastValue = values.get(lastIndex);
values.set(index, lastValue);
valueToIndex.put(lastValue, index);
values.remove(lastIndex);
valueToIndex.remove(value);
return true;
}
} class DenseIndexTemplate:
def __init__(self):
self.values = []
self.value_to_index = {}
def add(self, value: int) -> bool:
if value in self.value_to_index:
return False
self.value_to_index[value] = len(self.values)
self.values.append(value)
return True
def remove(self, value: int) -> bool:
if value not in self.value_to_index:
return False
index = self.value_to_index[value]
last_value = self.values[-1]
self.values[index] = last_value
self.value_to_index[last_value] = index
self.values.pop()
del self.value_to_index[value]
return True Hash map plus doubly linked ordering
- Map each key directly to its linked-list node.
- Use head and tail sentinels so insertion and removal never need special endpoint logic.
- Reorder an existing key by detaching its node and inserting it at the desired position.
- When evicting a node, remove it from both the list and the map.
import java.util.HashMap;
import java.util.Map;
class LinkedOrderNode {
int key;
LinkedOrderNode prev;
LinkedOrderNode next;
LinkedOrderNode(int key) {
this.key = key;
}
}
class LinkedOrderTemplate {
private final Map<Integer, LinkedOrderNode> nodes = new HashMap<>();
private final LinkedOrderNode head = new LinkedOrderNode(-1);
private final LinkedOrderNode tail = new LinkedOrderNode(-1);
public LinkedOrderTemplate() {
head.next = tail;
tail.prev = head;
}
public void touch(int key) {
LinkedOrderNode node = nodes.get(key);
if (node == null) {
node = new LinkedOrderNode(key);
nodes.put(key, node);
} else {
removeNode(node);
}
insertAfter(head, node);
}
public int removeLast() {
if (tail.prev == head) {
return -1;
}
LinkedOrderNode node = tail.prev;
removeNode(node);
nodes.remove(node.key);
return node.key;
}
private void removeNode(LinkedOrderNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void insertAfter(LinkedOrderNode prevNode, LinkedOrderNode node) {
LinkedOrderNode nextNode = prevNode.next;
prevNode.next = node;
node.prev = prevNode;
node.next = nextNode;
nextNode.prev = node;
}
} class LinkedOrderNode:
def __init__(self, key: int):
self.key = key
self.prev = None
self.next = None
class LinkedOrderTemplate:
def __init__(self):
self.nodes = {}
self.head = LinkedOrderNode(-1)
self.tail = LinkedOrderNode(-1)
self.head.next = self.tail
self.tail.prev = self.head
def touch(self, key: int) -> None:
node = self.nodes.get(key)
if node is None:
node = LinkedOrderNode(key)
self.nodes[key] = node
else:
self._remove_node(node)
self._insert_after(self.head, node)
def remove_last(self) -> int:
if self.tail.prev == self.head:
return -1
node = self.tail.prev
self._remove_node(node)
del self.nodes[node.key]
return node.key
def _remove_node(self, node) -> None:
node.prev.next = node.next
node.next.prev = node.prev
def _insert_after(self, prev_node, node) -> None:
next_node = prev_node.next
prev_node.next = node
node.prev = prev_node
node.next = next_node
next_node.prev = node Authoritative map plus lazy heap
- Store the current priority for every active ID in a map.
- On an add or edit, update the map and push a new heap record; do not search the heap for the old record.
- Remove an item by deleting it from the map.
- When popping, discard records whose ID is absent or whose priority no longer matches the map.
import java.util.*;
class LazyHeapTemplate {
private final Map<Integer, Integer> livePriority = new HashMap<>();
private final PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> {
if (a[0] != b[0]) {
return Integer.compare(b[0], a[0]);
}
return Integer.compare(b[1], a[1]);
});
public void upsert(int id, int priority) {
livePriority.put(id, priority);
heap.offer(new int[]{priority, id});
}
public void remove(int id) {
livePriority.remove(id);
}
public int popTop() {
while (!heap.isEmpty()) {
int[] entry = heap.poll();
int priority = entry[0];
int id = entry[1];
if (!livePriority.containsKey(id) || livePriority.get(id) != priority) {
continue;
}
livePriority.remove(id);
return id;
}
return -1;
}
} import heapq
class LazyHeapTemplate:
def __init__(self):
self.live_priority = {}
self.heap = [] # (-priority, -id)
def upsert(self, id: int, priority: int) -> None:
self.live_priority[id] = priority
heapq.heappush(self.heap, (-priority, -id))
def remove(self, id: int) -> None:
self.live_priority.pop(id, None)
def pop_top(self) -> int:
while self.heap:
neg_priority, neg_id = heapq.heappop(self.heap)
priority = -neg_priority
id = -neg_id
if id not in self.live_priority or self.live_priority[id] != priority:
continue
del self.live_priority[id]
return id
return -1 Append-only history plus binary search
- Keep a sorted history of
(version, value)records for each key or index. - Append changes when versions arrive in nondecreasing order.
- Search for the first version greater than the requested version.
- Return the record immediately before that boundary.
import java.util.*;
class VersionedHistoryTemplate {
private final Map<Integer, List<int[]>> history = new HashMap<>();
public void set(int key, int version, int value) {
history.computeIfAbsent(key, ignored -> new ArrayList<>())
.add(new int[]{version, value});
}
public int get(int key, int version) {
if (!history.containsKey(key)) {
return 0;
}
List<int[]> entries = history.get(key);
int left = 0;
int right = entries.size();
while (left < right) {
int mid = (left + right) / 2;
if (entries.get(mid)[0] <= version) {
left = mid + 1;
} else {
right = mid;
}
}
return left == 0 ? 0 : entries.get(left - 1)[1];
}
} from bisect import bisect_left
from collections import defaultdict
class VersionedHistoryTemplate:
def __init__(self):
self.history = defaultdict(list)
def set(self, key: int, version: int, value: int) -> None:
self.history[key].append((version, value))
def get(self, key: int, version: int) -> int:
if key not in self.history:
return 0
entries = self.history[key]
index = bisect_left(entries, version + 1, key = lambda entry: entry[0])
return 0 if index == 0 else entries[index-1][1] Problems
146. LRU Cache
Combine a key-to-node hash map with a doubly linked list ordered by recency. The node after head is most recently used, and the node before tail is least recently used. A successful get or an existing-key put moves its node to the front; inserting a new key into a full cache evicts the last node before adding the replacement, so the cache never exceeds capacity. Both operations run in O(1).
import java.util.HashMap;
import java.util.Map;
class ListNode {
int key;
int val;
ListNode prev;
ListNode next;
ListNode(int key, int val) {
this.key = key;
this.val = val;
}
}
class LRUCache {
private final int capacity;
private final ListNode head;
private final ListNode tail;
private final Map<Integer, ListNode> cache;
public LRUCache(int capacity) {
this.capacity = capacity;
this.head = new ListNode(0, 0);
this.tail = new ListNode(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
this.cache = new HashMap<>();
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
ListNode node = cache.get(key);
removeNode(node);
addFront(node);
return node.val;
}
public void put(int key, int value) {
if (cache.size() == capacity && !cache.containsKey(key)) {
// Evict before insertion so the cache never exceeds its capacity.
ListNode lastNode = tail.prev;
int lastKey = lastNode.key;
removeNode(lastNode);
cache.remove(lastKey);
}
ListNode node;
if (!cache.containsKey(key)) {
node = new ListNode(key, value);
cache.put(key, node);
} else {
node = cache.get(key);
node.val = value;
removeNode(node);
}
addFront(node);
}
private void removeNode(ListNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
node.next = null;
node.prev = null;
}
private void addFront(ListNode node) {
ListNode tmp = head.next;
head.next = node;
node.prev = head;
node.next = tmp;
tmp.prev = node;
}
} class ListNode:
def __init__(self, key, val):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.head = ListNode(0, 0)
self.tail = ListNode(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
self.cache = {}
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
self.removeNode(node)
self.addFront(node)
return node.val
def put(self, key: int, value: int) -> None:
if len(self.cache) == self.capacity and key not in self.cache:
# evict last node
last_node = self.tail.prev
last_key = last_node.key
self.removeNode(last_node)
del self.cache[last_key]
if key not in self.cache:
node = ListNode(key, value)
self.cache[key] = node
else:
node = self.cache[key]
node.val = value
self.removeNode(node)
self.addFront(node)
def removeNode(self, node: ListNode) -> None:
node.prev.next = node.next
node.next.prev = node.prev
node.next = None
node.prev = None
def addFront(self, node: ListNode) -> None:
tmp = self.head.next
self.head.next = node
node.prev = self.head
node.next = tmp
tmp.prev = node 232. Implement Queue Using Stacks
Use one stack for newly pushed values and another stack for values ready to leave the queue. Transfer all values from the input stack only when the output stack is empty; the reversal exposes the oldest value. Each value is pushed and transferred at most once before removal, giving amortized O(1) queue operations.
import java.util.ArrayDeque;
import java.util.Deque;
class MyQueue {
private final Deque<Integer> stack;
private final Deque<Integer> queue;
public MyQueue() {
this.stack = new ArrayDeque<>(); // push to stack
this.queue = new ArrayDeque<>(); // pop from queue
}
public void push(int x) {
stack.offerLast(x);
}
public int pop() {
if (queue.isEmpty()) {
transfer();
}
return queue.pollLast();
}
public int peek() {
if (queue.isEmpty()) {
transfer();
}
return queue.peekLast();
}
private void transfer() {
while (!stack.isEmpty()) {
queue.offerLast(stack.pollLast());
}
}
public boolean empty() {
return stack.isEmpty() && queue.isEmpty();
}
} class MyQueue:
def __init__(self):
self.stack = [] # push to stack
self.queue = [] # pop from queue
def push(self, x: int) -> None:
self.stack.append(x)
def pop(self) -> int:
if len(self.queue) == 0:
self._transfer()
return self.queue.pop()
def peek(self) -> int:
if len(self.queue) == 0:
self._transfer()
return self.queue[-1]
def _transfer(self):
while self.stack:
self.queue.append(self.stack.pop())
def empty(self) -> bool:
return len(self.stack) + len(self.queue) == 0 284. Peeking Iterator
Prefetch one value from the wrapped iterator and store it in next_val with a separate availability flag. peek returns the buffered value without advancing, while next returns it and immediately refills the buffer. Every operation uses O(1) time and the wrapper stores only one extra value.
import java.util.Iterator;
class PeekingIterator implements Iterator<Integer> {
private final Iterator<Integer> iterator;
// Do not name instance variables the same as methods.
private Integer nextVal;
private boolean hasNext;
public PeekingIterator(Iterator<Integer> iterator) {
this.iterator = iterator;
this.nextVal = null;
this.hasNext = false;
if (this.iterator.hasNext()) {
this.nextVal = this.iterator.next();
this.hasNext = true;
}
}
public Integer peek() {
return nextVal;
}
@Override
public Integer next() {
if (!hasNext) {
return null;
}
int val = nextVal;
if (iterator.hasNext()) {
nextVal = iterator.next();
hasNext = true;
} else {
nextVal = null;
hasNext = false;
}
return val;
}
@Override
public boolean hasNext() {
return hasNext;
}
} class PeekingIterator:
def __init__(self, iterator):
"""
Initialize your data structure here.
:type iterator: Iterator
"""
self.iter = iterator
# Do not name instance variables the same as methods!! Instance variable wins.
self.next_val = None
self.has_next = False
if self.iter.hasNext():
self.next_val = self.iter.next()
self.has_next = True
def peek(self):
"""
Returns the next element in the iteration without advancing the iterator.
:rtype: int
"""
return self.next_val
def next(self):
"""
:rtype: int
"""
if self.has_next == False:
return None
val = self.next_val
if self.iter.hasNext():
self.next_val = self.iter.next()
self.has_next = True
else:
self.next_val = None
self.has_next = False
return val
def hasNext(self):
"""
:rtype: bool
"""
return self.has_next 341. Flatten Nested List Iterator
This solution eagerly traverses the nested list with DFS and stores every integer in left-to-right order. The constructor takes O(n) time and space for n integers, after which next and hasNext are O(1). This favors simple iteration over a lazy iterator that would flatten values only when requested.
import java.util.*;
class NestedIterator implements Iterator<Integer> {
private final List<Integer> vals;
private int idx;
private final int size;
public NestedIterator(List<NestedInteger> nestedList) {
this.vals = new ArrayList<>();
dfs(nestedList);
this.idx = 0;
this.size = vals.size();
}
private void dfs(List<NestedInteger> nList) {
for (NestedInteger n : nList) {
if (n.isInteger()) {
vals.add(n.getInteger());
} else {
dfs(n.getList());
}
}
}
@Override
public Integer next() {
int val = vals.get(idx);
idx += 1;
return val;
}
@Override
public boolean hasNext() {
if (idx < size) {
return true;
} else {
return false;
}
}
} class NestedIterator:
def __init__(self, nestedList: [NestedInteger]):
self.vals = []
self._dfs(nestedList)
self.idx = 0
self.size = len(self.vals)
def _dfs(self, nList: [NestedInteger]):
for n in nList:
if n.isInteger():
self.vals.append(n.getInteger())
else:
self._dfs(n.getList())
def next(self) -> int:
val = self.vals[self.idx]
self.idx += 1
return val
def hasNext(self) -> bool:
if self.idx < self.size:
return True
else:
return False 362. Design Hit Counter
Group hits with the same timestamp into one deque bucket and maintain a running total. During getHits(timestamp), remove buckets at or before timestamp - 300 and subtract their counts. Each bucket enters and leaves the deque once, so hit recording is O(1) and retrieval is amortized O(1).
import java.util.ArrayDeque;
import java.util.Deque;
class HitCounter {
private final Deque<int[]> dq;
private int total;
public HitCounter() {
this.dq = new ArrayDeque<>();
this.total = 0;
}
public void hit(int timestamp) {
if (!dq.isEmpty() && timestamp == dq.peekLast()[0]) {
dq.peekLast()[1] += 1;
} else {
dq.offerLast(new int[]{timestamp, 1});
}
total += 1;
}
public int getHits(int timestamp) {
while (!dq.isEmpty() && dq.peekFirst()[0] <= timestamp - 300) {
int[] previous = dq.pollFirst();
int prevCount = previous[1];
total -= prevCount;
}
return total;
}
} import collections
class HitCounter:
def __init__(self):
self.dq = collections.deque()
self.total = 0
def hit(self, timestamp: int) -> None:
if self.dq and timestamp == self.dq[-1][0]:
self.dq[-1][1] += 1
else:
self.dq.append([timestamp, 1])
self.total += 1
def getHits(self, timestamp: int) -> int:
while self.dq and self.dq[0][0] <= timestamp - 300:
prev_time, prev_count = self.dq.popleft()
self.total -= prev_count
return self.total 380. Insert Delete GetRandom O(1)
Store values in a dense array for uniform random indexing and map each value to its array index. Removal swaps the target with the final value, updates the moved value’s index, and pops the last slot. This synchronization gives average O(1) insertion, removal, and random selection.
import java.util.*;
class RandomizedSet {
private final Map<Integer, Integer> valToIdx;
private final List<Integer> values;
private final Random random;
public RandomizedSet() {
this.valToIdx = new HashMap<>();
this.values = new ArrayList<>();
this.random = new Random();
}
public boolean insert(int val) {
if (valToIdx.containsKey(val)) {
return false;
} else {
valToIdx.put(val, values.size());
values.add(val);
return true;
}
}
public boolean remove(int val) {
if (!valToIdx.containsKey(val)) {
return false;
} else {
int idx = valToIdx.get(val);
int lastIdx = values.size() - 1;
int lastVal = values.get(lastIdx);
values.set(idx, lastVal);
values.set(lastIdx, val);
valToIdx.put(lastVal, idx);
values.remove(lastIdx);
valToIdx.remove(val);
return true;
}
}
public int getRandom() {
int N = values.size();
return values.get(random.nextInt(N));
}
} import random
class RandomizedSet:
def __init__(self):
self.val_to_idx = {}
self.values = []
def insert(self, val: int) -> bool:
if val in self.val_to_idx:
return False
else:
self.val_to_idx[val] = len(self.values)
self.values.append(val)
return True
def remove(self, val: int) -> bool:
if val not in self.val_to_idx:
return False
else:
idx = self.val_to_idx[val]
last_idx, last_val = len(self.values) - 1, self.values[-1]
self.values[idx], self.values[last_idx] = last_val, val
self.val_to_idx[last_val] = idx
self.values.pop()
del self.val_to_idx[val]
return True
def getRandom(self) -> int:
N = len(self.values)
return self.values[random.randint(0, N-1)] 432. All O`one Data Structure
Maintain a doubly linked list of frequency buckets in increasing count order. Each bucket stores a set of keys, while a hash map points every key directly to its current bucket. Incrementing or decrementing moves a key only to an adjacent bucket, empty buckets are removed, and the first and last buckets expose a minimum or maximum key in O(1).
import java.util.*;
class Node {
int count;
Set<String> keys;
Node prev;
Node next;
Node(int count) {
this.count = count;
this.keys = new HashSet<>();
}
}
class AllOne {
private final Map<String, Node> keyToNode;
private final Node head;
private final Node tail;
public AllOne() {
this.keyToNode = new HashMap<>();
this.head = new Node(-1);
this.tail = new Node(-1);
this.head.next = this.tail;
this.tail.prev = this.head;
}
public void inc(String key) {
if (!keyToNode.containsKey(key)) {
Node firstNode = head.next;
if (firstNode.count == 1) {
firstNode.keys.add(key);
keyToNode.put(key, firstNode);
} else {
Node newNode = new Node(1);
newNode.keys.add(key);
insertAfter(head, newNode);
keyToNode.put(key, newNode);
}
return;
}
// Handle an existing key below.
Node keyNode = keyToNode.get(key);
int keyCount = keyNode.count;
Node nextNode = keyNode.next;
keyNode.keys.remove(key);
if (keyNode.keys.isEmpty()) {
remove(keyNode);
}
if (nextNode.count == keyCount + 1) {
nextNode.keys.add(key);
keyToNode.put(key, nextNode);
} else {
Node newNode = new Node(keyCount + 1);
newNode.keys.add(key);
insertAfter(nextNode.prev, newNode);
keyToNode.put(key, newNode);
}
}
public void dec(String key) {
Node keyNode = keyToNode.get(key);
int keyCount = keyNode.count;
Node prevNode = keyNode.prev;
keyNode.keys.remove(key);
if (keyNode.keys.isEmpty()) {
remove(keyNode);
}
if (keyCount == 1) {
keyToNode.remove(key);
return;
}
if (prevNode.count == keyCount - 1) {
prevNode.keys.add(key);
keyToNode.put(key, prevNode);
} else {
Node newNode = new Node(keyCount - 1);
newNode.keys.add(key);
insertAfter(prevNode, newNode);
keyToNode.put(key, newNode);
}
}
public String getMaxKey() {
if (tail.prev != head) {
return tail.prev.keys.iterator().next();
} else {
return "";
}
}
public String getMinKey() {
if (head.next != tail) {
return head.next.keys.iterator().next();
} else {
return "";
}
}
private void remove(Node node) {
Node prevNode = node.prev;
Node nextNode = node.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
node.prev = null;
node.next = null;
}
private void insertAfter(Node prevNode, Node node) {
Node nextNode = prevNode.next;
prevNode.next = node;
node.prev = prevNode;
node.next = nextNode;
nextNode.prev = node;
}
} class Node:
def __init__(self, count = 0):
self.count = count
self.keys = set()
self.prev = None
self.next = None
class AllOne:
def __init__(self):
self.key_to_node = {}
self.head = Node(-1)
self.tail = Node(-1)
self.head.next = self.tail
self.tail.prev = self.head
def inc(self, key: str) -> None:
if key not in self.key_to_node:
first_node = self.head.next
if first_node.count == 1:
first_node.keys.add(key)
self.key_to_node[key] = first_node
else:
new_node = Node(1)
new_node.keys.add(key)
self._insertAfter(self.head, new_node)
self.key_to_node[key] = new_node
return
# handle key exists below
key_node = self.key_to_node[key]
key_count = key_node.count
next_node = key_node.next
key_node.keys.remove(key)
if len(key_node.keys) == 0:
self._remove(key_node)
if next_node.count == 1 + key_count:
next_node.keys.add(key)
self.key_to_node[key] = next_node
else:
new_node = Node(1 + key_count)
new_node.keys.add(key)
self._insertAfter(next_node.prev, new_node)
self.key_to_node[key] = new_node
def dec(self, key: str) -> None:
key_node = self.key_to_node[key]
key_count = key_node.count
prev_node = key_node.prev
key_node.keys.remove(key)
if len(key_node.keys) == 0:
self._remove(key_node)
if key_count == 1:
del self.key_to_node[key]
return
if prev_node.count == key_count - 1:
prev_node.keys.add(key)
self.key_to_node[key] = prev_node
else:
new_node = Node(key_count-1)
new_node.keys.add(key)
self._insertAfter(prev_node, new_node)
self.key_to_node[key] = new_node
def getMaxKey(self) -> str:
if self.tail.prev != self.head:
return next(iter(self.tail.prev.keys))
else:
return ""
def getMinKey(self) -> str:
if self.head.next != self.tail:
return next(iter(self.head.next.keys))
else:
return ""
def _remove(self, node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
node.prev = None
node.next = None
def _insertAfter(self, prev_node, node):
next_node = prev_node.next
prev_node.next = node
node.prev = prev_node
node.next = next_node
next_node.prev = node 622. Design Circular Queue
Use a fixed array with head as the next dequeue index, tail as the next enqueue index, and modulo arithmetic to wrap both indexes. An explicit size determines whether the queue is empty or full; stored values such as 0 or -1 never indicate occupancy. Every operation runs in O(1).
class MyCircularQueue {
private int[] dq;
int capacity;
int size;
int head; // where to dequeue
int tail; // where to enqueue
public MyCircularQueue(int k) {
dq = new int[k];
size = 0;
capacity = k;
head = 0;
tail = 0;
}
public boolean enQueue(int value) {
if (isFull()) {
return false;
}
dq[tail] = value;
tail = (1 + tail) % capacity;
size++;
return true;
}
public boolean deQueue() {
if (isEmpty()) {
return false;
}
// int val = dq[head];
dq[head] = -1;
head = (1 + head) % capacity;
size--;
return true;
}
public int Front() {
if (isEmpty()) {
return -1;
}
return dq[head];
}
public int Rear() {
if (isEmpty()) {
return -1;
}
return dq[(tail - 1 + capacity) % capacity];
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
} class MyCircularQueue:
def __init__(self, k: int):
self.dq = [0] * k
self.size = 0
self.capacity = k
self.head = 0 # where to dequeue
self.tail = 0 # where to enqueue
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.dq[self.tail] = value
self.tail = (1 + self.tail) % self.capacity
self.size += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
self.dq[self.head] = -1
self.head = (1 + self.head) % self.capacity
self.size -= 1
return True
def Front(self) -> int:
if self.isEmpty():
return -1
return self.dq[self.head]
def Rear(self) -> int:
if self.isEmpty():
return -1
return self.dq[(self.tail - 1 + self.capacity) % self.capacity]
def isEmpty(self) -> bool:
return self.size == 0
def isFull(self) -> bool:
return self.size == self.capacity 635. Design Log Storage System
Keep (timestamp, ID) records sorted lexicographically. For a query, truncate both endpoints at the requested granularity, append minimum components to the start and maximum components to the end, then use binary search to isolate the inclusive range. Array insertion costs O(n), while retrieval costs O(log n + k) for k returned IDs.
import java.util.*;
class LogEntry {
String timestamp;
int id;
LogEntry(String timestamp, int id) {
this.timestamp = timestamp;
this.id = id;
}
}
class LogSystem {
private final List<LogEntry> data;
private final Map<String, Integer> lastIdx;
private final Map<String, String> minSuffix;
private final Map<String, String> maxSuffix;
public LogSystem() {
this.data = new ArrayList<>();
this.lastIdx = Map.of(
"Year", 4,
"Month", 7,
"Day", 10,
"Hour", 13,
"Minute", 16,
"Second", 19
);
this.minSuffix = Map.of(
"Year", ":00:00:00:00:00",
"Month", ":00:00:00:00",
"Day", ":00:00:00",
"Hour", ":00:00",
"Minute", ":00",
"Second", ""
);
this.maxSuffix = Map.of(
"Year", ":99:99:99:99:99",
"Month", ":99:99:99:99",
"Day", ":99:99:99",
"Hour", ":99:99",
"Minute", ":99",
"Second", ""
);
}
public void put(int id, String timestamp) {
int left = 0;
int right = data.size();
while (left < right) {
int mid = (left + right) / 2;
LogEntry curr = data.get(mid);
int comparison = curr.timestamp.compareTo(timestamp);
if (comparison < 0 || (comparison == 0 && curr.id <= id)) {
left = mid + 1;
} else {
right = mid;
}
}
data.add(left, new LogEntry(timestamp, id));
}
public List<Integer> retrieve(String start, String end, String granularity) {
int last = lastIdx.get(granularity);
String startStr = start.substring(0, last) + minSuffix.get(granularity);
String endStr = end.substring(0, last) + maxSuffix.get(granularity);
int startIdx = lowerBound(startStr);
if (startIdx == data.size()) {
return new ArrayList<>();
}
int endIdx = upperBound(endStr);
List<Integer> res = new ArrayList<>();
for (int i = startIdx; i < endIdx; i++) {
res.add(data.get(i).id);
}
return res;
}
private int lowerBound(String target) {
int left = 0;
int right = data.size();
while (left < right) {
int mid = (left + right) / 2;
if (data.get(mid).timestamp.compareTo(target) < 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
private int upperBound(String target) {
int left = 0;
int right = data.size();
while (left < right) {
int mid = (left + right) / 2;
if (data.get(mid).timestamp.compareTo(target) <= 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
} import bisect
from typing import List
class LogSystem:
def __init__(self):
self.data = []
self.last_idx = {
"Year" : 4,
"Month" : 7,
"Day" : 10,
"Hour" : 13,
"Minute" : 16,
"Second" : 19
}
self.min_suffix = {
"Year" : ":00:00:00:00:00",
"Month" : ":00:00:00:00",
"Day" : ":00:00:00",
"Hour" : ":00:00",
"Minute" : ":00",
"Second" : ""
}
self.max_suffix = {
"Year" : ":99:99:99:99:99",
"Month" : ":99:99:99:99",
"Day" : ":99:99:99",
"Hour" : ":99:99",
"Minute" : ":99",
"Second" : ""
}
def put(self, id: int, timestamp: str) -> None:
bisect.insort(self.data, (timestamp, id))
def retrieve(self, start: str, end: str, granularity: str) -> List[int]:
start_str = start[:self.last_idx[granularity]] + self.min_suffix[granularity]
end_str = end[:self.last_idx[granularity]] + self.max_suffix[granularity]
start_idx = bisect.bisect_left(self.data, start_str, key = lambda x : x[0])
if start_idx == len(self.data):
return []
end_idx = bisect.bisect_right(self.data, end_str, key = lambda x : x[0])
return [idx for _, idx in self.data[start_idx:end_idx]] 895. Maximum Frequency Stack
Map each value to its current frequency and each frequency to a stack of values that reached it in push order. max_freq identifies the stack to pop, and that stack automatically resolves ties by recency. Both push and pop run in O(1).
import java.util.*;
class FreqStack {
private final Map<Integer, Deque<Integer>> freqToStack;
private final Map<Integer, Integer> numToFreq;
private int maxFreq;
public FreqStack() {
// frequency to a stack of numbers
this.freqToStack = new HashMap<>();
this.numToFreq = new HashMap<>();
this.maxFreq = 0;
}
public void push(int val) {
int valFreq = 1 + numToFreq.getOrDefault(val, 0);
numToFreq.put(val, valFreq);
freqToStack.computeIfAbsent(valFreq, ignored -> new ArrayDeque<>()).offerLast(val);
maxFreq = Math.max(maxFreq, valFreq);
}
public int pop() {
Deque<Integer> stack = freqToStack.get(maxFreq);
int num = stack.pollLast();
int numFreq = maxFreq - 1;
if (stack.isEmpty()) {
freqToStack.remove(maxFreq);
maxFreq -= 1;
}
numToFreq.put(num, numFreq);
return num;
}
} from collections import defaultdict
class FreqStack:
def __init__(self):
# freq to a stack of nums
self.freq_to_stack = defaultdict(list)
self.num_to_freq = defaultdict(int)
self.max_freq = 0
def push(self, val: int) -> None:
val_freq = 1+self.num_to_freq[val]
self.num_to_freq[val] = val_freq
self.freq_to_stack[val_freq].append(val)
self.max_freq = max(self.max_freq, val_freq)
def pop(self) -> int:
num = self.freq_to_stack[self.max_freq].pop()
num_freq = self.max_freq - 1
if len(self.freq_to_stack[self.max_freq]) == 0:
del self.freq_to_stack[self.max_freq]
self.max_freq -= 1
self.num_to_freq[num] = num_freq
return num 981. Time Based Key-Value Store
Because timestamps for each key arrive in increasing order, append them and their values to parallel per-key arrays. A lookup uses bisect_left(timestamp + 1) to find the first later timestamp and returns the preceding value. set is O(1), and get is O(log n) for that key’s history.
import java.util.*;
class TimeMap {
private final Map<String, List<Integer>> keyToTime;
private final Map<String, List<String>> keyToValue;
public TimeMap() {
this.keyToTime = new HashMap<>();
this.keyToValue = new HashMap<>();
}
public void set(String key, String value, int timestamp) {
keyToTime.computeIfAbsent(key, ignored -> new ArrayList<>()).add(timestamp);
keyToValue.computeIfAbsent(key, ignored -> new ArrayList<>()).add(value);
}
public String get(String key, int timestamp) {
if (!keyToTime.containsKey(key)) {
return "";
}
List<Integer> times = keyToTime.get(key);
List<String> values = keyToValue.get(key);
// Using 1 + timestamp here saves many corner cases.
int idx = lowerBound(times, 1 + timestamp);
if (idx == 0) {
return "";
}
return values.get(idx - 1);
}
private int lowerBound(List<Integer> values, int target) {
int left = 0;
int right = values.size();
while (left < right) {
int mid = (left + right) / 2;
if (values.get(mid) < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
} import collections
from bisect import bisect_left
class TimeMap:
def __init__(self):
self.key_to_time = collections.defaultdict(list)
self.key_to_value = collections.defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.key_to_time[key].append(timestamp)
self.key_to_value[key].append(value)
def get(self, key: str, timestamp: int) -> str:
if key not in self.key_to_time:
return ""
times = self.key_to_time[key]
values = self.key_to_value[key]
N = len(times)
# using 1 + timestamp here saves lots of corner cases
idx = bisect_left(times, 1+timestamp)
if idx == 0:
return ""
return values[idx-1] 1146. Snapshot Array
Store an append-only history of (snapshot ID, value) pairs for each index rather than copying the entire array. set appends under the current snapshot, snap advances a global ID, and get uses binary search to find the latest record at or before the requested snapshot. Updates and snapshots are O(1), while retrieval is O(log u) for u updates at that index.
import java.util.ArrayList;
import java.util.List;
class SnapshotArray {
private int snapId;
// data[i] is a list of {snapId, value} pairs.
private final List<List<int[]>> data;
public SnapshotArray(int length) {
this.snapId = 0;
this.data = new ArrayList<>();
for (int i = 0; i < length; i++) {
List<int[]> history = new ArrayList<>();
history.add(new int[]{0, 0});
data.add(history);
}
}
public void set(int index, int val) {
data.get(index).add(new int[]{snapId, val});
}
public int snap() {
snapId += 1;
return snapId - 1;
}
public int get(int index, int snapId) {
List<int[]> history = data.get(index);
int ptr = lowerBound(history, 1 + snapId);
return history.get(ptr - 1)[1];
}
private int lowerBound(List<int[]> history, int target) {
int left = 0;
int right = history.size();
while (left < right) {
int mid = (left + right) / 2;
if (history.get(mid)[0] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
} from bisect import bisect_left
class SnapshotArray:
def __init__(self, length: int):
self.snap_id = 0
# data[i] is a list of (snap_id, val) tuple
self.data = [[(0, 0)] for _ in range(length)]
def set(self, index: int, val: int) -> None:
self.data[index].append((self.snap_id, val))
def snap(self) -> int:
self.snap_id += 1
return self.snap_id - 1
def get(self, index: int, snap_id: int) -> int:
history = self.data[index]
ptr = bisect_left(history, 1+snap_id, key = lambda x : x[0])
return history[ptr-1][1] 2502. Design Memory Allocator
Represent free memory as half-open intervals and keep every allocated interval grouped by mID. Allocation scans for the first fitting free range and consumes space from its left edge. Freeing an ID returns all of its intervals, then sorts and merges overlapping or touching ranges. With f free intervals, allocation is O(f) and merging is O(f log f).
import java.util.*;
class Allocator {
// Free intervals are half-open: [start, end).
private List<int[]> free;
private final Map<Integer, List<int[]>> used;
public Allocator(int n) {
this.free = new ArrayList<>();
this.free.add(new int[]{0, n});
this.used = new HashMap<>(); // mID -> list of [start, end)
}
public int allocate(int size, int mID) {
for (int idx = 0; idx < free.size(); idx++) {
int[] interval = free.get(idx);
int start = interval[0];
int end = interval[1];
if (end - start >= size) {
used.computeIfAbsent(mID, ignored -> new ArrayList<>())
.add(new int[]{start, start + size});
// It is fine to leave an empty range such as [5, 5).
free.set(idx, new int[]{start + size, end});
return start;
}
}
return -1;
}
public int freeMemory(int mID) {
// Check first.
if (!used.containsKey(mID)) {
return 0;
}
int total = 0;
for (int[] interval : used.get(mID)) {
int start = interval[0];
int end = interval[1];
total += end - start;
free.add(interval);
}
used.remove(mID);
free = merge(free);
return total;
}
private List<int[]> merge(List<int[]> intervals) {
intervals.sort((a, b) -> {
int startComparison = Integer.compare(a[0], b[0]);
return startComparison != 0
? startComparison
: Integer.compare(a[1], b[1]);
});
List<int[]> res = new ArrayList<>();
for (int[] interval : intervals) {
int start = interval[0];
int end = interval[1];
// Must be >, not >=.
if (res.isEmpty() || start > res.get(res.size() - 1)[1]) {
res.add(new int[]{start, end});
} else {
int[] previous = res.get(res.size() - 1);
previous[1] = Math.max(previous[1], end);
}
}
return res;
}
} from collections import defaultdict
class Allocator:
def __init__(self, n: int):
# Free intervals are half-open: [start, end)
self.free = [[0, n]]
self.used = defaultdict(list) # mID -> list of [start, end)
def allocate(self, size: int, mID: int) -> int:
for idx, interval in enumerate(self.free):
start, end = interval
if end-start >= size:
self.used[mID].append([start, start+size])
# It is fine to leave an empty range [5, 5]
self.free[idx] = [start+size, end]
return start
return -1
def freeMemory(self, mID: int) -> int:
# should check first
if mID not in self.used:
return 0
total = 0
for interval in self.used[mID]:
start, end = interval
total += end - start
self.free.append(interval)
del self.used[mID]
self.free = self._merge(self.free)
return total
def _merge(self, intervals):
res = []
for start, end in sorted(intervals):
# must be >, not >=
if len(res) == 0 or start > res[-1][1]:
res.append([start, end])
else:
res[-1][1] = max(res[-1][1], end)
return res 3408. Design Task Manager
Treat the hash map from task ID to current (priority, user ID) as authoritative, and keep (priority, task ID) snapshots in a max-heap. Adds and edits push a new snapshot, removals only delete from the map, and execTop discards heap records that no longer match current map state. This lazy deletion avoids arbitrary heap removal while preserving highest-priority and highest-task-ID ordering.
import java.util.*;
class TaskManager {
// taskId to [priority, userId]
private Map<Integer, int[]> taskInfo;
// [priority, taskId]
private PriorityQueue<int[]> heap;
public TaskManager(List<List<Integer>> tasks) {
taskInfo = new HashMap<>();
heap = new PriorityQueue<>((a, b) -> {
if (a[0] != b[0]) {
return b[0] - a[0];
} else {
return b[1] - a[1];
}
});
for (List<Integer> task : tasks) {
int userId = task.get(0);
int taskId = task.get(1);
int priority = task.get(2);
taskInfo.put(taskId, new int[]{priority, userId});
heap.offer(new int[]{priority, taskId});
}
}
public void add(int userId, int taskId, int priority) {
taskInfo.put(taskId, new int[]{priority, userId});
heap.offer(new int[]{priority, taskId});
}
public void edit(int taskId, int newPriority) {
int[] info = taskInfo.get(taskId);
info[0] = newPriority;
// Don't forget to add the new task to the heap!
heap.offer(new int[]{newPriority, taskId});
}
public void rmv(int taskId) {
taskInfo.remove(taskId);
}
public int execTop() {
while (!heap.isEmpty()) {
int[] task = heap.poll();
int priority = task[0];
int taskId = task[1];
if (!taskInfo.containsKey(taskId) || priority != taskInfo.get(taskId)[0]) {
continue;
}
int userId = taskInfo.get(taskId)[1];
taskInfo.remove(taskId);
return userId;
}
return -1;
}
} import heapq
class TaskManager:
def __init__(self, tasks: list[list[int]]):
# task_id to [priority, user_id]
self.task_info = {}
# (-priority, -task_id)
self.heap = []
for user_id, task_id, priority in tasks:
self.task_info[task_id] = [priority, user_id]
heapq.heappush(self.heap, (-priority, -task_id))
def add(self, userId: int, taskId: int, priority: int) -> None:
self.task_info[taskId] = [priority, userId]
heapq.heappush(self.heap, (-priority, -taskId))
def edit(self, taskId: int, newPriority: int) -> None:
info = self.task_info[taskId]
info[0] = newPriority
# Don't forget to add the new task to the heap!
heapq.heappush(self.heap, (-newPriority, -taskId))
def rmv(self, taskId: int) -> None:
del self.task_info[taskId]
def execTop(self) -> int:
while self.heap:
neg_priority, neg_task_id = heapq.heappop(self.heap)
priority = -neg_priority
task_id = -neg_task_id
if task_id not in self.task_info or priority != self.task_info[task_id][0]:
continue
user_id = self.task_info[task_id][1]
del self.task_info[task_id]
return user_id
return -1