Category 4

Hash Maps & Sets

Master dictionary-based problem-solving patterns including frequency counting, bijection mapping, cycle detection, and complement strategies. Solve 15 essential LeetCode and Codility problems with complete C# implementations.

15
Problems
6
Core Patterns
⭐ ★★★
Difficulty Range
15+
Code Examples
Table of Contents
THEORY

Core Concepts

Hash maps and sets provide constant-time lookups and enable pattern-based solutions across counting, grouping, and relationship problems.

Dictionary<TKey,TValue>

Average O(1) lookup, insertion, deletion. TryGetValue pattern prevents exceptions and is idiomatic C#.

C#
var freq = new Dictionary<int, int>();
if (!freq.TryGetValue(num, out int count)) count = 0;
freq[num] = count + 1;

HashSet<T>

O(1) Contains, Add, Remove. No duplicates. Use for deduplication and membership testing.

C#
var seen = new HashSet<int>();
if (!seen.Add(num)) { // Duplicate found }

Core Patterns

Frequency Counting
Store element counts in dict. Identify mode, outliers, or duplicates.
Complement Strategy
Preprocess all values into set, then iterate looking for target - current.
Grouping by Key
Sorted string as key. Group anagrams, transformations by signature.
Bijection Mapping
Two dicts for each direction. Validate one-to-one correspondence.
Cycle Detection
Track visited nodes. Loop exists if revisit before termination.
Prefix Sum + Map
Store cumulative sum → count. Find subarray sums matching target.
1. Odd Occurrences In Array

Find element appearing odd number of times. Codility ★

XOR O(n) O(1) space
1

Problem Statement

Given array where every element except one appears even times, return the single odd-occurrence element. All other integers appear even times.

Approach: XOR Properties

a ^ a = 0 and a ^ 0 = a. XOR all elements—pairs cancel, leaving the odd one.

C#
public int OddOccurrences(int[] arr)
{
    int result = 0;
    foreach (int num in arr)
        result ^= num;
    return result;
}

Trace: arr = [9, 3, 9, 3, 9, 7, 9]

1
result = 0 ^ 9 = 9
2
result = 9 ^ 3 = 10 (1010)
3
result = 10 ^ 9 = 3 (pairs cancel)
4
result = 3 ^ 3 = 0
5
result = 0 ^ 9 = 9
6
result = 9 ^ 7 = 14
7
result = 14 ^ 9 = 7. Return 7.

Edge Cases

Valid Cases
Single element [5] → 5
All pairs [2,2,3,3,4] → 4
Constraints
Exactly one odd-occurrence element guaranteed
Array non-empty

Complexity

Time
O(n)
Space
O(1)
2. Dominator

Find element appearing > n/2 times. Codility ★★

Boyer-Moore O(n) O(1) space
2

Problem Statement

Find majority element appearing > n/2 times, or -1 if none exists.

Approach: Boyer-Moore Voting

Track candidate with a count. Increment for matches, decrement otherwise. When count hits 0, switch candidate. Verify the final candidate.

C#
public int Dominator(int[] arr)
{
    int candidate = -1, count = 0;

    // Find candidate
    foreach (int num in arr) {
        if (count == 0) candidate = num;
        count += (num == candidate) ? 1 : -1;
    }

    // Verify candidate
    count = 0;
    foreach (int num in arr)
        if (num == candidate) count++;

    return (count * 2 > arr.Length) ? candidate : -1;
}

Trace: arr = [3, 1, 3, 3, 2]

1
num=3, count=0 → candidate=3, count=1
2
num=1, count=1 → count=0 (mismatch)
3
num=3, count=0 → candidate=3, count=1
4
num=3, count=1 → count=2 (match)
5
num=2, count=2 → count=1 (mismatch). Verify: 3 appears 3 times > 5/2. Return 3.

Edge Cases

Valid Cases
Single element [5] → 5
No majority [1,2,3] → -1
Exact majority [1,1,2] → 1
Constraints
Must verify candidate (could be incorrect before count check)
Two-pass: find candidate, verify

Complexity

Time
O(n)
Space
O(1)
3. Group Anagrams

Partition strings into anagram groups. LeetCode 49 ★★

Sorting O(nk log k) Grouping
3

Problem Statement

Given list of strings, group anagrams together. Return list of groups.

Approach: Sorted Signature as Key

Sort each string's characters. Identical anagrams have identical sorted forms. Use sorted form as dict key to group.

C#
public IList<IList<string>> GroupAnagrams(string[] strs)
{
    var groups = new Dictionary<string, List<string>>();

    foreach (string s in strs) {
        char[] sorted = s.ToCharArray();
        Array.Sort(sorted);
        string key = new string(sorted);

        if (!groups.ContainsKey(key))
            groups[key] = new List<string>();
        groups[key].Add(s);
    }

    return new List<IList<string>>(groups.Values);
}

Trace: strs = ["eat","tea","ate","bat"]

1
"eat" → sorted "aet" → groups["aet"] = ["eat"]
2
"tea" → sorted "aet" → groups["aet"] = ["eat","tea"]
3
"ate" → sorted "aet" → groups["aet"] = ["eat","tea","ate"]
4
"bat" → sorted "abt" → groups["abt"] = ["bat"]. Return values.

Edge Cases

Valid Cases
Single word ["a"] → [["a"]]
No anagrams ["abc","def"] → [["abc"],["def"]]
Constraints
Empty strings treated as single group
Case-sensitive (eat ≠ Eat)

Complexity

Time
O(n·k log k)
Space
O(n·k)

n = number of strings, k = max string length

4. Top K Frequent Elements

Return k most frequent elements. LeetCode 347 ★★

Bucket Sort O(n) Best
4

Problem Statement

Given array and k, return k most frequent elements. Assume k ≤ unique elements.

Approach: Bucket Sort

Count frequencies. Create array of buckets indexed by frequency. Elements can appear 1 to n times. Collect from high-frequency buckets down.

C#
public int[] TopKFrequent(int[] nums, int k)
{
    var freq = new Dictionary<int, int>();
    foreach (int n in nums) {
        if (!freq.TryGetValue(n, out int c)) c = 0;
        freq[n] = c + 1;
    }

    var buckets = new List<int>[nums.Length + 1];
    for (int i = 0; i < buckets.Length; i++)
        buckets[i] = new List<int>();

    foreach (var pair in freq)
        buckets[pair.Value].Add(pair.Key);

    var result = new List<int>();
    for (int i = buckets.Length - 1; i >= 0 && result.Count < k; i--)
        result.AddRange(buckets[i]);

    return result.ToArray();
}

Trace: nums = [1,1,1,2,2,3], k = 2

1
freq = {1:3, 2:2, 3:1}
2
buckets[3]=[1], buckets[2]=[2], buckets[1]=[3]
3
Iterate from buckets[6] down. Add 1 (count=1), then 2 (count=2). Return [1,2].

Edge Cases

Valid Cases
k = 1: return [top_freq_element]
All same frequency: any k work
Constraints
Bucket size = n+1 (frequencies 1 to n)
Tie-breaking arbitrary (order depends on dict iteration)

Complexity

Time
O(n)
Space
O(n)
5. Longest Consecutive Sequence

Find longest streak of consecutive numbers. LeetCode 128 ★★

Expand O(n) HashSet
5

Problem Statement

Given unsorted array, return length of longest consecutive elements sequence. O(n) time.

Approach: Expand from Sequence Start

Store all nums in HashSet. For each number, if it's a sequence start (num-1 not in set), expand right counting consecutive numbers.

C#
public int LongestConsecutive(int[] nums)
{
    if (nums.Length == 0) return 0;

    var numSet = new HashSet<int>(nums);
    int maxLen = 0;

    foreach (int num in numSet) {
        // Only start from sequence beginning
        if (!numSet.Contains(num - 1)) {
            int current = num;
            int streak = 1;

            while (numSet.Contains(current + 1)) {
                current++;
                streak++;
            }

            maxLen = Math.Max(maxLen, streak);
        }
    }

    return maxLen;
}

Trace: nums = [100, 4, 200, 1, 3, 2]

1
numSet = {100, 4, 200, 1, 3, 2}
2
num=100: 99 not in set. Expand: 100→101? No. streak=1
3
num=4: 3 in set, skip (not start)
4
num=1: 0 not in set. Expand: 1→2→3→4. streak=4. maxLen=4
5
num=200, 3, 2: already processed or not starts. Return 4.

Edge Cases

Valid Cases
Empty array [] → 0
Duplicates [1,2,0,1] → 3
Unordered [9,1,4,7,3,2,8,5,6] → 9
Constraints
Only process sequence starts (critical for O(n))
Each element visited once, not per sequence

Complexity

Time
O(n)
Space
O(n)

Each element visited once during expansion (amortized)

6. Valid Anagram

Check if two strings are anagrams. LeetCode 242 ★

Frequency O(n) Array
6

Problem Statement

Given two strings s and t, return true if t is anagram of s (same characters, different order).

Approach: Frequency Array

Count chars in s. Decrement counts for chars in t. If all counts are 0, they're anagrams.

C#
public bool IsAnagram(string s, string t)
{
    if (s.Length != t.Length) return false;

    int[] freq = new int[26];

    for (int i = 0; i < s.Length; i++) {
        freq[s[i] - 'a']++;
        freq[t[i] - 'a']--;
    }

    foreach (int f in freq)
        if (f != 0) return false;

    return true;
}

Trace: s = "anagram", t = "nagaram"

1
Both length 7. freq = int[26]
2
i=0: 'a','n' → freq[0]=1, freq[13]=-1
3
Continue pairing s[i] and t[i]
4
After loop: freq[0]=3-3=0, freq[13]=2-2=0, all balanced. Return true.

Edge Cases

Valid Cases
s = "ab", t = "ba" → true
s = "ab", t = "a" → false (length)
s = "", t = "" → true
Constraints
Only lowercase a-z
Early return if length mismatch

Complexity

Time
O(n)
Space
O(1)

Fixed array size 26 (alphabet)

7. Isomorphic Strings

Check if strings follow same character mapping. LeetCode 205 ★

Bijection O(n) Two Maps
7

Problem Statement

Two strings s and t are isomorphic if chars map one-to-one (bijection). E.g., "badc" and "baba" map a→b, b→a, d→b, c→a.

Approach: Bidirectional Mapping

Track s→t and t→s mappings. For each pair, check consistency in both directions. If conflict, return false.

C#
public bool IsIsomorphic(string s, string t)
{
    var sToT = new Dictionary<char, char>();
    var tToS = new Dictionary<char, char>();

    for (int i = 0; i < s.Length; i++) {
        char c1 = s[i], c2 = t[i];

        if (sToT.ContainsKey(c1)) {
            if (sToT[c1] != c2) return false;
        } else {
            sToT[c1] = c2;
        }

        if (tToS.ContainsKey(c2)) {
            if (tToS[c2] != c1) return false;
        } else {
            tToS[c2] = c1;
        }
    }

    return true;
}

Trace: s = "badc", t = "baba"

1
i=0: 'b'→'b'. sToT['b']='b', tToS['b']='b'
2
i=1: 'a'→'a'. sToT['a']='a', tToS['a']='a'
3
i=2: 'd'→'b'. sToT['d']='b', tToS['b'] exists, is 'b'≠'d'? tToS['b'] should be 'd'. Conflict! Return false.

Edge Cases

Valid Cases
s = "egg", t = "add" → true
s = "badc", t = "baba" → false
Constraints
Must check both directions (s→t and t→s)
One-many or many-one mappings invalid

Complexity

Time
O(n)
Space
O(1)

Max 26 keys per dict (alphabet)

8. First Non-Repeating Character

Find first char appearing once. LeetCode 387 ★

Frequency O(n) Two Pass
8

Problem Statement

Given string s, return index of first char appearing only once. Return -1 if none.

Approach: Count, then Scan

Pass 1: Count frequency of all chars. Pass 2: Scan left-to-right, return index of first char with frequency 1.

C#
public int FirstUniqChar(string s)
{
    var freq = new Dictionary<char, int>();

    // Count frequencies
    foreach (char c in s) {
        if (!freq.TryGetValue(c, out int count)) count = 0;
        freq[c] = count + 1;
    }

    // Find first non-repeating
    for (int i = 0; i < s.Length; i++) {
        if (freq[s[i]] == 1)
            return i;
    }

    return -1;
}

Trace: s = "leetcode"

1
freq = {l:1, e:3, t:1, c:1, o:1, d:1}
2
Scan: i=0 'l' freq=1. Return 0.

Edge Cases

Valid Cases
s = "aab" → 2 (b)
s = "aabb" → -1
s = "a" → 0
Constraints
Return index, not char
First occurrence (left-to-right scan matters)

Complexity

Time
O(n)
Space
O(1)

Max 26 keys (lowercase letters)

9. Subarray Sum Equals K

Count subarrays with sum = k. LeetCode 560 ★★

Prefix Sum O(n) Elegant
9

Problem Statement

Given array of integers and integer k, return count of contiguous subarrays with sum = k.

Approach: Prefix Sum + Hash Map

Track cumulative sums. For each position, check if (cumSum - k) exists in map. If yes, all subarrays ending here with that prefix sum have target sum.

C#
public int SubarraySum(int[] nums, int k)
{
    var sumCount = new Dictionary<int, int>();
    sumCount[0] = 1; // Base case

    int cumSum = 0, count = 0;

    foreach (int num in nums) {
        cumSum += num;
        int target = cumSum - k;

        if (sumCount.TryGetValue(target, out int freq))
            count += freq;

        if (!sumCount.TryGetValue(cumSum, out int c)) c = 0;
        sumCount[cumSum] = c + 1;
    }

    return count;
}

Trace: nums = [1,1,1], k = 2

1
sumCount = {0:1}. cumSum = 0, count = 0
2
num=1: cumSum=1, target=1-2=-1. Not found. sumCount = {0:1, 1:1}
3
num=1: cumSum=2, target=2-2=0. Found! count += sumCount[0]=1. sumCount = {0:1, 1:1, 2:1}
4
num=1: cumSum=3, target=3-2=1. Found! count += sumCount[1]=1. count=2. Return 2.

Edge Cases

Valid Cases
k = sum of entire array → 1
Negative numbers work
No valid subarrays → 0
Constraints
Initialize sumCount[0]=1 (empty prefix)
Multiple subarrays can match (count all)

Complexity

Time
O(n)
Space
O(n)
10. 4Sum II

Count quadruplets with sum = 0. LeetCode 454 ★★

Two-Pair Hash O(n²) Split
10

Problem Statement

Given 4 arrays nums1-4, count tuples (i,j,k,l) where nums1[i]+nums2[j]+nums3[k]+nums4[l]=0.

Approach: Split into Two Pairs

Compute all pair sums from nums1+nums2, store in map. For each pair from nums3+nums4, check if its negation exists in map.

C#
public int FourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4)
{
    var sumMap = new Dictionary<int, int>();

    // Store all pair sums from nums1 + nums2
    for (int i = 0; i < nums1.Length; i++) {
        for (int j = 0; j < nums2.Length; j++) {
            int sum = nums1[i] + nums2[j];
            if (!sumMap.TryGetValue(sum, out int c)) c = 0;
            sumMap[sum] = c + 1;
        }
    }

    int count = 0;
    for (int k = 0; k < nums3.Length; k++) {
        for (int l = 0; l < nums4.Length; l++) {
            int needed = -(nums3[k] + nums4[l]);
            if (sumMap.TryGetValue(needed, out int cnt))
                count += cnt;
        }
    }

    return count;
}

