A hash table (hash map) stores key-value pairs. A hash function maps keys to array indices (buckets) for fast lookups.
// Concept:
// key → hashFunction(key) → index → store value at index
// JavaScript's Map is a built-in hash table:
const map = new Map();
map.set("name", "Alice"); // O(1)
map.get("name"); // "Alice" — O(1)
map.has("name"); // true — O(1)
map.delete("name"); // O(1)
Average time complexity: O(1) for insert, lookup, and delete. Worst case O(n) with many collisions.
A good hash function should:
// Simple hash function for strings
function simpleHash(key, tableSize) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash * 31 + key.charCodeAt(i)) % tableSize;
}
return hash;
}
// simpleHash("hello", 100) => some index 0-99
Multiplying by a prime (31) helps distribute keys more uniformly.
Chaining: Each bucket holds a linked list. Colliding keys are stored in the same bucket's list.
Open Addressing: On collision, probe for the next available slot (linear probing, quadratic probing, or double hashing).
// Chaining example
class HashTableChaining {
constructor(size = 53) {
this.table = new Array(size).fill(null).map(() => []);
}
_hash(key) {
let h = 0;
for (const ch of String(key)) h = (h * 31 + ch.charCodeAt(0)) % this.table.length;
return h;
}
set(key, val) {
const idx = this._hash(key);
const existing = this.table[idx].find(p => p[0] === key);
if (existing) existing[1] = val;
else this.table[idx].push([key, val]);
}
get(key) {
const pair = this.table[this._hash(key)].find(p => p[0] === key);
return pair ? pair[1] : undefined;
}
}
Chaining is simpler; open addressing has better cache performance.
Hash map (hash table) operation complexities:
Worst case occurs when all keys hash to the same bucket. A good hash function and a proper load factor (typically < 0.75) keep operations near O(1).
// Load factor = number of entries / number of buckets
// When load factor exceeds threshold, the table is resized
// (usually doubled) and all entries are rehashed.
JavaScript's Map and Object handle this automatically.
Store each number's index in a hash map. For each element, check if target - num already exists in the map.
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
return [];
}
// twoSum([2, 7, 11, 15], 9) => [0, 1]
// twoSum([3, 3], 6) => [0, 1]
Time: O(n) | Space: O(n). Much better than the brute-force O(n²) approach.
Use sorted characters as the key. All anagrams produce the same sorted key.
function groupAnagrams(strs) {
const map = new Map();
for (const str of strs) {
const key = str.split('').sort().join('');
if (!map.has(key)) map.set(key, []);
map.get(key).push(str);
}
return Array.from(map.values());
}
// groupAnagrams(["eat","tea","tan","ate","nat","bat"])
// => [["eat","tea","ate"], ["tan","nat"], ["bat"]]
Time: O(n × k log k) where k is the max string length | Space: O(n × k)
Iterate through the array and add each element to a set. The first element already in the set is the first duplicate.
function firstDuplicate(arr) {
const seen = new Set();
for (const num of arr) {
if (seen.has(num)) return num;
seen.add(num);
}
return -1; // no duplicate
}
// firstDuplicate([2, 1, 3, 5, 3, 2]) => 3
// firstDuplicate([1, 2, 3]) => -1
Time: O(n) | Space: O(n)
Iterate through the collection and increment counts in a Map (or object).
function countFrequency(arr) {
const freq = new Map();
for (const item of arr) {
freq.set(item, (freq.get(item) || 0) + 1);
}
return freq;
}
// countFrequency([1,2,2,3,3,3])
// => Map { 1 => 1, 2 => 2, 3 => 3 }
// Find element with highest frequency:
function mostFrequent(arr) {
const freq = countFrequency(arr);
let maxItem, maxCount = 0;
for (const [item, count] of freq) {
if (count > maxCount) { maxCount = count; maxItem = item; }
}
return maxItem;
}
Time: O(n) | Space: O(k) where k is the number of unique elements.
Put the first array into a Set, then filter the second array to keep only elements present in the Set.
function intersection(arr1, arr2) {
const set = new Set(arr1);
return [...new Set(arr2.filter(x => set.has(x)))];
}
// intersection([1,2,2,1], [2,2]) => [2]
// intersection([4,9,5], [9,4,9,8,4]) => [9, 4]
// For intersection with duplicates:
function intersect(arr1, arr2) {
const map = new Map();
for (const n of arr1) map.set(n, (map.get(n) || 0) + 1);
const result = [];
for (const n of arr2) {
if (map.get(n) > 0) { result.push(n); map.set(n, map.get(n) - 1); }
}
return result;
}
Time: O(n + m) | Space: O(n)
An LRU (Least Recently Used) Cache evicts the least recently accessed item when capacity is full. It combines a hash map (for O(1) lookup) and a doubly linked list (for O(1) insertion/removal).
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key); // remove
this.cache.set(key, val); // re-insert (most recent)
return val;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
// Delete the least recently used (first inserted)
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
}
}
JavaScript's Map preserves insertion order, making this implementation simple. All operations are O(1).
Use a hash map to store each number's complement (target - num). For each element, check if it already exists in the map.
function twoSum(nums, target) {
const map = new Map(); // value -> index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) return [map.get(complement), i];
map.set(nums[i], i);
}
return [];
}
// Input: nums=[2,7,11,15], target=9 => Output: [0,1]
// Input: nums=[3,2,4], target=6 => Output: [1,2]
// Input: nums=[3,3], target=6 => Output: [0,1]
Time: O(n) | Space: O(n)
Use a prefix sum hash map. Count occurrences of (prefixSum - k) as you scan: each match means a subarray ending here sums to k.
function subarraySum(nums, k) {
const map = new Map([[0, 1]]); // prefixSum -> count
let sum = 0, count = 0;
for (const num of nums) {
sum += num;
count += map.get(sum - k) || 0;
map.set(sum, (map.get(sum) || 0) + 1);
}
return count;
}
// Input: nums=[1,1,1], k=2 => Output: 2
// Input: nums=[1,2,3], k=3 => Output: 2 ([1,2] and [3])
// Input: nums=[-1,-1,1], k=0 => Output: 1
Time: O(n) | Space: O(n)
A string can form a palindrome if at most one character has an odd frequency. Use a Set to toggle character presence.
function canPermutePalindrome(s) {
const set = new Set();
for (const ch of s) {
if (set.has(ch)) set.delete(ch); // even count: remove
else set.add(ch); // odd count: add
}
return set.size <= 1; // at most one odd-frequency char
}
// Input: "aab" => Output: true ("aba" is palindrome)
// Input: "code" => Output: false
// Input: "carerac" => Output: true ("racecar")
Time: O(n) | Space: O(k) unique chars
Put all numbers in a Set. Only start counting from elements where num - 1 is NOT in the set (start of a sequence). Track the longest streak.
function longestConsecutive(nums) {
const set = new Set(nums);
let best = 0;
for (const num of set) {
if (!set.has(num - 1)) { // start of a sequence
let len = 1;
while (set.has(num + len)) len++;
best = Math.max(best, len);
}
}
return best;
}
// Input: [100,4,200,1,3,2] => Output: 4 (1,2,3,4)
// Input: [0,3,7,2,5,8,4,6,0,1] => Output: 9
// Input: [] => Output: 0
Time: O(n) | Space: O(n)
Use a Set and scan left to right. The first number already in the set is the answer.
function firstDuplicate(arr) {
const seen = new Set();
for (const num of arr) {
if (seen.has(num)) return num;
seen.add(num);
}
return -1;
}
// Input: [2,1,3,5,3,2] => Output: 3 (3 appears at index 2 and 4)
// Input: [2,4,3,5,1] => Output: -1 (no duplicates)
// Input: [1,1] => Output: 1
Time: O(n) | Space: O(n)
Split the string on whitespace, then use a Map to tally each word's count.
function wordFrequency(sentence) {
const freq = new Map();
for (const word of sentence.toLowerCase().split(/s+/)) {
freq.set(word, (freq.get(word) || 0) + 1);
}
return freq;
}
// Input: "the sky is blue the sky"
// Output: Map { 'the' => 2, 'sky' => 2, 'is' => 1, 'blue' => 1 }
// Input: "hello world hello"
// Output: Map { 'hello' => 2, 'world' => 1 }
Time: O(n) | Space: O(k) unique words
Parse each path, extract content, and use a Map from content to list of paths. Return groups with 2+ paths.
function findDuplicate(paths) {
const map = new Map();
for (const path of paths) {
const parts = path.split(' ');
const dir = parts[0];
for (let i = 1; i < parts.length; i++) {
const bracketIdx = parts[i].indexOf('(');
const filename = parts[i].slice(0, bracketIdx);
const content = parts[i].slice(bracketIdx + 1, parts[i].length - 1);
const fullPath = dir + '/' + filename;
if (!map.has(content)) map.set(content, []);
map.get(content).push(fullPath);
}
}
return [...map.values()].filter(group => group.length > 1);
}
// Input: ["root/a 1.txt(abcd) 2.txt(efgh)", "root/b 4.txt(abcd)"]
// Output: [["root/a/1.txt","root/b/4.txt"]]
Time: O(n × m) average | Space: O(n × m)
Sort the array, then use two outer loops + two-pointer technique for the inner pair. Use a Set to deduplicate results easily.
function fourSum(nums, target) {
nums.sort((a, b) => a - b);
const result = [];
for (let i = 0; i < nums.length - 3; i++) {
if (i > 0 && nums[i] === nums[i-1]) continue; // skip duplicates
for (let j = i + 1; j < nums.length - 2; j++) {
if (j > i+1 && nums[j] === nums[j-1]) continue;
let l = j + 1, r = nums.length - 1;
while (l < r) {
const sum = nums[i] + nums[j] + nums[l] + nums[r];
if (sum === target) {
result.push([nums[i], nums[j], nums[l], nums[r]]);
while (l < r && nums[l] === nums[l+1]) l++;
while (l < r && nums[r] === nums[r-1]) r--;
l++; r--;
} else if (sum < target) l++;
else r--;
}
}
}
return result;
}
// Input: nums=[1,0,-1,0,-2,2], target=0
// Output: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
Time: O(n³) | Space: O(1) excluding output
Use an array of buckets with separate chaining (linked list or array per bucket). The hash function maps a key to a bucket index.
class HashMap {
constructor(size = 1000) {
this.buckets = new Array(size).fill(null).map(() => []);
this.size = size;
}
_hash(key) {
let hash = 0;
for (const ch of String(key)) hash = (hash * 31 + ch.charCodeAt(0)) % this.size;
return hash;
}
set(key, value) {
const bucket = this.buckets[this._hash(key)];
const pair = bucket.find(p => p[0] === key);
if (pair) pair[1] = value;
else bucket.push([key, value]);
}
get(key) {
const pair = this.buckets[this._hash(key)].find(p => p[0] === key);
return pair ? pair[1] : undefined;
}
delete(key) {
const idx = this._hash(key);
this.buckets[idx] = this.buckets[idx].filter(p => p[0] !== key);
}
}
// set("a",1), set("b",2), get("a") => 1, delete("a"), get("a") => undefined
Average Time: O(1) per op | Worst (all collide): O(n) | Space: O(n)
Build a frequency map, then either find the max in O(n) or use a bucket sort approach for a full frequency ranking.
function topKFrequent(nums, k) {
const freq = new Map();
for (const n of nums) freq.set(n, (freq.get(n) || 0) + 1);
// Bucket sort: index = frequency
const buckets = new Array(nums.length + 1).fill(null).map(() => []);
for (const [num, count] of freq) buckets[count].push(num);
const result = [];
for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) {
result.push(...buckets[i]);
}
return result.slice(0, k);
}
// Input: nums=[1,1,1,2,2,3], k=2 => Output: [1,2]
// Input: nums=[1], k=1 => Output: [1]
Time: O(n) bucket sort | Space: O(n)
For an undirected graph, use DFS with a visited set. If you visit a node that's already in the current path (recursion stack), a cycle exists.
function hasCycle(numNodes, edges) {
const adj = Array.from({length: numNodes}, () => []);
for (const [u, v] of edges) { adj[u].push(v); adj[v].push(u); }
const visited = new Set();
function dfs(node, parent) {
visited.add(node);
for (const neighbor of adj[node]) {
if (!visited.has(neighbor)) {
if (dfs(neighbor, node)) return true;
} else if (neighbor !== parent) {
return true; // back edge = cycle
}
}
return false;
}
for (let i = 0; i < numNodes; i++) {
if (!visited.has(i) && dfs(i, -1)) return true;
}
return false;
}
// Input: 4 nodes, edges=[[0,1],[1,2],[2,3],[3,0]] => Output: true
// Input: 4 nodes, edges=[[0,1],[0,2],[1,3]] => Output: false
Time: O(V + E) | Space: O(V)
Calculate net balance for each person using a map. Then greedily match the person with maximum debt against the person with maximum credit.
function minTransfers(transactions) {
const balance = new Map();
for (const [from, to, amount] of transactions) {
balance.set(from, (balance.get(from) || 0) - amount);
balance.set(to, (balance.get(to) || 0) + amount);
}
const debts = [...balance.values()].filter(v => v !== 0);
function settle(k) {
while (k < debts.length && debts[k] === 0) k++;
if (k === debts.length) return 0;
let min = Infinity;
for (let i = k + 1; i < debts.length; i++) {
if (debts[k] * debts[i] < 0) { // opposite signs
debts[i] += debts[k];
min = Math.min(min, 1 + settle(k + 1));
debts[i] -= debts[k]; // backtrack
}
}
return min;
}
return settle(0);
}
// Input: [[0,1,10],[2,0,5]] => Output: 2
// Input: [[0,1,10],[1,0,1],[1,2,5],[2,0,5]] => Output: 1
Time: O(n!) worst case | Space: O(n)
Generate all substrings and insert them into a Set to automatically deduplicate. Return the set's size.
function countDistinctSubstrings(s) {
const set = new Set();
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
set.add(s.slice(i, j));
}
}
return set.size;
}
// Input: "abc" => Output: 6 ("a","b","c","ab","bc","abc")
// Input: "aaa" => Output: 3 ("a","aa","aaa")
// Input: "abab" => Output: 7
Time: O(n³) due to string creation | Space: O(n²)
For larger inputs, use Trie or Suffix Array for O(n²) or O(n log n).