Dictionary<TKey, TValue> is one of the most useful collections in C#, giving you fast, key-based lookups for everything from phonebooks to inventory systems.
This collection of 30 C# Dictionary exercises covers adding, updating, and removing entries, safely checking for keys, and iterating over key-value pairs to build real, practical mini-programs.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you get comfortable using dictionaries the way they’re meant to be used.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Safe Lookups:
TryGetValue(),ContainsKey(), and handling missing keys. - Iteration: Looping through
KeyValuePair<TKey, TValue>entries. - Practical Use: Frequency counters, grouping, and simple lookup tables.
- Fundamentals: Creating, adding, and accessing key-value pairs.
+ Table Of Contents (30 Exercises)
Table of contents
- Exercise 1: Word Counter
- Exercise 2: Phonebook Lookup
- Exercise 3: Unique ID Manager
- Exercise 4: Remove by Value
- Exercise 5: Update Grades
- Exercise 6: Dictionary Iteration
- Exercise 7: Clear and Count
- Exercise 8: Inventory Check
- Exercise 9: Keys and Values Lists
- Exercise 10: Case-Insensitive Search
- Exercise 11: Invert a Dictionary
- Exercise 12: Filter by Criteria
- Exercise 13: Highest and Lowest
- Exercise 14: Average Value Calculator
- Exercise 15: Group by Length
- Exercise 16: Dictionary Merging
- Exercise 17: Transform Values
- Exercise 18: Sort by Value
- Exercise 19: Lookup Conversion
- Exercise 20: Dictionary to JSON
- Exercise 21: Nested Dictionaries (Multi-Key)
- Exercise 22: Custom Equality Comparer
- Exercise 23: Thread-Safe Dictionary Cache
- Exercise 24: LRU (Least Recently Used) Cache
- Exercise 25: Graph Representation (Adjacency List)
- Exercise 26: State Machine
- Exercise 27: Frequency Tracker with Priority
- Exercise 28: Undo/Redo System Command History
- Exercise 29: Sparse Matrix Representation
- Exercise 30: Dependency Injection Container
Exercise 1: Word Counter
Practice Problem: Take a string of text and count the frequency of each word using a dictionary.
Purpose: This exercise helps you practice using a dictionary as a frequency counter, checking whether a key already exists with ContainsKey(), and updating or initializing a count depending on that check.
Given Input: string text = "the quick brown fox the lazy dog the fox";
Expected Output:
the: 3 quick: 1 brown: 1 fox: 2 lazy: 1 dog: 1
▼ Hint
- Split the input text on spaces to get an array of words.
- Loop through the words, and for each one check
ContainsKey(word)on the dictionary. - If the word already exists, increment its count; otherwise add it with a starting count of 1.
- Loop through the finished dictionary to print each word and its count.
▼ Solution & Explanation
Explanation:
text.Split(' '): Breaks the sentence into individual words wherever a space appears.counts.ContainsKey(word): Checks whether this word has already been seen before deciding how to update the count.counts[word]++: Increments the existing count for a word that has already appeared at least once.counts[word] = 1: Adds a brand new entry to the dictionary the first time a word is encountered.
Exercise 2: Phonebook Lookup
Practice Problem: Create a simple phonebook where you add names (keys) and phone numbers (values), then search for a specific name.
Purpose: This exercise helps you practice the most basic dictionary workflow: adding key-value pairs, then safely looking a key up with TryGetValue() instead of risking an exception on a missing key.
Given Input: phonebook.Add("Alice", "555-1234"); phonebook.Add("Bob", "555-5678");, then search for "Bob".
Expected Output: Bob's number is 555-5678
▼ Hint
- Use a
Dictionary<string, string>with names as keys and phone numbers as values. - Use
Add(name, number)to populate the phonebook. - Use
TryGetValue(name, out string number)to search, so a missing name doesn’t throw an exception.
▼ Solution & Explanation
Explanation:
phonebook.Add("Alice", "555-1234"): Inserts a new key-value pair into the dictionary.phonebook.TryGetValue(name, out string number): Attempts to find the name and, if found, places the matching number intonumberwhile returningtrue.return name + " was not found...": Handles the case whereTryGetValuereturnsfalse, avoiding aKeyNotFoundException.
Exercise 3: Unique ID Manager
Practice Problem: Create a program that stores employee IDs and names. Ensure that trying to add a duplicate ID throws a friendly warning using ContainsKey().
Purpose: This exercise helps you practice guarding against duplicate keys before calling Add(), since Add() throws an exception on a duplicate key while a manual check lets you handle it gracefully instead.
Given Input: AddEmployee(employees, 1001, "Alice"); AddEmployee(employees, 1002, "Bob"); AddEmployee(employees, 1001, "Charlie");
Expected Output:
Added employee 1001: Alice Added employee 1002: Bob Warning: Employee ID 1001 already exists.
▼ Hint
- Before adding, check
employees.ContainsKey(id). - If the ID already exists, print a warning message and return without adding.
- If the ID is new, call
Add(id, name)and confirm with a message.
▼ Solution & Explanation
Explanation:
employees.ContainsKey(id): Checks for an existing entry before attempting to add a new one.Console.WriteLine("Warning: ...")thenreturn: Reports the conflict and exits the method early, leaving the original entry untouched.employees.Add(id, name): Only runs when the ID is confirmed to be new, so it never throws a duplicate-key exception.
Exercise 4: Remove by Value
Practice Problem: Create a dictionary of products and prices. Write a method to remove all products that cost less than $5.00.
Purpose: This exercise helps you practice filtering a dictionary by its values, and shows why keys marked for removal must be collected first, since modifying a dictionary while iterating over it directly throws an exception.
Given Input: products = { "Gum": 1.50, "Soda": 2.00, "Sandwich": 6.50, "Chips": 3.25, "Coffee": 5.00 }
Expected Output:
Sandwich: 6.5 Coffee: 5
▼ Hint
- Loop through the dictionary and collect the keys of entries below the threshold into a separate list.
- Do not call
Remove()while iterating over the dictionary directly, this throws anInvalidOperationException. - After the first loop finishes, loop through the collected keys and call
Remove(key)for each one.
▼ Solution & Explanation
Explanation:
List<string> toRemove: Temporarily holds the keys that should be deleted, since the dictionary itself cannot be safely modified mid-iteration.if (pair.Value < threshold): Flags any product priced below the cutoff for removal.products.Remove(key): Runs in a second, separate loop, only after the first loop has finished reading the dictionary.
Exercise 5: Update Grades
Practice Problem: Given a dictionary of student names and their letter grades, write a program that updates a specific student’s grade, or adds them if they don’t exist, using TryGetValue().
Purpose: This exercise helps you practice the common “update or insert” pattern, using TryGetValue() to branch between updating an existing entry and adding a brand new one.
Given Input: grades.Add("Alice", "B"); grades.Add("Bob", "C"); UpdateGrade(grades, "Alice", "A"); UpdateGrade(grades, "Charlie", "B");
Expected Output:
Updated Alice's grade to A. Added Charlie with grade B.
▼ Hint
- Call
grades.TryGetValue(student, out string currentGrade)first. - If it returns
true, overwrite the value withgrades[student] = newGrade. - If it returns
false, callAdd(student, newGrade)instead.
▼ Solution & Explanation
Explanation:
grades.TryGetValue(student, out string currentGrade): Checks for an existing entry without risking an exception if the student is not yet in the dictionary.grades[student] = newGrade: Overwrites the value for an existing key using the indexer.grades.Add(student, newGrade): Runs only in the else branch, inserting a brand new student who was not previously tracked.
Exercise 6: Dictionary Iteration
Practice Problem: Create a dictionary of city names and their populations. Print them all out in the format: "City: [Name], Population: [Count]".
Purpose: This exercise helps you practice iterating over a dictionary with foreach using KeyValuePair<TKey, TValue>, and formatting each pair’s key and value into a readable string.
Given Input: cities.Add("Tokyo", 37400000); cities.Add("Delhi", 30300000); cities.Add("Shanghai", 27058000);
Expected Output:
City: Tokyo, Population: 37400000 City: Delhi, Population: 30300000 City: Shanghai, Population: 27058000
▼ Hint
- Use a
foreach (KeyValuePair<string, int> city in cities)loop. - Inside the loop, access
city.Keyandcity.Valueseparately. - Build the output string in the exact requested format for each entry.
▼ Solution & Explanation
Explanation:
foreach (KeyValuePair<string, int> city in cities): Iterates over every entry in the dictionary, exposing both the key and value together as one pair.city.Key: Refers to the city name for the current entry.city.Value: Refers to the population number for the current entry.
Exercise 7: Clear and Count
Practice Problem: Write a program that fills a dictionary with temporary configuration settings, prints the count, clears the dictionary, and verifies that the count is now 0.
Purpose: This exercise helps you practice the Count property and the Clear() method, useful whenever you need to reset a dictionary’s state without creating a brand new instance.
Given Input: settings.Add("Theme", "Dark"); settings.Add("Timeout", "30"); settings.Add("Language", "en-US");
Expected Output:
Count before clear: 3 Count after clear: 0
▼ Hint
Use the dictionary’s Count property before and after calling Clear() to compare the two values.
▼ Solution & Explanation
Explanation:
settings.Count: Reports how many key-value pairs are currently stored in the dictionary.settings.Clear(): Removes every entry from the dictionary in one call, without replacing the dictionary instance itself.- Second
Console.WriteLine: Confirms the dictionary is now empty by readingCountagain after clearing.
Exercise 8: Inventory Check
Practice Problem: Create a dictionary representing store inventory (Item -> Quantity). Check if a specific item is in stock and has a quantity greater than 10.
Purpose: This exercise helps you practice combining a TryGetValue() lookup with an additional condition on the retrieved value, a common pattern when a simple key check is not enough.
Given Input: inventory.Add("Widget", 25); inventory.Add("Gadget", 4);, then check "Widget", "Gadget", and "Gizmo".
Expected Output:
Widget is in stock with sufficient quantity (25). Gadget is in stock but low (4). Gizmo is not in the inventory.
▼ Hint
- Use
TryGetValue(item, out int quantity)to see if the item exists at all. - If it exists, compare
quantityagainst 10 to decide which message to print. - If it does not exist, print a separate “not in inventory” message.
▼ Solution & Explanation
Explanation:
inventory.TryGetValue(item, out int quantity): Checks for the item and, if present, retrieves its quantity in the same step.quantity > 10: Applies the extra business rule on top of the existence check, distinguishing “in stock” from “in stock but low”.- else branch: Handles an item that was never added to the dictionary at all.
Exercise 9: Keys and Values Lists
Practice Problem: Extract all the keys of a dictionary into a List<string> and all the values into a List<int> without using a foreach loop.
Purpose: This exercise helps you practice the Keys and Values properties of a dictionary, and shows how to build a List<T> directly from them using a constructor instead of manually looping.
Given Input: scores = { "Alice": 90, "Bob": 85, "Charlie": 78 }
Expected Output:
Keys: Alice, Bob, Charlie Values: 90, 85, 78
▼ Hint
- A dictionary exposes its keys through the
Keysproperty and its values through theValuesproperty. - Pass
dictionary.Keysdirectly into theList<string>constructor instead of looping manually. - Do the same for
dictionary.Valueswith aList<int>. - Use
string.Join(", ", list)to print each list on one line.
▼ Solution & Explanation
Explanation:
new List<string>(scores.Keys): Builds a new list directly from the dictionary’s key collection, with no explicit loop written.new List<int>(scores.Values): Does the same thing for the values, producing an independent list of numbers.string.Join(", ", keys): Combines every element of the list into a single comma-separated string for display.
Exercise 10: Case-Insensitive Search
Practice Problem: Create a dictionary where the keys are usernames. Configure it so that looking up "john_doe", "John_Doe", or "JOHN_DOE" all return the same user data.
Purpose: This exercise helps you practice supplying a custom IEqualityComparer<string> to a dictionary’s constructor, letting you change how keys are compared without changing any of the lookup code.
Given Input: users.Add("john_doe", "John Doe - Admin");, then look up "john_doe", "John_Doe", and "JOHN_DOE".
Expected Output:
john_doe -> John Doe - Admin John_Doe -> John Doe - Admin JOHN_DOE -> John Doe - Admin
▼ Hint
- Pass
StringComparer.OrdinalIgnoreCaseinto theDictionary<string, string>constructor. - Add the username once, in whatever casing you like.
- Look the same user up using different casings to confirm they all resolve to the same entry.
▼ Solution & Explanation
Explanation:
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase): Tells the dictionary to compare keys ignoring case, instead of the default case-sensitive comparison.users.Add("john_doe", ...): Stores the entry once, under a single canonical casing.users["John_Doe"]andusers["JOHN_DOE"]: Both resolve to the same stored entry, since the comparer treats all three casings as equal keys.
Exercise 11: Invert a Dictionary
Practice Problem: Take a dictionary of English words and their Spanish translations, and swap the keys and values, assuming all translations are unique.
Purpose: This exercise helps you practice using LINQ’s ToDictionary() to rebuild a dictionary with the keys and values reversed, and highlights why the original values must be unique for this to work safely.
Given Input: translations = { "hello": "hola", "goodbye": "adios", "please": "por favor" }
Expected Output:
hola -> hello adios -> goodbye por favor -> please
▼ Hint
- Use LINQ’s
ToDictionary()on the source dictionary. - Supply
pair => pair.Valueas the new key selector. - Supply
pair => pair.Keyas the new value selector. - Remember this only works cleanly if the original values contain no duplicates.
▼ Solution & Explanation
Explanation:
source.ToDictionary(pair => pair.Value, pair => pair.Key): Builds a brand new dictionary, using the old values as new keys and the old keys as new values.- First lambda,
pair => pair.Value: Selects what becomes the key in the resulting dictionary. - Second lambda,
pair => pair.Key: Selects what becomes the value in the resulting dictionary.
Exercise 12: Filter by Criteria
Practice Problem: Given a dictionary of car models and their top speeds, use LINQ to create a new dictionary containing only cars that can go over 150 mph.
Purpose: This exercise helps you practice chaining Where() and ToDictionary() together to filter a dictionary by its values while producing a brand new dictionary rather than mutating the original.
Given Input: cars = { "Civic": 124, "Corvette": 184, "Veyron": 254, "Model 3": 162, "Camry": 135 }
Expected Output:
Corvette: 184 Veyron: 254 Model 3: 162
▼ Hint
- Call
Where(pair => pair.Value > 150)on the dictionary first. - Chain
ToDictionary(pair => pair.Key, pair => pair.Value)onto the filtered result. - Remember that
Where()alone returns a sequence of pairs, not a dictionary, so it needs to be converted back.
▼ Solution & Explanation
Explanation:
cars.Where(pair => pair.Value > minSpeed): Filters the dictionary down to only the pairs whose speed exceeds the threshold..ToDictionary(pair => pair.Key, pair => pair.Value): Converts the filtered sequence of pairs back into an actual dictionary.- Original
carsdictionary: Remains completely unchanged, since a new dictionary is returned rather than modifying it in place.
Exercise 13: Highest and Lowest
Practice Problem: Find the key with the highest value and the key with the lowest value in a dictionary of stock names and current prices.
Purpose: This exercise helps you practice combining OrderBy() or OrderByDescending() with First() to pull out the single extreme entry from a dictionary, rather than manually looping and tracking a running maximum.
Given Input: stocks = { "AAPL": 190.50, "GOOG": 140.25, "AMZN": 178.90, "TSLA": 250.75 }
Expected Output:
Highest: TSLA at 250.75 Lowest: GOOG at 140.25
▼ Hint
- Call
OrderByDescending(pair => pair.Value).First()to get the pair with the highest price. - Call
OrderBy(pair => pair.Value).First()to get the pair with the lowest price. - Each result is a full
KeyValuePair, so you can read both.Keyand.Valuefrom it directly.
▼ Solution & Explanation
Explanation:
OrderByDescending(pair => pair.Value): Sorts the pairs from highest price to lowest..First(): Grabs just the top entry from the sorted sequence, avoiding the need to materialize the whole sorted list.OrderBy(pair => pair.Value).First(): Mirrors the same idea in ascending order to find the lowest-priced stock.
Exercise 14: Average Value Calculator
Practice Problem: Given a dictionary of monthly expenses (Month to Amount), calculate the average monthly expenditure using LINQ.
Purpose: This exercise helps you practice using LINQ’s Average() directly on a dictionary’s Values collection, instead of manually summing and dividing by the count.
Given Input: expenses = { "Jan": 1200.00, "Feb": 950.50, "Mar": 1100.75, "Apr": 1300.25 }
Expected Output: Average expenditure = 1137.875
▼ Hint
Access the dictionary’s Values property and call .Average() directly on it, since Average() works on any sequence of numbers.
▼ Solution & Explanation
Explanation:
expenses.Values: Exposes just the numeric amounts from the dictionary, without the month names attached..Average(): Sums every value in the sequence and divides by the count in a single call.
Exercise 15: Group by Length
Practice Problem: Take a list of strings and group them into a Dictionary<int, List<string>> where the key is the length of the words and the value is a list of words of that length.
Purpose: This exercise helps you practice LINQ’s GroupBy(), which clusters items by a computed key, followed by ToDictionary() to turn those groups into an actual dictionary of lists.
Given Input: words = new List<string> { "cat", "dog", "fish", "ant", "lion", "owl" };
Expected Output:
Length 3: cat, dog, ant, owl Length 4: fish, lion
▼ Hint
- Call
GroupBy(word => word.Length)on the list of words. - Each resulting group has a
Key(the length) and can be enumerated like a list of matching words. - Chain
ToDictionary(group => group.Key, group => group.ToList())to convert the groups into the final dictionary.
▼ Solution & Explanation
Explanation:
GroupBy(word => word.Length): Clusters every word into a group keyed by its character length.group.Key: Refers to the shared length value for all words inside that particular group.group.ToList(): Converts each group’s contents into a concreteList<string>, which becomes the dictionary’s value.
Exercise 16: Dictionary Merging
Practice Problem: Merge two dictionaries of store inventories. If an item exists in both, sum their quantities.
Purpose: This exercise helps you practice combining two dictionaries into one while resolving key conflicts, a common task when consolidating data from multiple sources.
Given Input: storeA = { "Apples": 10, "Bananas": 5, "Oranges": 8 } and storeB = { "Bananas": 7, "Oranges": 2, "Grapes": 12 }
Expected Output:
Apples: 10 Bananas: 12 Oranges: 10 Grapes: 12
▼ Hint
- Start by copying the first dictionary into a new one using its constructor, so the original is left untouched.
- Loop through the second dictionary’s entries.
- If the key already exists in the merged dictionary, add the second dictionary’s quantity to it.
- If the key does not exist yet, add it as a brand new entry.
▼ Solution & Explanation
Explanation:
new Dictionary<string, int>(storeA): Creates an independent copy of the first store, so merging never mutates the original dictionary passed in.merged.ContainsKey(pair.Key): Checks whether an item from the second store already exists in the merged result.merged[pair.Key] += pair.Value: Adds the second store’s quantity on top of the existing quantity when the item is shared between both stores.merged.Add(pair.Key, pair.Value): Inserts items that only existed in the second store as brand new entries.
Exercise 17: Transform Values
Practice Problem: Given a dictionary of item prices, use LINQ to create a new dictionary where all prices have a 10% sales tax applied.
Purpose: This exercise helps you practice transforming a dictionary’s values while keeping its keys the same, using ToDictionary() with a computed value selector.
Given Input: prices = { "Book": 15.00, "Pen": 2.50, "Notebook": 4.75 }
Expected Output:
Book: 16.5 Pen: 2.75 Notebook: 5.225
▼ Hint
- Call
ToDictionary(pair => pair.Key, pair => pair.Value * (1 + taxRate))on the original dictionary. - Keep the key selector unchanged, since only the prices need to be transformed.
- Pass the tax rate as a decimal, such as
0.10for 10 percent.
▼ Solution & Explanation
Explanation:
pair => pair.Key: Leaves the item name untouched in the resulting dictionary.pair.Value * (1 + taxRate): Computes the new price by adding the tax percentage on top of the original price.- Original
pricesdictionary: Stays untouched, sinceToDictionary()always produces a separate result.
Exercise 18: Sort by Value
Practice Problem: Take a dictionary of video game titles and their review scores, and sort it in descending order based on the scores.
Purpose: This exercise helps you practice sorting a dictionary’s entries with OrderByDescending(), and shows that a dictionary itself has no inherent order, so sorting always produces a separate ordered sequence.
Given Input: games = { "Game A": 85, "Game B": 92, "Game C": 78, "Game D": 95 }
Expected Output:
Game D: 95 Game B: 92 Game A: 85 Game C: 78
▼ Hint
- Call
OrderByDescending(pair => pair.Value)on the dictionary. - The result is an ordered sequence of pairs, not a dictionary, since dictionaries themselves don’t preserve a sort order.
- Loop through that ordered sequence directly to print the results in order.
▼ Solution & Explanation
Explanation:
OrderByDescending(pair => pair.Value): Produces a new sequence of the same pairs, arranged from the highest score to the lowest.IOrderedEnumerable<KeyValuePair<string, int>>: The specific return type of an ordering operation, which still supportsforeachlike any other sequence.- Original
gamesdictionary: Remains in whatever order it always had internally; only the separate sorted sequence reflects the new order.
Exercise 19: Lookup Conversion
Practice Problem: Convert a standard List<Employee>, where Employee has a Department property, into a lookup or dictionary grouped by department using LINQ’s .ToDictionary() or .GroupBy().
Purpose: This exercise helps you practice turning a flat list of objects into a dictionary of grouped lists, a very common step when preparing data for reporting or display by category.
Given Input: A list of five employees across the Engineering, Sales, and Marketing departments.
Expected Output:
Engineering: Alice, Charlie Sales: Bob, Eve Marketing: Diana
▼ Hint
- Call
GroupBy(emp => emp.Department)on the list of employees first. - Chain
ToDictionary(group => group.Key, group => group.ToList())to turn the groups into a dictionary of lists. - Use
.Select(emp => emp.Name)when printing, to pull just the names out of each group’s employee list.
▼ Solution & Explanation
Explanation:
GroupBy(emp => emp.Department): Clusters every employee into a group keyed by their department name.ToDictionary(group => group.Key, group => group.ToList()): Converts each department group into a dictionary entry whose value is a list of the employees in it.pair.Value.Select(emp => emp.Name): Pulls just the employee names out of each department’s list for display.
Exercise 20: Dictionary to JSON
Practice Problem: Manually convert a basic Dictionary<string, string> into a valid JSON string format, without using an external library like Json.NET.
Purpose: This exercise helps you practice building a formatted string by hand with a StringBuilder, and reinforces exactly what a simple JSON object literal looks like under the hood.
Given Input: data = { "name": "Alice", "city": "Paris" }
Expected Output: {"name":"Alice","city":"Paris"}
▼ Hint
- Use a
StringBuilderand start by appending an opening curly brace. - Loop through the dictionary, appending each pair as
"key":"value". - Track whether you are on the first pair so you only add a comma separator before every pair after the first.
- Finish by appending a closing curly brace.
▼ Solution & Explanation
Explanation:
sb.Append("{"): Starts the JSON object with its required opening brace.bool first: Tracks whether a comma separator is needed before the current pair, so there’s no trailing or leading comma."\"" + pair.Key + "\":\"" + pair.Value + "\"": Wraps both the key and the value in escaped double quotes, matching valid JSON string syntax.sb.Append("}"): Closes the JSON object once every pair has been appended.
Exercise 21: Nested Dictionaries (Multi-Key)
Practice Problem: Create a nested dictionary structure to represent a university system: Dictionary<string, Dictionary<string, List<int>>> (Department to Course ID to List of Student Grades). Write a method to calculate the average grade for a specific course.
Purpose: This exercise helps you practice working with dictionaries nested several levels deep, and shows how to safely drill down through multiple TryGetValue() calls instead of risking an exception at any level.
Given Input: A university dictionary with a "ComputerScience" department containing course "CS101" with grades { 85, 90, 78, 92 }.
Expected Output: Average grade for CS101 = 86.25
▼ Hint
- First call
TryGetValue(department, ...)on the outer dictionary to get the inner course dictionary. - Then call
TryGetValue(courseId, ...)on that inner dictionary to get the list of grades. - Chain both lookups together with
&&so the method only proceeds if both keys exist. - Use LINQ’s
Average()on the resulting list of grades.
▼ Solution & Explanation
Explanation:
Dictionary<string, Dictionary<string, List<int>>>: Models a three-level hierarchy, department to course to grades, entirely with generic collections.university.TryGetValue(department, ...) && courses.TryGetValue(courseId, ...): Drills down safely through both levels, short-circuiting if either the department or the course does not exist.grades.Average(): Computes the mean of the retrieved grade list once both lookups succeed.
Exercise 22: Custom Equality Comparer
Practice Problem: Create a custom class Point with X and Y coordinates. Implement a custom IEqualityComparer<Point> so that a Dictionary<Point, string> treats points with the same coordinates as identical keys.
Purpose: This exercise helps you practice supplying a custom IEqualityComparer<T> to a dictionary, since without one, two separate Point objects with identical coordinates would be treated as different keys based on reference identity.
Given Input: labels.Add(new Point(1, 2), "Origin marker");, then check labels.ContainsKey(new Point(1, 2)) using a brand new Point instance.
Expected Output: Contains (1,2)? True
▼ Hint
- Implement
IEqualityComparer<Point>with anEquals(Point a, Point b)method that comparesXandYon both objects. - Implement
GetHashCode(Point point)so that points with equal coordinates always produce the same hash code. - Pass an instance of your comparer into the
Dictionary<Point, string>constructor. - Without the custom comparer, two different
Pointobjects with the same coordinates would never be considered equal keys.
▼ Solution & Explanation
Explanation:
Equals(Point a, Point b): Defines coordinate equality explicitly, sincePointitself doesn’t override the default reference-based equality.GetHashCode(Point point): Must agree withEquals, so two points considered equal always hash to the same bucket inside the dictionary.new Dictionary<Point, string>(new PointComparer()): Tells the dictionary to use the custom comparer for every key lookup instead of default reference equality.labels.ContainsKey(new Point(1, 2)): Succeeds even though this is a completely different object instance, because the comparer only checks coordinates.
Exercise 23: Thread-Safe Dictionary Cache
Practice Problem: Simulate a multi-threaded environment where multiple threads read and write to a shared cache. Implement ConcurrentDictionary<TKey, TValue> and demonstrate the use of GetOrAdd().
Purpose: This exercise helps you practice using ConcurrentDictionary<TKey, TValue> instead of a plain Dictionary, since a plain dictionary is not safe to read and write from multiple threads at once without external locking.
Given Input: Five parallel tasks each call cache.GetOrAdd(i, key => "Value-" + key) for keys 0 through 4.
Expected Output:
Total cached entries: 5 Value for key 3: Value-3
▼ Hint
- Use
ConcurrentDictionary<int, string>instead of a plainDictionary<int, string>. - Use
Parallel.For()to simulate several threads running at the same time. - Inside the loop body, call
cache.GetOrAdd(key, factory), which atomically adds a value only if the key is not already present. - Since threads may finish in any order, only check things that don’t depend on timing, like the final count or a specific key’s value.
▼ Solution & Explanation
Explanation:
ConcurrentDictionary<int, string>: Handles its own internal locking, so multiple threads can safely read and write without corrupting the collection.Parallel.For(0, 5, i => ...): Runs the loop body across multiple threads at once, simulating concurrent access.cache.GetOrAdd(i, key => "Value-" + key): Atomically checks for the key and adds it if missing, all in one thread-safe operation, avoiding the check-then-add race condition a plain dictionary would have.cache[3]: Reads back a specific entry after all parallel work has finished, which is safe to check regardless of the order threads actually ran in.
Exercise 24: LRU (Least Recently Used) Cache
Practice Problem: Build a simple LRU cache using a combination of a Dictionary, for O(1) lookups, and a LinkedList, to track usage order.
Purpose: This exercise helps you practice combining two different collections to get properties neither one offers alone: a dictionary gives fast lookups but no ordering, while a linked list gives ordering but slow lookups by key.
Given Input: A cache with capacity 2. Put “a”, put “b”, access “a”, then put “c”.
Expected Output:
Contains a? True Contains b? False Contains c? True
▼ Hint
- Store a
Dictionary<TKey, LinkedListNode<...>>mapping each key directly to its node in the linked list. - Keep the linked list ordered so the front represents the most recently used entry and the back represents the least recently used.
- On every access or update, remove the node from its current position and re-insert it at the front.
- When adding a new key would exceed capacity, remove the node at the back of the list, along with its dictionary entry.
▼ Solution & Explanation
Explanation:
_map: Gives instant O(1) access to any node by key, avoiding a slow linear scan through the linked list._order.AddFirst(newNode): Places the most recently touched entry at the front of the list every time it is put or accessed._order.Lastand_order.RemoveLast(): Identify and evict the least recently used entry once the cache is full.TryGet: Not only retrieves a value, but also refreshes its position at the front, marking it as recently used.
Exercise 25: Graph Representation (Adjacency List)
Practice Problem: Represent a social network graph using a Dictionary<string, HashSet<string>> where the key is a person and the value is the set of their friends. Write a function to find mutual friends.
Purpose: This exercise helps you practice pairing a dictionary with HashSet<T> values, and shows how Intersect() makes finding common elements between two sets straightforward.
Given Input: "Alice" is friends with Bob, Charlie, and Diana. "Bob" is friends with Alice, Charlie, and Eve.
Expected Output: Mutual friends: Charlie
▼ Hint
- Look up both people’s friend sets with
TryGetValue(), returning an empty result if either person is missing from the graph. - Call LINQ’s
Intersect()on the two friend sets to find the friends they have in common. - Wrap the result back into a
HashSet<string>so duplicates are impossible and set operations remain available.
▼ Solution & Explanation
Explanation:
Dictionary<string, HashSet<string>>: Models each person as a key whose value is the unique set of their friends, a natural fit for an adjacency list.!graph.TryGetValue(...) || !graph.TryGetValue(...): Bails out early with an empty set if either person doesn’t exist in the graph at all.friendsA.Intersect(friendsB): Returns only the friends that appear in both sets, which is exactly the definition of a mutual friend.
Exercise 26: State Machine
Practice Problem: Implement a simple turn-based game state machine using a Dictionary<Tuple<State, Trigger>, State> to define valid state transitions.
Purpose: This exercise helps you practice using a Tuple as a composite dictionary key, letting a single lookup capture “given this state and this trigger, what state comes next” in one step.
Given Input: Starting from State.Idle, fire the triggers Start, Pause, Resume, then End in sequence.
Expected Output:
State: Playing State: Paused State: Playing State: GameOver
▼ Hint
- Define
StateandTriggeras enums representing the possible states and actions. - Build a dictionary keyed by
Tuple.Create(currentState, trigger), with the resulting next state as the value. - To fire a trigger, build the same tuple key from the current state and look it up with
TryGetValue(). - Throw an exception if the combination of current state and trigger has no valid transition defined.
▼ Solution & Explanation
Explanation:
Dictionary<Tuple<State, Trigger>, State>: Uses a two-part tuple as a single composite key, so a lookup only succeeds for an exact, valid state-and-trigger combination.Tuple.Create(current, trigger): Builds the same kind of key used when the transition table was defined, so the lookup lines up correctly.throw new InvalidOperationException(...): Rejects any trigger that has no defined transition from the current state, preventing the game from entering an undefined state.
Exercise 27: Frequency Tracker with Priority
Practice Problem: Create a data structure that tracks the frequency of elements added to it, and can return the top K most frequent elements using a dictionary paired with a sorting step.
Purpose: This exercise helps you practice using a dictionary purely as a counter, then layering LINQ’s OrderByDescending() and Take() on top of it whenever a ranked view of the data is needed.
Given Input: { "apple", "banana", "apple", "cherry", "apple", "banana", "date" }, requesting the top 2 most frequent items.
Expected Output: Top 2: apple, banana
▼ Hint
- Keep a
Dictionary<string, int>where eachAdd(item)call increments that item’s count. - For
GetTopK(k), callOrderByDescending(pair => pair.Value)on the dictionary. - Chain
Take(k)to limit the result, thenSelect(pair => pair.Key)to pull out just the item names.
▼ Solution & Explanation
Explanation:
_counts[item]++and_counts[item] = 1: Increment an existing count or start a new one at 1, exactly like a basic frequency counter.OrderByDescending(pair => pair.Value): Ranks every tracked item from most frequent to least frequent.Take(k): Trims the ranked sequence down to just the topkentries.
Exercise 28: Undo/Redo System Command History
Practice Problem: Use a dictionary to map specific command string names to executable Action delegates, implementing a dynamic command pattern execution engine with undo support.
Purpose: This exercise helps you practice storing behavior itself, not just data, as dictionary values, and shows how pairing a command history stack with matching undo delegates enables reverting the most recent action.
Given Input: Register "increment" and "addTen" commands, execute both, then call Undo() once.
Expected Output:
Counter after commands: 11 Counter after undo: 1
▼ Hint
- Keep two dictionaries: one mapping command names to their “do”
Action, and one mapping them to their matching “undo”Action. - Keep a
Stack<string>of executed command names, pushing a new name every time a command runs. - For
Undo(), pop the most recent command name off the stack and invoke its matching undo action. - Register commands as pairs of lambdas, one for the forward action and one that reverses it.
▼ Solution & Explanation
Explanation:
Dictionary<string, Action> _commands: Stores actual executable behavior keyed by a friendly command name, instead of a long if-else chain._history.Push(name): Records every executed command in order, so the most recent one can be found again for undoing._history.Pop(): Retrieves and removes the most recently executed command’s name whenUndo()is called._undoCommands.TryGetValue(lastCommand, ...): Looks up and runs the specific action that reverses whatever the last command did.
Exercise 29: Sparse Matrix Representation
Practice Problem: Represent a massive, mostly empty 10,000 by 10,000 grid using a Dictionary<Tuple<int, int>, double> to efficiently save memory, and write a method to calculate the dot product of two rows.
Purpose: This exercise helps you practice a sparse representation pattern, storing only the non-zero cells of a huge grid instead of allocating a full two-dimensional array that would waste enormous amounts of memory.
Given Input: Row 0 has values at columns 5 and 9998. Row 1 has values at the same two columns.
Expected Output: Dot product of row 0 and row 1 = 12.5
▼ Hint
- Use
Tuple.Create(row, col)as the dictionary key, storing only cells that have a non-zero value. - Write a
Get(row, col)helper that returns 0 for any coordinate not present in the dictionary. - For the dot product, only loop through the entries that actually belong to one of the two rows, instead of looping through all 10,000 columns.
- For each stored entry in that row, multiply its value by the corresponding column’s value in the other row and add it to a running total.
▼ Solution & Explanation
Explanation:
Dictionary<Tuple<int, int>, double> _values: Stores only the coordinates that actually have a value, instead of the ten billion cells a full array would require.Get(row, col): Returns 0 by convention for any coordinate that was never explicitly set, matching how a mostly-empty grid should behave.entry.Key.Item1 == rowA: Filters down to only the stored cells belonging to the row being multiplied, avoiding a full 10,000-column scan.sum += entry.Value * Get(rowB, col): Multiplies matching column values from each row together and accumulates the running total, exactly like a standard dot product.
Exercise 30: Dependency Injection Container
Practice Problem: Build a bare-bones Dependency Injection container using a Dictionary<Type, Func<object>> to register and resolve service instances dynamically.
Purpose: This exercise helps you practice using Type objects as dictionary keys and storing factory delegates as values, the core mechanism behind most real-world DI containers.
Given Input: container.Register<IGreeter>(() => new EnglishGreeter());, then resolve IGreeter and call its method.
Expected Output: Hello!
▼ Hint
- Store registrations in a
Dictionary<Type, Func<object>>, usingtypeof(TService)as the key. Register<TService>(Func<TService> factory)should wrap the typed factory in aFunc<object>before storing it.Resolve<TService>()should look uptypeof(TService), invoke the stored factory, and cast the result back toTService.- Throw a clear exception if someone tries to resolve a type that was never registered.
▼ Solution & Explanation
Explanation:
Dictionary<Type, Func<object>> _registrations: Maps each service type to a factory function capable of producing an instance of it.Register<TService>(Func<TService> factory): Wraps the strongly typed factory in a genericFunc<object>so it can be stored alongside factories for completely different types.(TService)factory(): Casts the resolved object back to the specific type the caller asked for.throw new InvalidOperationException(...): Gives a clear error message instead of a confusing null reference if a type was never registered.

Leave a Reply