C# (.NET 8 LTS) track

Lesson 12 · Part 3: Collections & LINQ

List, Dictionary, HashSet

Choose the right collection for ordered lists, key-value lookups, unique sets, queues and stacks.

Part 3: Collections & LINQ4 min readFree lesson

Arrays are of fixed size, but in real programs you will often need collections of varying size. C# has pre-created various data structures in the System.Collections.Generic namespace.

Learning Objectives

  • Use the add, delete, search, and sort operations of List<T>
  • Store and search key-value pairs with Dictionary<TKey, TValue>
  • Perform duplicate removal and set operations (union/intersection) with HashSet<T>
  • Know the difference between Queue<T> (FIFO) and Stack<T> (LIFO)

Key concepts

1) List<T> — variable-length lists

List<int> nums = [10, 20, 30];        //Collection type OK
nums.Add(40);
nums.Remove(20);     //remove first by value
nums.RemoveAt(0);    //remove by index
bool has = nums.Contains(30);
nums.Sort();
  • Accessible like an array with indexer nums[i].
  • Automatically expands the array internally.

2) Dictionary<TKey, TValue> — key-value mapping

Dictionary<string, int> ages = new()
{
    ["exponent"] = 20,
    ["minho"] = 25
};

ages["Seoyeon"] = 22;                 //Add or Renew
if (ages.TryGetValue("minho", out int age))
{
    Console.WriteLine(age);
}
  • The key must be unique, and an exception will be thrown if Add is used twice with the same key.
  • TryGetValue is a safe query pattern.

3) HashSet<T> — sets without duplicates

HashSet<string> a = ["apple", "ship", "persimmon"];
HashSet<string> b = ["ship", "persimmon", "grapes"];

a.UnionWith(b);      //union (a is changed)
a.IntersectWith(b);  //intersection
  • Add/Search is very fast, with an average of O(1).
  • Order is not guaranteed.

4) Queue<T> and Stack<T>

  • Queue<T> (First-in-first-out, FIFO): Input with Enqueue and take out with Dequeue. Parable of people waiting in line.
  • Stack<T> (last in, first out, LIFO): Input with Push and take out with Pop. Parable of a pile of books.
  • In both cases, you can only preview the value to be pulled out next with Peek.

See as example

Example 1 — ListBasics : Basics of List<T>

List<int> nums = [10, 20, 30];

nums.Add(40);
nums.Remove(20);

Console.WriteLine($"Including 30? {nums.Contains(30)}");
Console.WriteLine($"Count: {nums.Count}");

nums.Sort();
Console.WriteLine($"Sort: [{string.Join(", ", nums)}]");

Execution result

Including 30? True
Count: 3
Sort: [10, 30, 40]

Memo: List<T>.Count has the same role as Length in the array. Remove is erased by value, and RemoveAt is erased by 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}");
}

Execution result

apple: 3
banana: 2
cherry: 1

Memo: GetValueOrDefault(key, default) is a convenient method that returns a default value if the key does not exist. The var (k, v) decomposition syntax of foreach works naturally in .NET 8.

Example 3 — HashSet: Deduplication and set operations

string[] input = ["apple", "ship", "apple", "persimmon", "ship"];
HashSet<string> unique = new(input);
Console.WriteLine($"Remove duplicates: [{string.Join(", ", unique)}]");

HashSet<string> a = ["apple", "ship", "persimmon"];
HashSet<string> b = ["ship", "persimmon", "grapes"];

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)}]");

Execution result

Remove duplicates: [apple, pear, persimmon]
Intersection: [pear, persimmon]
Union: [apple, pear, persimmon, grape]

Memo: To preserve the original, copy it to new HashSet<T>(original) and then call the set operation. IntersectWith directly modifies the calling side.

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());
}

Execution result

=== Queue (FIFO) ===
first
second
third
=== Stack (LIFO) ===
third
second
first

Note: Queue appears in the order it came in, and Stack appears backwards. Back in web browser = Stack, printer job queue = Queue.

