12. List · Dictionary · HashSet
Cover three core collections from System.Collections.Generic in one lecture: variable-length List<T>, key-value Dictionary<TKey,TValue>, and the dedupe set HashSet<T>.
What you'll learn
- 1Use `List<T>` for add / remove / search / sort
- 2Store and look up key-value pairs with `Dictionary<TKey, TValue>`
- 3Remove duplicates and do set operations (union/intersection) with `HashSet<T>`
- 4Know the difference between `Queue<T>` (FIFO) and `Stack<T>` (LIFO)
Overview
Arrays have fixed sizes, but real programs much more often need **variable-size collections**. C# ships several data structures ready to use in `System.Collections.Generic`.
Core Concepts
1) `List<T>` — variable-length array
List<int> nums = [10, 20, 30]; // collection expression OK
nums.Add(40);
nums.Remove(20); // remove first match by value
nums.RemoveAt(0); // remove by index
bool has = nums.Contains(30);
nums.Sort();- Indexer `nums[i]` lets you access like an array.
- Internally auto-expands the backing array.
2) `Dictionary<TKey, TValue>` — key-value map
Dictionary<string, int> ages = new()
{
["Jisoo"] = 20,
["Minho"] = 25
};
ages["Seoyeon"] = 22; // add or update
if (ages.TryGetValue("Minho", out int age))
{
Console.WriteLine(age);
}- Keys must be **unique**; calling `Add` twice with the same key throws.
- `TryGetValue` is the safe lookup pattern.
3) `HashSet<T>` — no-duplicate set
HashSet<string> a = ["apple", "pear", "persimmon"];
HashSet<string> b = ["pear", "persimmon", "grape"];
a.UnionWith(b); // union (a is mutated)
a.IntersectWith(b); // intersection- Add/lookup is **O(1)** average — very fast.
- Order is not guaranteed.
4) `Queue<T>` and `Stack<T>`
- **`Queue<T>` (first-in, first-out)**: `Enqueue` to add, `Dequeue` to remove. Like a line of people.
- **`Stack<T>` (last-in, first-out)**: `Push` to add, `Pop` to remove. Like a pile of books.
- Both have `Peek` to see the next-out without removing.
Examples
Example 1 — `ListBasics`: `List<T>` basics
List<int> nums = [10, 20, 30];
nums.Add(40);
nums.Remove(20);
Console.WriteLine($"Contains 30? {nums.Contains(30)}");
Console.WriteLine($"Count: {nums.Count}");
nums.Sort();
Console.WriteLine($"Sorted: [{string.Join(", ", nums)}]");**Output**
Contains 30? True
Count: 3
Sorted: [10, 30, 40]**Note:** `List<T>.Count` plays the role of array `Length`. `Remove` uses a value; `RemoveAt` uses an index.
Example 2 — `DictBasics`: word counter
string text = "apple banana apple cherry banana apple";
Dictionary<string, int> counts = new();
foreach (string word in text.Split(' '))
{
counts[word] = counts.GetValueOrDefault(word, 0) + 1;
}
foreach (var (word, count) in counts)
{
Console.WriteLine($"{word}: {count}");
}**Output**
apple: 3
banana: 2
cherry: 1**Note:** `GetValueOrDefault(key, default)` returns the default if the key is missing — handy. `var (k, v)` deconstruction in `foreach` works naturally in .NET 8.
Example 3 — `HashSet`: dedupe + set operations
string[] input = ["apple", "pear", "apple", "persimmon", "pear"];
HashSet<string> unique = new(input);
Console.WriteLine($"Dedup: [{string.Join(", ", unique)}]");
HashSet<string> a = ["apple", "pear", "persimmon"];
HashSet<string> b = ["pear", "persimmon", "grape"];
HashSet<string> intersection = new(a);
intersection.IntersectWith(b);
Console.WriteLine($"Intersection: [{string.Join(", ", intersection)}]");
HashSet<string> union = new(a);
union.UnionWith(b);
Console.WriteLine($"Union: [{string.Join(", ", union)}]");**Output**
Dedup: [apple, pear, persimmon]
Intersection: [pear, persimmon]
Union: [apple, pear, persimmon, grape]**Note:** To preserve the original, do set operations on a copy made via `new HashSet<T>(source)`. `IntersectWith` mutates the receiver.
Example 4 — `QueueStack`: FIFO and LIFO
Queue<string> queue = new();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");
Console.WriteLine("=== Queue (FIFO) ===");
while (queue.Count > 0)
{
Console.WriteLine(queue.Dequeue());
}
Stack<string> stack = new();
stack.Push("first");
stack.Push("second");
stack.Push("third");
Console.WriteLine("=== Stack (LIFO) ===");
while (stack.Count > 0)
{
Console.WriteLine(stack.Pop());
}**Output**
=== Queue (FIFO) ===
first
second
third
=== Stack (LIFO) ===
third
second
first**Note:** `Queue` comes out in insertion order; `Stack` reverses it. Browser back-button = Stack; printer queue = Queue.
Full example code (src/)
src/DictBasics/DictBasics.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/DictBasics/Program.cs
#nullable enable
string text = "apple banana apple cherry banana apple";
Dictionary<string, int> counts = new();
foreach (string word in text.Split(' '))
{
// 0 when absent; existing value otherwise → + 1
counts[word] = counts.GetValueOrDefault(word, 0) + 1;
}
foreach (var (word, count) in counts)
{
Console.WriteLine($"{word}: {count}");
}
src/HashSet/HashSet.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/HashSet/Program.cs
#nullable enable
string[] input = ["apple", "pear", "apple", "persimmon", "pear"];
HashSet<string> unique = new(input);
Console.WriteLine($"Dedup: [{string.Join(", ", unique)}]");
HashSet<string> a = ["apple", "pear", "persimmon"];
HashSet<string> b = ["pear", "persimmon", "grape"];
// Operate on a copy to preserve the original
HashSet<string> intersection = new(a);
intersection.IntersectWith(b);
Console.WriteLine($"Intersection: [{string.Join(", ", intersection)}]");
HashSet<string> union = new(a);
union.UnionWith(b);
Console.WriteLine($"Union: [{string.Join(", ", union)}]");
src/ListBasics/ListBasics.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/ListBasics/Program.cs
#nullable enable
List<int> nums = [10, 20, 30];
nums.Add(40); // append
nums.Remove(20); // remove first match by value
Console.WriteLine($"Contains 30? {nums.Contains(30)}");
Console.WriteLine($"Count: {nums.Count}");
nums.Sort(); // ascending
Console.WriteLine($"Sorted: [{string.Join(", ", nums)}]");
src/QueueStack/Program.cs
#nullable enable
// FIFO — a line
Queue<string> queue = new();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");
Console.WriteLine("=== Queue (FIFO) ===");
while (queue.Count > 0)
{
Console.WriteLine(queue.Dequeue());
}
// LIFO — a stack of books
Stack<string> stack = new();
stack.Push("first");
stack.Push("second");
stack.Push("third");
Console.WriteLine("=== Stack (LIFO) ===");
while (stack.Count > 0)
{
Console.WriteLine(stack.Pop());
}
src/QueueStack/QueueStack.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- Calling `Add` twice on a `Dictionary` with the same key → `ArgumentException`. To update, use `dict[key] = value`.
- `Dequeue`/`Pop` on an empty `Queue`/`Stack` → `InvalidOperationException`. Check `Count > 0` first.
- Adding/removing on a `List<T>` while iterating via `foreach` → collection-modified exception. Iterate `for` in reverse or use a new list.
- Thinking `HashSet<T>` preserves order — it doesn't. Use `SortedSet<T>` for ordering.
- `List<T>.Remove(value)` removes only the **first** match. Use `RemoveAll(predicate)` to wipe all.
Summary
- `List<T>` is a variable-length array — the default choice for "multiple things."
- `Dictionary<K,V>` is a key-value map with O(1) average lookup.
- `HashSet<T>` is dedupe + set operations.
- `Queue<T>` (FIFO) and `Stack<T>` (LIFO) matter when processing order matters.
Practice
**Practice - 12. List·Dictionary·HashSet**
Problem 1 — Names: dedupe + sort
- Project folder: `Homework01/`
- Key concepts: `HashSet<T>`, `List<T>`, `Sort`
Requirements
- Use this input.
```csharp string[] names = ["Minho", "Jisoo", "Minho", "Seoyeon", "Jisoo", "Yoonjae"]; ```
- Dedupe with a `HashSet<string>`.
- Move the result into a `List<string>`, sort alphabetically, and print.
Expected output
After dedup and sort:
Jisoo
Minho
Seoyeon
YoonjaeHints
- Convert with `new List<string>(hashSet)`.
- `Sort()` alone sorts alphabetically.
---
Problem 2 — Word counter (Top 3)
- Project folder: `Homework02/`
- Key concepts: `Dictionary<string, int>`, sorting
Requirements
- Count word occurrences in `"the quick brown fox jumps over the lazy dog the fox is quick"`.
- Print the **top 3** by frequency, descending.
Expected output
the: 3
quick: 2
fox: 2Hints
- Split words with `Split(' ')`.
- Use `dict.OrderByDescending(kv => kv.Value).Take(3)` (LINQ preview) — or convert to `List<KeyValuePair<string,int>>` and `Sort` with a lambda.
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
#nullable enable
string[] names = ["Minho", "Jisoo", "Minho", "Seoyeon", "Jisoo", "Yoonjae"];
HashSet<string> unique = new(names);
List<string> sorted = new(unique);
sorted.Sort();
Console.WriteLine("After dedup and sort:");
foreach (string name in sorted)
{
Console.WriteLine(name);
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll12</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
#nullable enable
string sentence = "the quick brown fox jumps over the lazy dog the fox is quick";
Dictionary<string, int> counts = new();
foreach (string word in sentence.Split(' '))
{
counts[word] = counts.GetValueOrDefault(word, 0) + 1;
}
// Sort descending by frequency and take top 3 (LINQ — OrderBy is stable)
var top3 = counts.OrderByDescending(kv => kv.Value).Take(3);
foreach (var kv in top3)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Try It Yourself
cd src/ListBasics
dotnet run
cd ../DictBasics
dotnet run
cd ../HashSet
dotnet run
cd ../QueueStack
dotnet runNext Lecture
[13_Generics](../13_%EC%A0%9C%EB%84%A4%EB%A6%AD/) — Learn the generics syntax that lets collections accept any type.
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