Trace: nums1=[1,0], nums2=[-1,0], nums3=[-1,0], nums4=[0,1]

1
Pair sums (nums1+nums2): 1-1=0, 1+0=1, 0-1=-1, 0+0=0. sumMap = {0:2, 1:1, -1:1}
2
nums3[0]+nums4[0]=-1+0=-1. needed=1. Found! count+=1
3
nums3[0]+nums4[1]=-1+1=0. needed=0. Found! count+=2
4
nums3[1]+nums4[0]=0+0=0. needed=0. Found! count+=2
5
nums3[1]+nums4[1]=0+1=1. needed=-1. Found! count+=1. Return 6.

Edge Cases

Valid Cases
All zeros [0,0,0,0,0,0,0,0] → many
No valid quadruplets → 0
Constraints
Duplicates allowed in arrays
Need to count multiple occurrences

Complexity

Time
O(n²)
Space
O(n²)

n = array length (all same size)

11. LRU Cache

Implement Least-Recently-Used cache. LeetCode 146 ★★★

Dict + LinkedList O(1) per op Hard
11

Problem Statement

Design cache with get(key) and put(key,val). Both O(1). Evict least-recently-used when capacity exceeded.

Approach: Hash Map + Doubly Linked List

Dict maps key→node. LinkedList maintains LRU order (most recent at tail). On access, move to tail. On full, evict head.