Common Mistakes

  1. Call Add twice on the same key of Dictionary → ArgumentException. Update to dict[key] = value.
  2. Call Dequeue/Pop from empty Queue/Stack → InvalidOperationException. Check Count > 0 in advance.
  3. While traversing List<T> to foreach, Add/Remove → collection change exception. for Reverse traversal or use new list.
  4. The illusion that HashSet<T> keeps order — order is not guaranteed. If you need sorting, use SortedSet<T>.
  5. Remove(value) of List<T> removes only the first match. To clear them all, click RemoveAll(predicate).

Cleanup

  • List<T> is a variable length array, the first choice in most "multiple" situations.
  • Dictionary<K,V> is key-value mapping, lookup average O(1).
  • HashSet<T> removes duplicates and sets operations.
  • Queue<T>(FIFO), Stack<T>(LIFO) are used in situations where processing order is important.

Try it yourself

cd src/ListBasics
dotnet run

cd ../DictBasics
dotnet run

cd ../HashSet
dotnet run

cd ../QueueStack
dotnet run

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/DictBasics/DictBasics.csprojText
<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.csC#
#nullable enable

string text = "apple banana apple cherry banana apple";
Dictionary<string, int> counts = new();

foreach (string word in text.Split(' '))
{
    //If not, 0, if present, existing value → + 1
    counts[word] = counts.GetValueOrDefault(word, 0) + 1;
}

foreach (var (word, count) in counts)
{
    Console.WriteLine($"{word}: {count}");
}
src/HashSet/HashSet.csprojText
<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.csC#
#nullable enable

string[] input = ["apple", "ship", "apple", "persimmon", "ship"];
HashSet<string> unique = new(input);
Console.WriteLine($"Remove duplicates: [{string.Join(", ", unique)}]");

HashSet<string> a = ["apple", "ship", "persimmon"];
HashSet<string> b = ["ship", "persimmon", "grapes"];

//Call set operations on copied copies 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.csprojText
<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.csC#
#nullable enable

List<int> nums = [10, 20, 30];

nums.Add(40);          //add to end
nums.Remove(20);       //remove first by value

Console.WriteLine($"Including 30? {nums.Contains(30)}");
Console.WriteLine($"Count: {nums.Count}");

nums.Sort();           //Sort ascending
Console.WriteLine($"Sort: [{string.Join(", ", nums)}]");
src/QueueStack/Program.csC#
#nullable enable

//FIFO — stand in 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 — 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.csprojText
<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>

Assignment

Problem 1 — Sort names + remove duplicates

  • Reference solution: shown below
  • Core concepts: HashSet<T>, List<T>, Sort

Requirements

  • Use the following array as input.
    string[] names = ["minho", "exponent", "minho", "Seoyeon", "exponent", "Yoonjae"];
    
  • Remove duplicates with HashSet<string>.
  • Move the results to List<string> and print them in alphabetical order.

Expected output

Sort after removing duplicates:
minho
Seoyeon
Yoonjae
exponent

Hint

  • Can be converted to new List<string>(hashSet).
  • For strings, call Sort() to sort them alphabetically.

Problem 2 — Word Counter (Top 3)

  • Reference solution: shown below
  • Key concepts: Dictionary<string, int>, sorting

Requirements

  • Count the number of occurrences of each word in the sentence "the quick brown fox jumps over the lazy dog the fox is quick".
  • Sort in descending order of frequency and output only top 3.

Expected output

the: 3
quick: 2
fox: 2

Hint

  • Separate words with Split(' ').
  • Top 3 with dict.OrderByDescending(kv => kv.Value).Take(3) (LINQ preview) — or convert to List<KeyValuePair<string,int>> and use Sort lambda.

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

Homework01/Homework01.csprojText
<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>
Homework01/Program.csC#
#nullable enable

string[] names = ["minho", "exponent", "minho", "Seoyeon", "exponent", "Yoonjae"];

HashSet<string> unique = new(names);
List<string> sorted = new(unique);
sorted.Sort();

Console.WriteLine("Sort after removing duplicates:");
foreach (string name in sorted)
{
    Console.WriteLine(name);
}
Homework02/Homework02.csprojText
<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>
Homework02/Program.csC#
#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 by descending frequency, then top 3 (using LINQ — OrderBy is a stable sort)
var top3 = counts.OrderByDescending(kv => kv.Value).Take(3);

foreach (var kv in top3)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

Learn this lesson with a mentor

Work through the C# (.NET 8 LTS) track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now