Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

18.7 Insert Delete GetRandom O(1)

Source: src/main/kotlin/probability/InsertDeleteGetRandom.kt Pattern: map + list swap-remove · Core page

The Problem

Design a data structure supporting insert(val) (false if present), remove(val) (false if absent), and getRandom() — a uniformly random existing value — all in O(1).

  • Constraints: up to $2 \times 10^5$ operations; values are unique.

Examples

RandomizedSet rs = new RandomizedSet();
rs.insert(1); rs.insert(2); rs.getRandom() -> 1 or 2 (uniform); rs.remove(1); rs.getRandom() -> 2

Intuition — a map alone can’t be random; a list alone can’t delete in O(1); together they can

The two requirements pull in opposite directions:

  • getRandom() in O(1) needs an array (random index);
  • remove(val) in O(1) needs a hash map (locate the value).

The composition: keep the elements in a list (the array for random access) and a map value -> index (for O(1) location). The join is swap-remove: to delete val, swap it with the last element, then pop the back:

remove(val):
    index = map[val]                 # O(1) locate
    last = list.removeLast()
    if index < list.size:            # val wasn't already last
        list[index] = last           # the last element moves into the hole
        map[last] = index            # ...and its index follows
    map.remove(val)                  # drop val's entry

Why swap-remove and not list-remove? Removing from the middle of a list shifts everything right of it — O(n). Swapping with the last element turns the delete into a constant-time pop, and the map update is a single overwrite. The invariant “list is exactly the map’s keys, compactly” is preserved by the swap.

Why is getRandom uniform? The list is dense (no holes — swap-remove guarantees it), so a random index picks each value with equal probability. A sparse list (with tombstones) would bias the sampling.

Why does insert reject duplicates? The map’s presence check (map[value] exists) is the O(1) membership test; the list must not hold duplicates or the index map breaks.

Approach 1 — Set + list without swap (O(n) remove)

A HashSet plus a list that does list.remove(value): correct, but shifts O(n) per delete.

Approach 2 — Map + swap-remove (the repo’s version, optimal)

class InsertDeleteGetRandom {
    private val elements = mutableListOf<Int>()
    private val elementIndices = mutableMapOf<Int, Int>()

    /**
     * @param value candidate
     * @return      true if newly inserted, false if already present
     */
    fun insert(value: Int): Boolean {
        elementIndices[value]?.let { return false }          // already present
        elementIndices[value] = elements.size.also { elements.add(value) }
        return true
    }

    /**
     * @param value candidate
     * @return      true if removed, false if absent
     */
    fun remove(value: Int): Boolean {
        val index = elementIndices[value] ?: return false    // absent
        val lastElement = elements.removeLast()

        if (index < elements.size) {                         // value wasn't the last element
            elements[index] = lastElement                    // last element fills the hole
            elementIndices[lastElement] = index              // ...and its index follows
        }
        elementIndices.remove(value)
        return true
    }

    /** @return a uniformly random existing value */
    fun getRandom(): Int = elements.random()
}
import java.util.*;

public class RandomizedSet {
    private final List<Integer> list = new ArrayList<>();
    private final Map<Integer, Integer> index = new HashMap<>();
    private final Random random = new Random();

    /**
     * @param val candidate
     * @return    true if newly inserted, false if already present
     */
    public boolean insert(int val) {
        if (index.containsKey(val)) return false;            // already present
        index.put(val, list.size());
        list.add(val);
        return true;
    }

    /**
     * @param val candidate
     * @return    true if removed, false if absent
     */
    public boolean remove(int val) {
        Integer pos = index.get(val);
        if (pos == null) return false;                       // absent

        int last = list.get(list.size() - 1);
        list.set(pos, last);                                 // last element fills the hole
        index.put(last, pos);                                // ...and its index follows
        list.remove(list.size() - 1);                        // pop the back
        index.remove(val);
        return true;
    }

    /** @return a uniformly random existing value */
    public int getRandom() {
        return list.get(random.nextInt(list.size()));
    }
}
#include <cstdlib>
#include <unordered_map>
#include <vector>