C#
public class LRUCache
{
    private class Node
    {
        public int Key, Val;
        public Node Prev, Next;
    }

    private Node head, tail;
    private Dictionary<int, Node> cache;
    private int capacity;

    public LRUCache(int capacity)
    {
        this.capacity = capacity;
        cache = new Dictionary<int, Node>();
        head = new Node();
        tail = new Node();
        head.Next = tail;
        tail.Prev = head;
    }

    public int Get(int key)
    {
        if (!cache.ContainsKey(key)) return -1;
        Node node = cache[key];
        MoveToTail(node);
        return node.Val;
    }

    public void Put(int key, int value)
    {
        if (cache.ContainsKey(key)) {
            Node node = cache[key];
            node.Val = value;
            MoveToTail(node);
        } else {
            Node newNode = new Node { Key = key, Val = value };
            cache[key] = newNode;
            AddToTail(newNode);

            if (cache.Count > capacity) {
                Node lru = head.Next;
                Remove(lru);
                cache.Remove(lru.Key);
            }
        }
    }

    private void MoveToTail(Node node)
    {
        Remove(node);
        AddToTail(node);
    }

    private void AddToTail(Node node)
    {
        node.Prev = tail.Prev;
        node.Next = tail;
        tail.Prev.Next = node;
        tail.Prev = node;
    }

