← Back to C# series
📚
Collections & LINQ
Collections · Prerequisite: array

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>.

C#.NET 8collectionsListDictionary
Duration
~1-1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Array
OUTCOME
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

csharp
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

csharp
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

csharp
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

csharp
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**

text
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

csharp
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**

text
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

csharp
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**

text
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

csharp
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**

text
=== 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

xml
<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

csharp
#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

xml
<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

csharp
#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

xml
<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

csharp
#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

csharp
#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

xml
<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

  1. Calling `Add` twice on a `Dictionary` with the same key → `ArgumentException`. To update, use `dict[key] = value`.
  2. `Dequeue`/`Pop` on an empty `Queue`/`Stack` → `InvalidOperationException`. Check `Count > 0` first.
  3. Adding/removing on a `List<T>` while iterating via `foreach` → collection-modified exception. Iterate `for` in reverse or use a new list.
  4. Thinking `HashSet<T>` preserves order — it doesn't. Use `SortedSet<T>` for ordering.
  5. `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

text
After dedup and sort:
Jisoo
Minho
Seoyeon
Yoonjae

Hints

  • 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

text
the: 3
quick: 2
fox: 2

Hints

  • 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

xml
<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

csharp
#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

xml
<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

csharp
#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

bash
cd src/ListBasics
dotnet run

cd ../DictBasics
dotnet run

cd ../HashSet
dotnet run

cd ../QueueStack
dotnet run

Next Lecture

[13_Generics](../13_%EC%A0%9C%EB%84%A4%EB%A6%AD/) — Learn the generics syntax that lets collections accept any type.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