C# offers a rich set of collection types beyond the basic array, and knowing which one to reach for, a List<T>, HashSet<T>, Stack<T>, Queue<T>, or Dictionary<TKey, TValue>, is a skill every C# developer needs.
This collection of 30 C# collection exercises gives you hands-on practice with each of these types, showing how their unique behaviors solve different kinds of problems.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand not just how each collection works, but when to choose it.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Lists: Dynamic resizing, adding, removing, and searching.
- Sets: Enforcing uniqueness with
HashSet<T>. - Stacks & Queues: LIFO and FIFO processing for real-world scenarios.
- Dictionaries: Key-based storage and lookup.
+ Table Of Contents (30 Exercises)
Table of contents
- Exercise 1: Finding the Top 3 Scores
- Exercise 2: Removing Duplicate Names
- Exercise 3: Shuffling a Playlist
- Exercise 4: Managing a Product Inventory
- Exercise 5: Merging Two Lists
- Exercise 6: Counting Word Frequency
- Exercise 7: Tracking User Sessions
- Exercise 8: Grouping Cities by Country
- Exercise 9: Flipping a Dictionary
- Exercise 10: Calculating a Shopping Cart Total
- Exercise 11: Simulating a Printer Queue
- Exercise 12: Limiting a Support Ticket Queue
- Exercise 13: Breadth-First Search with a Queue
- Exercise 14: Checking Balanced Parentheses
- Exercise 15: Building an Undo Stack
- Exercise 16: Converting Decimal to Binary with a Stack
- Exercise 17: Logging Unique IP Addresses
- Exercise 18: Finding Mutual Friends
- Exercise 19: Keeping Tags Unique
- Exercise 20: Finding a Missing Number
- Exercise 21: Finding Exclusive Interests
- Exercise 22: Building a Sorted Leaderboard
- Exercise 23: Processing Tasks by Priority
- Exercise 24: Keeping Contacts Alphabetized
- Exercise 25: Logging Safely from Multiple Threads
- Exercise 26: Exposing a Read-Only List
- Exercise 27: Filtering and Selecting with LINQ
- Exercise 28: Grouping and Averaging with LINQ
- Exercise 29: Paginating a List with LINQ
- Exercise 30: Sorting by Multiple Fields
Exercise 1: Finding the Top 3 Scores
Practice Problem: Create a List<int> of test scores. Write a program to find and display the top 3 highest scores without sorting the entire list manually.
Purpose: This exercise helps you practice using LINQ’s OrderByDescending() and Take() methods to extract a subset of ranked data without writing a manual sorting algorithm.
Given Input: scores = new List<int> { 88, 95, 70, 100, 65, 90, 78 }
Expected Output:
Finding the top 3 scores: Top 3 Scores: 100, 95, 90 Top score search completed successfully.
▼ Hint
- Use LINQ’s
OrderByDescending()to arrange the scores from highest to lowest. - Chain
.Take(3)onto the ordered sequence to grab only the top three values. - Convert the result to a list with
.ToList()so it can be used like a normal collection.
▼ Solution & Explanation
Explanation:
List<int> scores = new List<int> { ... }: Creates a list of integers holding the test scores.scores.OrderByDescending(score => score): Uses LINQ to arrange the scores from highest to lowest without writing a manual sorting algorithm..Take(3): Selects only the first three elements of the ordered sequence, which are the three highest scores.string.Join(", ", topScores): Combines the list of top scores into a single comma-separated string for display.
Exercise 2: Removing Duplicate Names
Practice Problem: Given a List<string> of names with duplicates, remove all duplicate entries without creating a second list.
Purpose: This exercise helps you practice modifying a list in place while iterating over it, using a HashSet as a helper to track which values have already been seen.
Given Input: names = new List<string> { "Alice", "Bob", "Alice", "Charlie", "Bob", "Dave" }
Expected Output:
Removing duplicate names in place: Result: Alice, Bob, Charlie, Dave Duplicate removal completed successfully.
▼ Hint
- Use a
HashSetto track which names have already been seen. - Loop through the list by index; if the current name is already in the set, remove it with
RemoveAt()at that index. - Only increment the index when you don’t remove an item, since removing shifts the remaining elements.
▼ Solution & Explanation
Explanation:
HashSet<string> seen: Keeps track of names that have already appeared, using a set for fast lookups.while (i < names.Count): Loops through the list, but the index only advances when no item is removed, since removing an item shifts all later elements down by one.names.RemoveAt(i): Deletes a duplicate directly from the original list, without ever creating a second list to hold the results.seen.Add(names[i]); i++;: Records the first occurrence of a name and moves on to the next index.
Exercise 3: Shuffling a Playlist
Practice Problem: Implement a custom shuffle algorithm (like the Fisher-Yates shuffle) to randomize the order of elements in a List<string> representing a music playlist.
Purpose: This exercise helps you practice implementing the Fisher-Yates shuffle, a classic algorithm that randomizes a list in place by walking backward and swapping each element with a randomly chosen earlier one.
Given Input: playlist = new List<string> { "Song A", "Song B", "Song C", "Song D", "Song E" }
Expected Output (this program uses randomness, so the exact shuffled order will differ every time you run it; a sample run looks like this):
Original Playlist: Song A, Song B, Song C, Song D, Song E Shuffled Playlist: Song C, Song A, Song E, Song B, Song D Playlist shuffling completed successfully.
▼ Hint
- Loop backward through the list, from the last index down to index 1.
- For each position, generate a random index between 0 and the current position (inclusive).
- Swap the element at the current position with the element at the random index.
▼ Solution & Explanation
Explanation:
static void Shuffle<T>(List<T> list): A generic method that can shuffle a list of any type, implementing the Fisher-Yates algorithm.for (int i = list.Count - 1; i > 0; i--): Walks backward through the list, treating each position as the “current” slot to fill with a random remaining element.random.Next(0, i + 1): Picks a random index from 0 up to and including the current positioni.- Swapping
list[i]andlist[j]: Exchanges the current element with the randomly chosen one, gradually randomizing the entire list in place with no extra list needed.
Exercise 4: Managing a Product Inventory
Practice Problem: Create a list of product objects. Write methods to add a product, update its quantity by ID, and remove products that are out of stock.
Purpose: This exercise helps you practice managing a list of custom objects with helper methods, using LINQ to find items by ID and RemoveAll() to filter out unwanted entries.
Given Input: Products with Id 1 (Keyboard, Qty 10), Id 2 (Mouse, Qty 5), Id 3 (Monitor, Qty 2)
Expected Output:
Managing a product inventory: Initial Inventory: Keyboard (Id=1, Qty=10), Mouse (Id=2, Qty=5), Monitor (Id=3, Qty=2) After updating Mouse quantity to 0: Keyboard (Id=1, Qty=10), Mouse (Id=2, Qty=0), Monitor (Id=3, Qty=2) After removing out-of-stock items: Keyboard (Id=1, Qty=10), Monitor (Id=3, Qty=2) Inventory management completed successfully.
▼ Hint
- Create a
Productclass with Id, Name, and Quantity fields. - Write a method to find a product by ID using
FirstOrDefault(), then update its Quantity. - Use
RemoveAll()with a condition to strip out every product that has zero quantity.
▼ Solution & Explanation
Explanation:
class Product: A simple class representing an item in the inventory, with an Id, Name, and Quantity.AddProduct(inventory, product): Adds a new product to the inventory list.inventory.FirstOrDefault(p => p.Id == id): Searches the list for a product matching the given ID, returningnullif none is found.inventory.RemoveAll(p => p.Quantity == 0): Removes every product whose quantity has dropped to zero in a single call, without a manual loop.
Exercise 5: Merging Two Lists
Practice Problem: Take two separate List<int> instances of equal length and merge them into a single list by alternating elements from each.
Purpose: This exercise helps you practice combining two collections by index, building a new result list that interleaves values from both sources.
Given Input: listA = new List<int> { 1, 3, 5, 7 }, listB = new List<int> { 2, 4, 6, 8 }
Expected Output:
Merging two lists by alternating elements: Merged List: 1, 2, 3, 4, 5, 6, 7, 8 Merging completed successfully.
▼ Hint
- Create an empty list to hold the merged result.
- Loop through the lists by index, since they’re the same length.
- On each iteration, add the element from the first list, then the element from the second list, to the result.
▼ Solution & Explanation
Explanation:
List<int> merged = new List<int>(): Creates an empty list to hold the interleaved result.for (int i = 0; i < listA.Count; i++): Loops once for each pair of elements, since both lists are the same length.merged.Add(listA[i]); merged.Add(listB[i]);: Adds one element from each list in turn, alternating their order in the merged result.
Exercise 6: Counting Word Frequency
Practice Problem: Read a paragraph of text and use a Dictionary<string, int> to count how many times each unique word appears.
Purpose: This exercise helps you practice using a dictionary as a counter, checking whether a key already exists before deciding to increment or initialize its value.
Given Input: text = "the quick brown fox jumps over the lazy dog the fox runs"
Expected Output:
Counting word frequency in the text: the = 3 quick = 1 brown = 1 fox = 2 jumps = 1 over = 1 lazy = 1 dog = 1 runs = 1 Word frequency count completed successfully.
▼ Hint
- Split the text into individual words using
Split(' '). - Use a
Dictionary<string, int>to store each word alongside its count. - For each word, increment its count if it’s already in the dictionary, or add it with a count of 1 if it’s new.
▼ Solution & Explanation
Explanation:
Dictionary<string, int> wordCounts: Maps each unique word to how many times it has appeared so far.wordCounts.ContainsKey(word): Checks whether this word has already been seen at least once.wordCounts[word]++: Increments the existing count for a word that has already appeared.wordCounts[word] = 1: Adds a brand new entry to the dictionary the first time a word is encountered.
Exercise 7: Tracking User Sessions
Practice Problem: Build a system where a Dictionary<Guid, User> tracks active users by a unique Session ID, allowing for instant lookups, additions, and logouts.
Purpose: This exercise helps you practice using a Guid as a dictionary key for fast, unique lookups, and using TryGetValue() to safely retrieve a value without risking an exception.
Given Input: A fixed session ID 11111111-1111-1111-1111-111111111111 logging in as user “jdoe”
Expected Output:
Managing active user sessions: Session Count After Login: 1 Found User: jdoe Session Count After Logout: 0 Session management completed successfully.
▼ Hint
- Use a
Dictionary<Guid, User>to map session IDs to logged-in users. - Add a new session with
dictionary[sessionId] = user, and look one up safely withTryGetValue(). - Remove a session with
Remove(sessionId)to simulate a logout.
▼ Solution & Explanation
Explanation:
Dictionary<Guid, User> activeSessions: Maps each unique session ID to the User currently logged in with that session.activeSessions[sessionId] = new User("jdoe");: Adds a new session to the dictionary, using the GUID as a fast, unique lookup key.activeSessions.TryGetValue(sessionId, out User loggedInUser): Safely looks up a user by session ID, returning true and setting theoutvariable only if the session exists.activeSessions.Remove(sessionId);: Removes the session from the dictionary, simulating a user logging out.
Exercise 8: Grouping Cities by Country
Practice Problem: Create a Dictionary<string, List<string>> to map countries (keys) to a list of their major cities (values). Write a method to add a city to an existing country safely.
Purpose: This exercise helps you practice a common dictionary pattern: checking if a key exists before creating its value collection, so you can safely add to a group whether or not it already exists.
Given Input: Adding “New York”, “Los Angeles”, and “Chicago” to “USA”, and “Tokyo” to “Japan”
Expected Output:
Grouping cities by country: USA: New York, Los Angeles, Chicago Japan: Tokyo City grouping completed successfully.
▼ Hint
- Use a
Dictionary<string, List<string>>to map each country to a list of cities. - Before adding a city, check if the country already has a list using
ContainsKey(); if not, create a new empty list for it first. - Add the city to the country’s list either way.
▼ Solution & Explanation
Explanation:
Dictionary<string, List<string>> countryCities: Maps each country name to a list of its major cities.if (!countryCities.ContainsKey(country)): Checks whether this is the first city being added for a given country.countryCities[country] = new List<string>();: Creates a new, empty list for the country the first time it’s encountered.countryCities[country].Add(city);: Adds the city to the country’s list, whether it was just created or already existed.
Exercise 9: Flipping a Dictionary
Practice Problem: Given a dictionary of employee IDs and their respective departments, write a function that flips it into a dictionary of departments mapping to a list of employee IDs.
Purpose: This exercise helps you practice inverting a one-to-one dictionary into a one-to-many dictionary, combining the grouping pattern from the previous exercise with iteration over an existing dictionary.
Given Input: { "E1": "Engineering", "E2": "Sales", "E3": "Engineering", "E4": "Marketing", "E5": "Sales" }
Expected Output:
Flipping employee-to-department mapping into department-to-employees: Engineering: E1, E3 Sales: E2, E5 Marketing: E4 Dictionary inversion completed successfully.
▼ Hint
- Loop through each key-value pair in the original dictionary.
- For each department found, check if it already has a list in the new dictionary; if not, create one.
- Add the current employee’s ID to that department’s list.
▼ Solution & Explanation
Explanation:
Dictionary<string, string> employeeDepartments: The original mapping from a single employee ID to a single department.foreach (KeyValuePair<string, string> entry in employeeDepartments): Iterates through every employee-department pair in the original dictionary.if (!departmentEmployees.ContainsKey(department)): Creates a new list for a department the first time it’s encountered while inverting.departmentEmployees[department].Add(employeeId);: Adds the current employee’s ID to their department’s growing list, building the reversed structure.
Exercise 10: Calculating a Shopping Cart Total
Practice Problem: Use a Dictionary<string, double> to store items and their prices. Simulate a shopping cart checkout that calculates the total price of an array of scanned items.
Purpose: This exercise helps you practice using a dictionary as a price lookup table while looping through a separate list of scanned items to accumulate a total.
Given Input: Prices { "Apple": 0.50, "Bread": 2.50, "Milk": 1.75 }, scanned items { "Apple", "Bread", "Apple", "Milk", "Bread" }
Expected Output:
Calculating total price for scanned items: Scanned Apple: $0.50 Scanned Bread: $2.50 Scanned Apple: $0.50 Scanned Milk: $1.75 Scanned Bread: $2.50 Total = $7.75 Checkout calculation completed successfully.
▼ Hint
- Use a
Dictionary<string, double>to store each item’s price. - Loop through the array of scanned items, looking up each one’s price in the dictionary.
- Add each found price to a running total, and print the total once every item has been processed.
▼ Solution & Explanation
Explanation:
Dictionary<string, double> prices: Maps each item name to its price for fast lookups during checkout.foreach (string item in scannedItems): Processes each scanned item one at a time, in the order they were scanned.prices.ContainsKey(item): Confirms the scanned item exists in the price list before trying to look up its price.total += prices[item];: Adds the price of the current item to the running checkout total.
Exercise 11: Simulating a Printer Queue
Practice Problem: Simulate a printer queue (Queue<string>) where documents are added to the queue and processed one by one in the order they arrived.
Purpose: This exercise helps you practice the first-in-first-out (FIFO) behavior of a Queue<T>, using Enqueue() to add items and Dequeue() to process them in arrival order.
Given Input: documents = "Report.docx", "Invoice.pdf", "Photo.png"
Expected Output:
Adding documents to the print queue: Queue Size: 3 Processing the print queue: Printing: Report.docx Printing: Invoice.pdf Printing: Photo.png Print spooling completed successfully.
▼ Hint
- Use a
Queue<string>to represent the printer’s job queue. - Add documents with
Enqueue(), which places them at the back of the line. - Process the queue in a loop, using
Dequeue()to remove and print the document at the front each time.
▼ Solution & Explanation
Explanation:
Queue<string> printQueue: A first-in-first-out collection, well suited for a printer’s job queue.printQueue.Enqueue(...): Adds a document to the back of the queue, in the order it was submitted.while (printQueue.Count > 0): Keeps processing documents as long as any remain in the queue.printQueue.Dequeue(): Removes and returns the document at the front of the queue, which is always the one that has been waiting the longest.
Exercise 12: Limiting a Support Ticket Queue
Practice Problem: Implement a system where incoming customer support requests are queued. Include a feature that limits the queue capacity and rejects new tickets if it’s full.
Purpose: This exercise helps you practice wrapping a Queue<T> inside a custom class to add extra business rules, like a maximum capacity, on top of the built-in collection.
Given Input: capacity = 3, incoming tickets "Ticket1", "Ticket2", "Ticket3", "Ticket4"
Expected Output:
Adding support tickets to a capacity-limited queue: Ticket1: Accepted Ticket2: Accepted Ticket3: Accepted Ticket4: Rejected (queue is full) Final Queue Size: 3 Ticket queueing completed successfully.
▼ Hint
- Wrap a
Queue<string>inside a class along with a capacity field. - Before enqueueing a new ticket, check if the queue’s current count has already reached the capacity.
- Return false to signal rejection if full, or enqueue the ticket and return true if there’s room.
▼ Solution & Explanation
Explanation:
private Queue<string> tickets; private int capacity;: Wraps aQueue<string>inside a class along with a maximum capacity limit.AddTicket(string ticket): Checks the current queue size against the capacity before allowing a new ticket to be added.if (tickets.Count >= capacity) return false;: Rejects the ticket without modifying the queue if it’s already full.tickets.Enqueue(ticket); return true;: Adds the ticket and confirms success if there’s still room.
Exercise 13: Breadth-First Search with a Queue
Practice Problem: Use a Queue<T> to implement a basic BFS traversal through a mock tree or graph structure.
Purpose: This exercise helps you practice using a queue to visit nodes level by level, which is the defining characteristic of a breadth-first search.
Given Input: A tree rooted at node 1, where 1 → {2, 3}, 2 → {4, 5}, 3 → {6}
Expected Output:
Performing a BFS traversal starting at node 1: Visit Order: 1, 2, 3, 4, 5, 6 BFS traversal completed successfully.
▼ Hint
- Represent the tree using a dictionary that maps each node to a list of its children.
- Use a
Queue<int>to hold nodes waiting to be visited, starting with the root node. - Repeatedly dequeue a node, record it as visited, and enqueue all of its children, until the queue is empty.
▼ Solution & Explanation
Explanation:
Dictionary<int, List<int>> tree: Represents a mock tree structure, mapping each node to a list of its children.Queue<int> queue: Holds nodes waiting to be visited, processed in the order they were discovered.queue.Enqueue(1): Starts the traversal by adding the root node to the queue.foreach (int child in tree[current]): Adds every child of the current node to the back of the queue, ensuring nodes are visited level by level rather than branch by branch.
Exercise 14: Checking Balanced Parentheses
Practice Problem: Use a Stack<char> to determine if a string containing parentheses, braces, and brackets is properly balanced and closed.
Purpose: This exercise helps you practice using a stack’s last-in-first-out (LIFO) behavior to verify that every closing symbol matches the most recently opened one.
Given Input: text = "([]{})"
Expected Output:
Checking if "([]{})" is balanced:
Result: The string is balanced.
Balance check completed successfully.
▼ Hint
- Use a
Stack<char>to track opening brackets as you scan the string. - When a closing bracket appears, pop the stack and check it matches the expected opening bracket type.
- If the stack is empty when a closing bracket appears, or brackets remain at the end, the string is not balanced.
▼ Solution & Explanation
Explanation:
Stack<char> stack: Keeps track of unmatched opening brackets in the order they appeared.stack.Push(ch): Pushes any opening bracket onto the stack.stack.Pop(): Removes the most recently opened bracket to compare against the current closing bracket.stack.Count != 0(after the loop): If any opening brackets are left unmatched at the end, the string is not balanced.
Exercise 15: Building an Undo Stack
Practice Problem: Create a simple text buffer where typing adds words to a Stack<string>. Implement an “Undo” command that removes the last typed action.
Purpose: This exercise helps you practice recognizing that a stack’s LIFO behavior naturally matches how “undo” works, the most recent action is always the first one removed.
Given Input: Typing “Hello”, “World”, “Foo”, then one Undo
Expected Output:
Typing words into the editor: Buffer: Hello World Foo Performing Undo: Removed: Foo Buffer: Hello World Undo operation completed successfully.
▼ Hint
- Use a
Stack<string>to store each word as it’s typed, since the most recent word naturally ends up on top. - Implement Undo by calling
Pop(), which removes the most recently added word. - To display the buffer in typing order, convert the stack to an array and reverse it, since the stack itself iterates top-to-bottom.
▼ Solution & Explanation
Explanation:
Stack<string> typedWords: Stores each typed word, with the most recently typed word always on top.typedWords.Push("Hello"), etc.: Adds each new word typed to the top of the stack, in the order they were typed.typedWords.Pop(): Removes and returns the most recently typed word, implementing the undo behavior naturally, since a stack’s LIFO order matches “undo the last action.”ToArray().Reverse(): Converts the stack to an array (top-first) and reverses it to display the words in the order they were originally typed.
Exercise 16: Converting Decimal to Binary with a Stack
Practice Problem: Write a program that accepts a base-10 integer and uses a Stack<int> to convert it into its binary (base-2) equivalent.
Purpose: This exercise helps you practice using a stack to naturally reverse a sequence, since the remainders from repeated division come out in the opposite order of the final binary digits.
Given Input: number = 26
Expected Output:
Converting 26 to binary using a stack: Binary Value = 11010 Decimal to binary conversion completed successfully.
▼ Hint
- Repeatedly divide the number by 2, pushing each remainder onto a
Stack<int>. - Once the number reaches 0, the stack holds the binary digits in reverse order (least significant bit on top).
- Pop the stack repeatedly to build the final binary string in the correct order.
▼ Solution & Explanation
Explanation:
Stack<int> remainders: Collects the remainders produced by repeatedly dividing the number by 2.remainders.Push(temp % 2): Pushes each remainder (0 or 1) onto the stack as it’s calculated, from least significant to most significant bit.- The second
whileloop: Pops the remainders off the stack one at a time, which naturally reverses their order back to most-significant-bit-first, since a stack is LIFO. StringBuilder binary: Efficiently builds the final binary string one digit at a time.
Exercise 17: Logging Unique IP Addresses
Practice Problem: Simulate a web server log where a HashSet<string> stores unique visitor IP addresses, ensuring no duplicates are recorded regardless of how many visits occur.
Purpose: This exercise helps you practice using a HashSet to automatically enforce uniqueness, without needing to manually check for duplicates before adding.
Given Input: visits = "192.168.1.1", "192.168.1.2", "192.168.1.1", "10.0.0.5", "192.168.1.2", "10.0.0.5"
Expected Output:
Logging visitor IP addresses: Total Visits: 6 Unique Visitors: 3 Unique IPs: 192.168.1.1, 192.168.1.2, 10.0.0.5 IP logging completed successfully.
▼ Hint
- Use a
HashSet<string>to store visitor IP addresses. - Loop through every recorded visit and call
Add()on the set for each IP;HashSetautomatically ignores duplicates. - Compare the total number of visits to the set’s
Countto see how many were repeat visitors.
▼ Solution & Explanation
Explanation:
HashSet<string> uniqueVisitors: A collection that automatically enforces uniqueness, silently ignoring any value that’s already present.uniqueVisitors.Add(ip): Attempts to add each visit’s IP address; duplicates are simply ignored without any extra checking needed.visits.LengthvsuniqueVisitors.Count: Comparing the two shows how many of the total visits came from repeat visitors.- Why this matters: Because a
HashSetnever stores duplicates, no matter how many times the same IP visits, it only ever appears once in the set.
Exercise 18: Finding Mutual Friends
Practice Problem: Given two HashSet<string> instances representing the friend lists of two different users, find all mutual friends using an intersection operation.
Purpose: This exercise helps you practice using IntersectWith() to find elements common to two sets in a single call, instead of writing a manual comparison loop.
Given Input: friendsA = "Alice", "Bob", "Charlie", "Dave", friendsB = "Bob", "Dave", "Eve", "Frank"
Expected Output:
Finding mutual friends between two users: Mutual Friends: Bob, Dave Mutual friend search completed successfully.
▼ Hint
- Store each user’s friend list in a
HashSet<string>. - Call
IntersectWith()on one set, passing the other set as an argument. - The set that called
IntersectWith()is modified in place to contain only the elements present in both sets.
▼ Solution & Explanation
Explanation:
HashSet<string> friendsA, friendsB: Two sets representing each user’s list of friends.friendsA.IntersectWith(friendsB): ModifiesfriendsAin place, keeping only the elements that also exist infriendsB, which is exactly the definition of a mutual friend.- Result: After this call,
friendsAno longer holds Alice or Charlie, since they don’t appear infriendsB. - Why this matters: This single method call replaces what would otherwise require a manual loop comparing every element of one set against the other.
Exercise 19: Keeping Tags Unique
Practice Problem: Create a system for a blog post where users can add tags. Use a HashSet<string> to ensure tags are unique and case-insensitive.
Purpose: This exercise helps you practice customizing a HashSet‘s equality logic with StringComparer.OrdinalIgnoreCase, so values that differ only in case are treated as duplicates.
Given Input: tags = "CSharp", "dotnet", "csharp", "DotNet", "Programming"
Expected Output:
Adding tags to the tag cloud: Added: CSharp Added: dotnet Skipped duplicate: csharp Skipped duplicate: DotNet Added: Programming Final Tag Count: 3 Tags: CSharp, dotnet, Programming Tag cloud generation completed successfully.
▼ Hint
- Create the HashSet with a case-insensitive comparer:
new HashSet<string>(StringComparer.OrdinalIgnoreCase). - Use the boolean return value of
Add()to detect whether a tag was newly added or was a case-insensitive duplicate. - The set automatically treats tags like “CSharp” and “csharp” as the same value.
▼ Solution & Explanation
Explanation:
new HashSet<string>(StringComparer.OrdinalIgnoreCase): Creates a set that treats strings differing only in case as equal, using a custom comparer instead of the default case-sensitive comparison.uniqueTags.Add(tag): Returns true if the tag was newly added, or false if an equivalent tag (ignoring case) already existed in the set.- Result: The set keeps the casing of whichever version was added first, and rejects every later variant that only differs by case.
- Why this matters: This approach avoids writing manual case-insensitive comparison logic anywhere else in the program.
Exercise 20: Finding a Missing Number
Practice Problem: Given an array containing numbers from 1 to N with one number missing, use a HashSet<int> to efficiently identify the missing number.
Purpose: This exercise helps you practice using a HashSet for fast, constant-time membership checks, instead of repeatedly scanning an array for each candidate value.
Given Input: n = 6, numbers = { 1, 2, 4, 5, 6 }
Expected Output:
Finding the missing number between 1 and 6: Missing Number = 3 Missing element detection completed successfully.
▼ Hint
- Load all the numbers from the array into a
HashSet<int>for fast lookups. - Loop through every number from 1 to N.
- Use the set’s
Contains()method to check which number in that range is missing from the original array.
▼ Solution & Explanation
Explanation:
HashSet<int> presentNumbers = new HashSet<int>(numbers): Loads all the numbers from the array into a set, giving constant-time lookups for checking whether a number exists.for (int i = 1; i <= n; i++): Checks every number from 1 to N in order.!presentNumbers.Contains(i): Uses the set’s fast lookup to check whether the current number appears in the array, rather than searching through the array itself each time.- Why this matters: Because a
HashSetlookup runs in constant time, this approach avoids the slower comparison of scanning the array repeatedly for each candidate number.
Exercise 21: Finding Exclusive Interests
Practice Problem: Given two sets of student interests, find the interests that are unique to each student using a symmetric difference operation.
Purpose: This exercise helps you practice using SymmetricExceptWith() to find elements that belong to exactly one of two sets, the opposite of an intersection.
Given Input: interestsA = "Reading", "Gaming", "Hiking", "Cooking", interestsB = "Gaming", "Painting", "Hiking", "Music"
Expected Output:
Finding interests exclusive to each student: Exclusive Interests: Cooking, Music, Painting, Reading Exclusive interest search completed successfully.
▼ Hint
- Copy the first set into a new
HashSetso the original isn’t modified. - Call
SymmetricExceptWith(), passing the second set as an argument. - The result keeps only the elements that appear in exactly one of the two original sets, removing anything shared by both.
▼ Solution & Explanation
Explanation:
HashSet<string> exclusiveInterests = new HashSet<string>(interestsA): Creates a copy ofinterestsA, so the symmetric difference operation doesn’t modify the original set.exclusiveInterests.SymmetricExceptWith(interestsB): Modifies the set in place, keeping only elements that appear in exactly one of the two sets, removing anything present in both.- Result: Shared interests like “Gaming” and “Hiking” are removed entirely, since they belong to both students.
- Why this matters: The remaining elements represent exactly the interests that make each student’s list unique compared to the other’s.
Exercise 22: Building a Sorted Leaderboard
Practice Problem: Maintain a real-time leaderboard of player scores that automatically stays sorted from highest to lowest as new scores are added, using a SortedSet<T>.
Purpose: This exercise helps you practice using a SortedSet with a custom comparer, so the collection maintains a specific sort order automatically as elements are inserted.
Given Input: Scores added in this order: 85, 92, 77, 100, 68
Expected Output:
Adding scores to the leaderboard: Leaderboard: 100, 92, 85, 77, 68 Leaderboard tracking completed successfully.
▼ Hint
- Create a
SortedSet<int>with a custom comparer that reverses the default ordering, so it sorts highest to lowest. - Add scores using
Add(); the set automatically places each one in its correct sorted position. - Print the set directly, since it’s always kept in sorted order automatically.
▼ Solution & Explanation
Explanation:
SortedSet<int> leaderboard: A collection that automatically keeps its elements sorted at all times, with no duplicate values.Comparer<int>.Create((a, b) => b.CompareTo(a)): Supplies a custom comparer that reverses the natural ordering, so the set sorts from highest to lowest instead of the default lowest to highest.leaderboard.Add(...): Inserts each score directly into its correct sorted position; there is no separate sort step required.- Printing the set: Always reflects its current sorted state, since a
SortedSetmaintains order internally as elements are added.
Exercise 23: Processing Tasks by Priority
Practice Problem: Build a task manager where tasks are processed not by arrival time, but by an assigned priority level, using PriorityQueue<TElement, TPriority>.
Purpose: This exercise helps you practice using a priority queue, where each element is dequeued according to its priority value rather than the order it was added.
Given Input: Tasks added in this order: “Update docs” (priority 3), “Server outage” (priority 0), “Review PR” (priority 2), “Refactor code” (priority 1)
Expected Output:
Adding tasks in arrival order (not priority order): Processing tasks by priority (0 = Critical, 3 = Low): Processing: Server outage Processing: Refactor code Processing: Review PR Processing: Update docs Priority task processing completed successfully.
▼ Hint
- Use
PriorityQueue<string, int>, enqueueing each task along with a priority number (lower number = higher priority). - Add the tasks in any order; arrival order doesn’t affect processing order.
- Dequeue in a loop; the queue always returns the task with the current lowest priority value first.
▼ Solution & Explanation
Explanation:
PriorityQueue<string, int> taskQueue: A collection that dequeues elements based on their associated priority value rather than the order they were added.taskQueue.Enqueue("Update docs", 3): Adds a task along with its priority; a lower number here means higher urgency.- Result: Even though “Update docs” was added before “Server outage”, the queue processes “Server outage” first, since it was enqueued with the highest priority (0).
taskQueue.Dequeue(): Removes and returns the element with the lowest priority value currently in the queue.
Exercise 24: Keeping Contacts Alphabetized
Practice Problem: Build a phone book application where contacts are automatically kept in alphabetical order by name, using a SortedList<string, string>.
Purpose: This exercise helps you practice using a SortedList, which behaves like a dictionary but always enumerates its entries in sorted key order.
Given Input: Adding “Charlie”, “Alice”, then “Bob”, in that order
Expected Output:
Adding contacts to the directory: Alice: 555-0001 Bob: 555-0002 Charlie: 555-0003 Contact directory completed successfully.
▼ Hint
- Use a
SortedList<string, string>to map contact names to phone numbers. - Add contacts in any order using
Add(); the list automatically keeps itself sorted by key. - Loop through the sorted list; entries will always appear in alphabetical order by name.
▼ Solution & Explanation
Explanation:
SortedList<string, string> contacts: A dictionary-like collection that automatically keeps its entries sorted by key.contacts.Add("Charlie", "555-0003"): Adds a contact even though it’s out of alphabetical order in the code; the collection re-sorts internally.- Iterating over
contacts: Always visits entries in ascending key order (alphabetical by name), regardless of the order they were added. - Why this matters: Unlike a
Dictionary, whose enumeration order isn’t guaranteed, aSortedListalways enumerates in sorted key order.
Exercise 25: Logging Safely from Multiple Threads
Practice Problem: Simulate a multi-threaded application where multiple background threads write log messages concurrently to a shared queue without data corruption, using ConcurrentQueue<T>.
Purpose: This exercise helps you practice using a thread-safe collection so multiple tasks can write to it at the same time without extra locking code, and without risking lost or corrupted data.
Given Input: threadCount = 3, messagesPerThread = 5
Expected Output (the exact order log messages are written in is not deterministic across threads, so this exercise checks the total count instead of the exact message order):
Writing logs from 3 threads concurrently: Expected Log Count: 15 Actual Log Count: 15 Thread-safe logging completed successfully.
▼ Hint
- Use a
ConcurrentQueue<string>instead of a regularQueue<string>for safe concurrent access. - Start several tasks with
Task.Run(), each writing a batch of messages to the shared queue. - Wait for all tasks to complete with
Task.WaitAll(), then verify the queue’s count matches the expected total number of messages.
▼ Solution & Explanation
Explanation:
ConcurrentQueue<string> logQueue: A thread-safe version ofQueue<T>, designed so multiple threads can safely enqueue and dequeue at the same time without corrupting the collection.Task.Run(() => { ... }): Starts a background task that writes several log messages, simulating one of several concurrent threads.Task.WaitAll(tasks): Blocks until every background task has finished writing its messages, ensuring all logging is complete before checking the results.- Why this matters: Because
ConcurrentQueuehandles synchronization internally, the final count always matches the expected total, with no messages lost or duplicated, even though the exact interleaving of writes between threads is unpredictable.
Exercise 26: Exposing a Read-Only List
Practice Problem: Create an internal configuration list that can be modified by the system admin class but is exposed to the rest of the application as a read-only wrapper to prevent accidental modifications.
Purpose: This exercise helps you practice using ReadOnlyCollection<T> to expose a live, read-only view of a private list, protecting internal data from being modified by external code.
Given Input: Initial settings "MaxUsers=100", "Theme=Dark", then the admin class internally adds "Timeout=30"
Expected Output:
Reading configuration through the read-only wrapper: Settings: MaxUsers=100, Theme=Dark After the admin class adds a new setting internally: Settings: MaxUsers=100, Theme=Dark, Timeout=30 Read-only catalog demonstration completed successfully.
▼ Hint
- Keep the actual
List<T>private inside the class. - Expose a public property that returns
settings.AsReadOnly(), which wraps the list without copying it. - The system admin class can still modify the private list directly, but any other code only sees a read-only view that has no
Add()orRemove()methods.
▼ Solution & Explanation
Explanation:
private List<string> settings: The actual mutable list, kept private so only this class can modify it directly.public ReadOnlyCollection<string> Settings => settings.AsReadOnly();: Exposes a read-only view of the same underlying list to any code outside this class.config.AddSetting("Timeout=30");: Only theSystemConfigclass itself can add new settings, using its own internal method.- Why this matters: Because
Settingsreturns a live wrapper (not a copy), any changes made internally, like the new “Timeout=30” entry, are immediately visible through the read-only view too, while outside code still cannot call.Add()on it directly.
Exercise 27: Filtering and Selecting with LINQ
Practice Problem: Given a list of Employee objects, use LINQ to filter out employees making under a certain salary, then select only their email addresses into a new collection.
Purpose: This exercise helps you practice chaining Where() and Select() together to filter and transform data in a single readable pipeline.
Given Input: Employees Alice ($85,000), Bob ($42,000), Charlie ($55,000), Dave ($38,000)
Expected Output:
Filtering employees earning under $50,000 and selecting their emails: Emails: bob@company.com, dave@company.com Filtering and transformation completed successfully.
▼ Hint
- Use
Where()with a lambda expression to filter employees by their Salary. - Chain
Select()onto the filtered results to project each Employee down to just their Email property. - Call
ToList()at the end to materialize the result into aList<string>.
▼ Solution & Explanation
Explanation:
employees.Where(e => e.Salary < 50000): Filters the list down to only employees whose salary is below the threshold..Select(e => e.Email): Transforms each remaining Employee object into just its Email property, discarding the rest of the object’s data..ToList(): Converts the resulting LINQ query into a concreteList<string>that can be used like any other list.- Why chain them: Chaining
Where()andSelect()together performs both the filtering and the transformation in a single, readable pipeline.
Exercise 28: Grouping and Averaging with LINQ
Practice Problem: Given a list of products, use LINQ to group them by their Category property and calculate the average price per category.
Purpose: This exercise helps you practice using GroupBy() to organize data into buckets, then applying an aggregate function like Average() within each group.
Given Input: Laptop ($999, Electronics), Mouse ($25, Electronics), Desk ($150, Furniture), Chair ($90, Furniture), Monitor ($200, Electronics)
Expected Output:
Grouping products by category and averaging price: Electronics: $408.00 Furniture: $120.00 Grouping completed successfully.
▼ Hint
- Use
GroupBy()with a lambda that selects the Category property to group products together. - For each group, use
Average()with a lambda selecting Price to calculate that category’s average. - Project the results into a new anonymous object containing the category name and its average price.
▼ Solution & Explanation
Explanation:
products.GroupBy(p => p.Category): Groups all products into buckets sharing the same Category value.group.Key: Inside each group, this holds the category name that all the products in that group share.group.Average(p => p.Price): Calculates the average price of every product within a single group.new { Category = ..., AveragePrice = ... }: Creates an anonymous type to hold both computed values together for each category, without needing to define a separate named class.
Exercise 29: Paginating a List with LINQ
Practice Problem: Use the .Skip() and .Take() LINQ methods on a large collection to simulate a web page pagination system.
Purpose: This exercise helps you practice the standard Skip-then-Take pattern for extracting a single “page” of results from a larger, ordered collection.
Given Input: A list of 50 sequential items, pageSize = 10, pageNumber = 2
Expected Output:
Simulating page 2 with a page size of 10: Items on Page 2: 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 Pagination completed successfully.
▼ Hint
- Calculate how many items to skip using
(pageNumber - 1) * pageSize. - Chain
Skip()with that value onto the collection, followed byTake(pageSize). - The result contains exactly the items that belong on the requested page.
▼ Solution & Explanation
Explanation:
Enumerable.Range(1, 50).ToList(): Generates a list of 50 sequential integers to simulate a large dataset.items.Skip((pageNumber - 1) * pageSize): Skips over all the items belonging to previous pages..Take(pageSize): Takes only enough items to fill a single page after skipping.- Why this pattern: This Skip-then-Take combination is the standard way to implement pagination over any ordered collection.
Exercise 30: Sorting by Multiple Fields
Practice Problem: Given a list of students, use LINQ to sort them first by their Grade (descending), and then by their Last Name (alphabetically).
Purpose: This exercise helps you practice chaining OrderByDescending() with ThenBy() to perform a multi-level sort, where the second key only matters when the first key ties.
Given Input: Alice Smith (90), Bob Jones (95), Charlie Adams (90), Dave Brown (95), Eve Clark (85)
Expected Output:
Sorting students by Grade (descending), then Last Name (ascending): Dave Brown (Grade: 95) Bob Jones (Grade: 95) Charlie Adams (Grade: 90) Alice Smith (Grade: 90) Eve Clark (Grade: 85) Complex sorting completed successfully.
▼ Hint
- Use
OrderByDescending()with the Grade property as the primary sort. - Chain
ThenBy()with the LastName property to break ties among students who share the same grade. - The combination sorts primarily by grade, and only falls back to last name when grades are equal.
▼ Solution & Explanation
Explanation:
students.OrderByDescending(s => s.Grade): Performs the primary sort, arranging students from the highest grade to the lowest..ThenBy(s => s.LastName): Performs a secondary sort within each group of students who share the same grade, arranging them alphabetically by last name.- Why order matters: Because
ThenBy()only affects the order among elements that are already equal by the primary sort key, students with different grades are never reordered by their last names. .ToList(): Converts the sorted LINQ sequence into a concrete list for easy iteration and display.

Leave a Reply