    private void Remove(Node node)
    {
        node.Prev.Next = node.Next;
        node.Next.Prev = node.Prev;
    }
}

Trace: Capacity 2, Put(1,1), Put(2,2), Get(1), Put(3,3), Get(2)

1
Put(1,1): cache={1}, list=[1]
2
Put(2,2): cache={1,2}, list=[1,2]
3
Get(1): found, move to tail. list=[2,1]. Return 1
4
Put(3,3): new node, full, evict 2. cache={1,3}, list=[1,3]
5
Get(2): not found. Return -1

Edge Cases

Valid Cases
capacity=1: only one key at a time
repeated Get updates LRU
Constraints
Must maintain both forward/backward pointers
Dummy head/tail prevent edge case null checks

Complexity

Get/Put
O(1)
Space
O(capacity)
12. Word Pattern

Check if word sequence matches pattern bijection. LeetCode 290 ★

Bijection O(n) Two Maps
12

Problem Statement

Given pattern and words, check if words follow pattern with bijection. E.g., pattern="abba", words=["redbluebluered"] should match.

Approach: Two-Way Mapping

Map pattern char→word and word→char. Ensure one-to-one correspondence. Reject if mapping conflicts arise.

C#
public bool WordPattern(string pattern, string s)
{
    string[] words = s.Split(' ');

    if (pattern.Length != words.Length) return false;

    var patToWord = new Dictionary<char, string>();
    var wordToPat = new Dictionary<string, char>();

    for (int i = 0; i < pattern.Length; i++) {
        char c = pattern[i];
        string w = words[i];

        if (patToWord.ContainsKey(c)) {
            if (patToWord[c] != w) return false;
        } else {
            patToWord[c] = w;
        }

        if (wordToPat.ContainsKey(w)) {
            if (wordToPat[w] != c) return false;
        } else {
            wordToPat[w] = c;
        }
    }

    return true;
}

