Solving problems with HashSet and Dictionary
Five patterns that turn O(n²) loops into a single O(n) pass: seen-before, counting, complement lookup, grouping by a key and set operations.
Pattern 1: have I seen this before?
HashSet you check each item once. HashSet.Add returns false when the value is already there, which makes the code very short.
string[] ids = { "B-12", "C-7", "B-40", "C-7", "S-3", "B-12" }; var seen = new HashSet<string>();var duplicates = new List<string>();foreach (var id in ids) if (!seen.Add(id)) // false: it was already in the set duplicates.Add(id); Console.WriteLine(string.Join(", ", duplicates));C-7, B-12
Pattern 2: counting
Dictionary<key, int> is a tally sheet. GetValueOrDefault returns 0 for a key that is not there yet, so one line counts everything.
string[] sections = { "IPE200", "HEA160", "IPE200", "SHS100", "IPE200", "HEA160" }; var count = new Dictionary<string, int>();foreach (var s in sections) count[s] = count.GetValueOrDefault(s) + 1; foreach (var (section, n) in count) Console.WriteLine($"{section}: {n}");IPE200: 3 HEA160: 2 SHS100: 1
Pattern 3: look up the partner (complement)
x, the partner you need is target − x. Keep a dictionary of the values seen so far and ask it for the partner.
static (int, int)? TwoSum(int[] a, int target){ var indexOf = new Dictionary<int, int>(); // value -> index for (int i = 0; i < a.Length; i++) { if (indexOf.TryGetValue(target - a[i], out int j)) return (j, i); // partner seen earlier indexOf[a[i]] = i; } return null;} int[] lengths = { 7, 2, 11, 4, 9, 6, 3, 8 };Console.WriteLine(TwoSum(lengths, 10));Console.WriteLine(TwoSum(lengths, 30)?.ToString() ?? "none");(3, 5) none
Pattern 4: group by a key
Dictionary<key, List<item>>. The trick is choosing a key that is equal for items that belong together. For anagrams (same letters, different order), sorting the letters gives such a key: “listen” and “silent” both become “eilnst”.
string[] words = { "listen", "silent", "enlist", "stone", "notes", "tones", "beam" }; var groups = new Dictionary<string, List<string>>();foreach (var w in words){ string key = new string(w.OrderBy(c => c).ToArray()); // sorted letters if (!groups.TryGetValue(key, out var list)) groups[key] = list = new List<string>(); list.Add(w);} foreach (var g in groups.Values) Console.WriteLine(string.Join(" ", g));listen silent enlist stone notes tones beam
GroupBy and ToLookup do exactly this internally.
Pattern 5: compare two collections
HashSet has set operations built in: ExceptWith (difference), IntersectWith (common items), UnionWith and SetEquals.
var designed = new HashSet<string> { "B-1", "B-2", "B-3", "B-4" };var installed = new HashSet<string> { "B-2", "B-4", "B-9" }; var missing = new HashSet<string>(designed);missing.ExceptWith(installed); // designed but not installed var unexpected = new HashSet<string>(installed);unexpected.ExceptWith(designed); // installed but not in the design Console.WriteLine("Missing: " + string.Join(", ", missing));Console.WriteLine("Not in design: " + string.Join(", ", unexpected));Missing: B-1, B-3 Not in design: B-9
| Question | Tool | Time |
|---|---|---|
HashSet.Add returns false | ||
Dictionary<key, int> | ||
Dictionary<value, index>, look up the complement | ||
Dictionary<key, List<T>> or GroupBy | ||
HashSet.ExceptWith |
LongestConsecutive(int[] a) that returns the length of the longest run of consecutive whole numbers, in any order in the array, in O(n). For { 100, 4, 200, 1, 3, 2 } the run 1, 2, 3, 4 gives 4. For { 9, 1, 8, 7, 3 } the answer is 3.
Show a hint
x − 1 is not in the set. From each start, count upwards while x + len is in the set. Each number is counted once in total.
Show the solution
static int LongestConsecutive(int[] a){ var set = new HashSet<int>(a); int best = 0; foreach (int x in set) { if (set.Contains(x - 1)) continue; // not the start of a run int len = 1; while (set.Contains(x + len)) len++; best = Math.Max(best, len); } return best;} Console.WriteLine(LongestConsecutive(new[] { 100, 4, 200, 1, 3, 2 }));Console.WriteLine(LongestConsecutive(new[] { 9, 1, 8, 7, 3 }));4 3
Remember
If an inner loop only searches, replace it with a hash lookup: O(n²) becomes O(n). HashSet.Addreturning false detects repeats;Dictionary+GetValueOrDefaultcounts.For pair problems, look up the complement in a dictionary of what you have already seen. Group with a well-chosen key; compare collections with ExceptWith/IntersectWith. The cost is O(n) extra memory.