class RandomizedSet {
    std::vector<int> list;
    std::unordered_map<int, int> index;

public:
    /**
     * @param val candidate
     * @return    true if newly inserted, false if already present
     */
    bool insert(int val) {
        if (index.count(val)) return false;                  // already present
        index[val] = list.size();
        list.push_back(val);
        return true;
    }

    /**
     * @param val candidate
     * @return    true if removed, false if absent
     */
    bool remove(int val) {
        if (!index.count(val)) return false;                 // absent

        int pos = index[val];
        int last = list.back();
        list[pos] = last;                                    // last element fills the hole
        index[last] = pos;                                   // ...and its index follows
        list.pop_back();                                     // pop the back
        index.erase(val);
        return true;
    }

    /** @return a uniformly random existing value */
    int getRandom() {
        return list[rand() % list.size()];
    }
};
import random

class RandomizedSet:
    def __init__(self):
        self.list = []
        self.index = {}

    def insert(self, val: int) -> bool:
        """@return: true if newly inserted, false if already present"""
        if val in self.index:
            return False                     # already present
        self.index[val] = len(self.list)
        self.list.append(val)
        return True

    def remove(self, val: int) -> bool:
        """@return: true if removed, false if absent"""
        if val not in self.index:
            return False                     # absent
        pos = self.index[val]
        last = self.list[-1]
        self.list[pos] = last                # last element fills the hole
        self.index[last] = pos               # ...and its index follows
        self.list.pop()                      # pop the back
        del self.index[val]
        return True

    def get_random(self) -> int:
        """@return: a uniformly random existing value"""
        return random.choice(self.list)
#![allow(unused)]
fn main() {
use rand::Rng;
use std::collections::HashMap;

struct RandomizedSet {
    list: Vec<i32>,
    index: HashMap<i32, usize>,
}

impl RandomizedSet {
    fn new() -> Self { RandomizedSet { list: Vec::new(), index: HashMap::new() } }

    /// @return true if newly inserted, false if already present
    fn insert(&mut self, val: i32) -> bool {
        if self.index.contains_key(&val) { return false; }     // already present
        self.index.insert(val, self.list.len());
        self.list.push(val);
        true
    }

    /// @return true if removed, false if absent
    fn remove(&mut self, val: i32) -> bool {
        let Some(pos) = self.index.remove(&val) else { return false; };  // absent
        let last = *self.list.last().unwrap();
        self.list[pos] = last;                // last element fills the hole
        self.index.insert(last, pos);         // ...and its index follows
        self.list.pop();                      // pop the back
        true
    }

    /// @return a uniformly random existing value
    fn get_random(&self) -> i32 {
        let i = rand::thread_rng().gen_range(0..self.list.len());
        self.list[i]
    }
}
}

Dry run

Input: the example sequence.

insert(1): index={1:0}, list=[1].
insert(2): index={1:0, 2:1}, list=[1,2].
getRandom(): random index in {0,1} -> 1 or 2, uniform.  (say 1)
remove(1): pos=0.  last=2.  list[0]=2 -> list=[2,2].  index[2]=0 -> index={2:0}.
           list.pop() -> list=[2].  index.remove(1).  -> true ✓
getRandom(): -> 2 (only element).
remove(1): 1 not in index -> false ✓

The swap-remove’s critical case is remove(1) with [1,2]: the last element 2 moves into position 0 — overwriting the doomed value — and its map entry follows. The list stays dense ([2], not [_, 2]), which is what keeps getRandom uniform and insert’s list.size index correct.

Complexity

Time. All operations are O(1) (hash + array ops):

$$ T(n) = O(1) \text{ per operation} $$

Space. The list + map:

$$ S(n) = O(n) $$

Variants & follow-ups

  • Insert Delete GetRandom With Duplicates — the map becomes value → set of indices; the swap-remove extends with one more bookkeeping layer.
  • RandomizedCollection / Blacklist variants — the same dense-list + map design with extra rules.
  • LRU Cache (18.1) — the sibling “map + ordered structure” design; this page’s list plays the role of LRU’s linked list.
  • Interview follow-up: “Why must the list stay dense?” getRandom samples a random index — uniform only if every index holds exactly one live value. Swap-remove guarantees density by always replacing a hole with the last element; any tombstone scheme would skew the sampling and is the classic wrong answer to name.