Trace: pattern="abba", s="red blue blue red"

1
words = ["red","blue","blue","red"]. Lengths match.
2
i=0: 'a'→"red", "red"→'a'
3
i=1: 'b'→"blue", "blue"→'b'
4
i=2: 'b'→"blue" (exists, matches), "blue"→'b' (exists, matches)
5
i=3: 'a'→"red" (exists, matches), "red"→'a' (exists, matches). Return true.

Edge Cases

Valid Cases
pattern="a", s="dog" → true
pattern="ab", s="dog cat" → true
pattern="ab", s="dog dog" → false (collision)
Constraints
Length mismatch returns false early
Check both directions (two maps)

Complexity

Time
O(n)
Space
O(n)

n = pattern length

13. Happy Number

Detect if repeated sum-of-squares reaches 1 or cycles. LeetCode 202 ★

Cycle Detection O(log n) HashSet
13

Problem Statement

Repeat: replace number with sum of squares of digits. Return true if reaches 1, false if cycles.

Approach: Cycle Detection with HashSet

Track seen sums. If we revisit a sum, we're in a cycle (not happy). If reach 1, happy.

C#
public bool IsHappy(int n)
{
    var seen = new HashSet<int>();

    while (n != 1 && !seen.Contains(n)) {
        seen.Add(n);
        n = GetNext(n);
    }

    return n == 1;
}

