Hashing Exercises: Answer Sheet & Worked Solutions
This is the answer sheet for the hashing exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and common pitfalls. Try the problems first — then check your work here.
Q1. Count frequencies
Idea: One pass over the array, incrementing a hash map keyed by value. A missing key starts at zero, so freq[x]++ just works.
Complexity: O(n) time, O(k) space for k distinct values.
#include <unordered_map>
#include <vector>
std::unordered_map<int,int> countFreq(const std::vector<int>& a) {
std::unordered_map<int,int> freq;
for (int x : a) freq[x]++;
return freq;
}
import java.util.*;
Map<Integer,Integer> countFreq(int[] a) {
Map<Integer,Integer> freq = new HashMap<>();
for (int x : a) freq.merge(x, 1, Integer::sum);
return freq;
}
function countFreq(a) {
const freq = new Map();
for (const x of a) freq.set(x, (freq.get(x) ?? 0) + 1);
return freq;
}
from collections import Counter
def count_freq(a):
return Counter(a)
Pitfall: In C++,
freq[x]++silently insertsxwith value 0 first — fine here, but watch for accidental inserts when you only mean to read a key.
Q2. Contains duplicate
Idea: Walk once, adding each value to a hash set. The moment you meet a value already in the set, you have a duplicate.
Complexity: O(n) time, O(n) space.
#include <unordered_set>
#include <vector>
bool containsDuplicate(const std::vector<int>& a) {
std::unordered_set<int> seen;
for (int x : a) {
if (seen.count(x)) return true;
seen.insert(x);
}
return false;
}
import java.util.*;
boolean containsDuplicate(int[] a) {
Set<Integer> seen = new HashSet<>();
for (int x : a) {
if (!seen.add(x)) return true; // add returns false if already present
}
return false;
}
function containsDuplicate(a) {
const seen = new Set();
for (const x of a) {
if (seen.has(x)) return true;
seen.add(x);
}
return false;
}
def contains_duplicate(a):
seen = set()
for x in a:
if x in seen:
return True
seen.add(x)
return False
Pitfall: Comparing
len(set(a)) != len(a)also works but always scans the whole array; the early-exit loop stops at the first repeat.
Q3. Two Sum
Idea: For each element x, the value that completes the target is need = target - x. Keep a map of value → index of everything seen so far; if need is already there, you have the pair. One pass.
Complexity: O(n) time, O(n) space.
#include <unordered_map>
#include <vector>
std::vector<int> twoSum(const std::vector<int>& nums, int target) {
std::unordered_map<int,int> seen;
for (int i = 0; i < (int)nums.size(); i++) {
int need = target - nums[i];
if (seen.count(need)) return {seen[need], i};
seen[nums[i]] = i;
}
return {};
}
import java.util.*;
int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> seen = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (seen.containsKey(need)) return new int[]{seen.get(need), i};
seen.put(nums[i], i);
}
return new int[]{};
}
function twoSum(nums, target) {
const seen = new Map();
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (seen.has(need)) return [seen.get(need), i];
seen.set(nums[i], i);
}
return [];
}
def two_sum(nums, target):
seen = {}
for i, x in enumerate(nums):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
return []
Pitfall: Insert
xafter the complement check. Inserting first lets a single element pair with itself (e.g. target 6 with a lone 3).
Q4. Group anagrams
Idea: Anagrams share the same multiset of letters, so their sorted letters form an identical signature. Use that signature as a map key and append each word to its group.
Complexity: O(n · k log k) time for n words of length up to k; O(n · k) space.
#include <unordered_map>
#include <vector>
#include <string>
#include <algorithm>
std::vector<std::vector<std::string>> groupAnagrams(const std::vector<std::string>& words) {
std::unordered_map<std::string, std::vector<std::string>> groups;
for (const auto& w : words) {
std::string key = w;
std::sort(key.begin(), key.end());
groups[key].push_back(w);
}
std::vector<std::vector<std::string>> out;
for (auto& [k, v] : groups) out.push_back(v);
return out;
}
import java.util.*;
List<List<String>> groupAnagrams(String[] words) {
Map<String, List<String>> groups = new HashMap<>();
for (String w : words) {
char[] c = w.toCharArray();
Arrays.sort(c);
String key = new String(c);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(w);
}
return new ArrayList<>(groups.values());
}
function groupAnagrams(words) {
const groups = new Map();
for (const w of words) {
const key = [...w].sort().join("");
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(w);
}
return [...groups.values()];
}
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for w in words:
key = "".join(sorted(w))
groups[key].append(w)
return list(groups.values())
Pitfall: A 26-letter count vector (e.g.
"a2b1...") is a faster O(k) signature than sorting, but sorting is simpler to get right and plenty fast for interviews.
Q5. First non-repeating character
Idea: Two passes. First count every character’s frequency; then scan left to right and return the index of the first character whose count is 1.
Complexity: O(n) time, O(1) space (the alphabet is bounded).
#include <unordered_map>
#include <string>
int firstUniqChar(const std::string& s) {
std::unordered_map<char,int> freq;
for (char c : s) freq[c]++;
for (int i = 0; i < (int)s.size(); i++)
if (freq[s[i]] == 1) return i;
return -1;
}
import java.util.*;
int firstUniqChar(String s) {
Map<Character,Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) freq.merge(c, 1, Integer::sum);
for (int i = 0; i < s.length(); i++)
if (freq.get(s.charAt(i)) == 1) return i;
return -1;
}
function firstUniqChar(s) {
const freq = new Map();
for (const c of s) freq.set(c, (freq.get(c) ?? 0) + 1);
for (let i = 0; i < s.length; i++)
if (freq.get(s[i]) === 1) return i;
return -1;
}
from collections import Counter
def first_uniq_char(s):
freq = Counter(s)
for i, c in enumerate(s):
if freq[c] == 1:
return i
return -1
Pitfall: Returning the first key with count 1 from the map gives the wrong answer — map order is not string order. Scan the original string for the index.
Q6. Subarray sum equals K
Idea: Let prefix be the running sum up to index i. A subarray (j, i] sums to k exactly when prefix - k equaled some earlier prefix. Keep a map of prefixSum → how many times it occurred and, at each step, add the count of prefix - k. Seed the map with {0: 1} so subarrays starting at index 0 are counted.
Complexity: O(n) time, O(n) space.
#include <unordered_map>
#include <vector>
int subarraySum(const std::vector<int>& nums, int k) {
std::unordered_map<int,int> count{{0, 1}};
int prefix = 0, result = 0;
for (int x : nums) {
prefix += x;
auto it = count.find(prefix - k);
if (it != count.end()) result += it->second;
count[prefix]++;
}
return result;
}
import java.util.*;
int subarraySum(int[] nums, int k) {
Map<Integer,Integer> count = new HashMap<>();
count.put(0, 1);
int prefix = 0, result = 0;
for (int x : nums) {
prefix += x;
result += count.getOrDefault(prefix - k, 0);
count.merge(prefix, 1, Integer::sum);
}
return result;
}
function subarraySum(nums, k) {
const count = new Map([[0, 1]]);
let prefix = 0, result = 0;
for (const x of nums) {
prefix += x;
result += count.get(prefix - k) ?? 0;
count.set(prefix, (count.get(prefix) ?? 0) + 1);
}
return result;
}
from collections import defaultdict
def subarray_sum(nums, k):
count = defaultdict(int)
count[0] = 1
prefix = result = 0
for x in nums:
prefix += x
result += count[prefix - k]
count[prefix] += 1
return result
Pitfall: A sliding window does not work here because values can be negative. And you must read
count[prefix - k]before incrementingcount[prefix], or a zero-valuedkwould over-count.
Q7. Longest consecutive sequence
Idea: Put every value in a hash set for O(1) membership. A number starts a run only if x - 1 is not in the set; from each such start, walk x, x+1, x+2, … while they exist and measure the run. Each value is visited at most twice, so it is O(n) — no sorting.
Complexity: O(n) time, O(n) space.
#include <unordered_set>
#include <vector>
#include <algorithm>
int longestConsecutive(const std::vector<int>& nums) {
std::unordered_set<int> s(nums.begin(), nums.end());
int best = 0;
for (int x : s) {
if (s.count(x - 1)) continue; // not a run start
int len = 1, cur = x;
while (s.count(cur + 1)) { cur++; len++; }
best = std::max(best, len);
}
return best;
}
import java.util.*;
int longestConsecutive(int[] nums) {
Set<Integer> s = new HashSet<>();
for (int x : nums) s.add(x);
int best = 0;
for (int x : s) {
if (s.contains(x - 1)) continue; // not a run start
int len = 1, cur = x;
while (s.contains(cur + 1)) { cur++; len++; }
best = Math.max(best, len);
}
return best;
}
function longestConsecutive(nums) {
const s = new Set(nums);
let best = 0;
for (const x of s) {
if (s.has(x - 1)) continue; // not a run start
let len = 1, cur = x;
while (s.has(cur + 1)) { cur++; len++; }
best = Math.max(best, len);
}
return best;
}
def longest_consecutive(nums):
s = set(nums)
best = 0
for x in s:
if x - 1 in s: # not a run start
continue
length, cur = 1, x
while cur + 1 in s:
cur += 1
length += 1
best = max(best, length)
return best
Pitfall: Without the
if x - 1 in sguard you would re-walk every run from every member, making it O(n²). The guard ensures each run is counted exactly once, from its smallest element.
That’s the full answer sheet. Back to the hashing exercises or continue to recursion.