private int GetNext(int n)
{
    int sum = 0;
    while (n > 0) {
        int digit = n % 10;
        sum += digit * digit;
        n /= 10;
    }
    return sum;
}

Trace: n = 19

1
n=19: 1²+9²=82. seen={19}
2
n=82: 8²+2²=68. seen={19,82}
3
n=68: 6²+8²=100. seen={19,82,68}
4
n=100: 1²+0²+0²=1. Return true.

Edge Cases

Valid Cases
n=1 → true (already 1)
n=2 → false (cycle: 2→4→16→37→58→89→145→42→20→4)
Constraints
Must detect cycle to avoid infinite loop
GetNext computes digit sum correctly

Complexity

Time
O(log n)
Space
O(log n)

Cycle size bounded by digit sum patterns

14. Intersection of Two Arrays II

Find common elements in two arrays. LeetCode 350 ★

Frequency O(n+m) Flexibility
14

Problem Statement

Given two arrays, return array of their intersection. Each element in result appears as many times as in both arrays.

Approach: Frequency Counting

Count freq of shorter array. Iterate longer array, matching and decrementing counts.

C#
public int[] Intersect(int[] nums1, int[] nums2)
{
    if (nums1.Length > nums2.Length)
        return Intersect(nums2, nums1);

    var freq = new Dictionary<int, int>();
    foreach (int n in nums1) {
        if (!freq.TryGetValue(n, out int c)) c = 0;
        freq[n] = c + 1;
    }

    var result = new List<int>();
    foreach (int n in nums2) {
        if (freq.TryGetValue(n, out int cnt) && cnt > 0) {
            result.Add(n);
            freq[n]--;
        }
    }

    return result.ToArray();
}

Trace: nums1 = [1,2,2,1], nums2 = [2,2]

1
nums1 is shorter, swap handled. freq from nums1 = {1:2, 2:2}
2
Iterate nums2: n=2, freq[2]=2>0, add 2, freq[2]=1
3
n=2, freq[2]=1>0, add 2, freq[2]=0
4
result = [2,2]. Return [2,2].

Edge Cases

Valid Cases
Duplicates: [1,2,2,1], [2] → [2]
No intersection: [] or empty
Constraints
Count must be decremented (avoid double-counting)
Order of result may vary

Complexity

Time
O(n+m)
Space
O(min(n,m))
15. Find All Duplicates in Array

Find all duplicates in 1-n array in-place. LeetCode 442 ★★

Index Marking O(n) O(1) space
15

Problem Statement

Given array of n integers [1,n], find all duplicates appearing twice. Use O(1) space (in-place).

Approach: Index Marking Trick

Use array indices as hash. For each num, mark arr[num-1] as negative. If already negative, num is duplicate.

C#
public IList<int> FindDuplicates(int[] nums)
{
    var result = new List<int>();

    foreach (int num in nums) {
        int idx = Math.Abs(num) - 1;

        if (nums[idx] < 0) {
            // Already marked, so num is duplicate
            result.Add(Math.Abs(num));
        } else {
            // Mark as visited
            nums[idx] = -nums[idx];
        }
    }

    return result;
}

Trace: nums = [4,3,2,7,8,2,3,1]

1
num=4: idx=3, nums[3]=7>0, mark nums[3]=-7
2
num=3: idx=2, nums[2]=2>0, mark nums[2]=-2
3
num=2: idx=1, nums[1]=3>0, mark nums[1]=-3
4
num=7: idx=6, nums[6]=3>0, mark nums[6]=-3
5
num=8: idx=7, nums[7]=1>0, mark nums[7]=-1
6
num=2: idx=1, nums[1]=-3<0, add 2. result=[2]
7
num=3: idx=2, nums[2]=-2<0, add 3. result=[2,3]
8
num=1: idx=0, nums[0]=4>0, mark nums[0]=-4. Return [2,3].

Edge Cases

Valid Cases
Single duplicate: [1,1] → [1]
Multiple: [4,3,2,7,8,2,3,1] → [2,3]
Constraints
Modifies array (in-place). Must use Abs() to recover num
Only works with [1,n] range

Complexity

Time
O(n)
Space
O(1)

Excludes output list space