← C# 강의 목록으로
📚
컬렉션 · LINQ
컬렉션 · 선수: 배열

12. List · Dictionary · HashSet

System.Collections.Generic 의 핵심 컬렉션 세 가지를 한 단원에서 정리합니다. 가변 길이의 List<T>, 키-값 사전 Dictionary<TKey,TValue>, 중복 없는 집합 HashSet<T>.

C#.NET 8컬렉션ListDictionary
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 배열
결과물
System.Collections.Generic 의 핵심 컬렉션 세 가지를 한 단원에서 정리합니다. 가변 길이의 List<T>, 키-값 사전 Dictionary<TKey,TValue>, 중복 없는 집합 HashSet<T>.

이 강의에서 배우는 것

  • 1`List<T>` 의 추가·삭제·검색·정렬 연산을 사용한다
  • 2`Dictionary<TKey, TValue>` 로 키-값 쌍을 저장하고 조회한다
  • 3`HashSet<T>` 로 중복 제거와 집합 연산(합/교집합)을 수행한다
  • 4`Queue<T>` (FIFO) 와 `Stack<T>` (LIFO) 의 차이를 안다

소개

배열은 크기가 고정이지만 실제 프로그램에서는 **크기가 변하는 컬렉션**이 훨씬 자주 필요합니다. C#은 `System.Collections.Generic` 네임스페이스에 다양한 자료구조를 미리 만들어 두었습니다.

핵심 개념

1) `List<T>` — 가변 길이 배열

csharp
List<int> nums = [10, 20, 30];        // 컬렉션 식 OK
nums.Add(40);
nums.Remove(20);     // 값으로 첫 번째 제거
nums.RemoveAt(0);    // 인덱스로 제거
bool has = nums.Contains(30);
nums.Sort();
  • 인덱서 `nums[i]` 로 배열처럼 접근 가능.
  • 내부적으로 배열을 자동 확장해 줍니다.

2) `Dictionary<TKey, TValue>` — 키-값 매핑

csharp
Dictionary<string, int> ages = new()
{
    ["지수"] = 20,
    ["민호"] = 25
};

ages["서연"] = 22;                 // 추가 또는 갱신
if (ages.TryGetValue("민호", out int age))
{
    Console.WriteLine(age);
}
  • 키는 **유일**해야 하고, 동일 키로 두 번 `Add` 하면 예외가 납니다.
  • `TryGetValue` 가 안전한 조회 패턴입니다.

3) `HashSet<T>` — 중복 없는 집합

csharp
HashSet<string> a = ["사과", "배", "감"];
HashSet<string> b = ["배", "감", "포도"];

a.UnionWith(b);      // 합집합 (a 가 바뀜)
a.IntersectWith(b);  // 교집합
  • 추가/검색이 평균 **O(1)** 로 매우 빠릅니다.
  • 순서가 보장되지 않습니다.

4) `Queue<T>` 와 `Stack<T>`

  • **`Queue<T>` (선입선출, FIFO)**: `Enqueue` 로 넣고 `Dequeue` 로 꺼냄. 줄 서는 사람들 비유.
  • **`Stack<T>` (후입선출, LIFO)**: `Push` 로 넣고 `Pop` 으로 꺼냄. 책 더미 비유.
  • 둘 다 `Peek` 로 다음에 꺼낼 값만 미리 볼 수 있습니다.

핵심 예제

예제 1 — `ListBasics` : `List<T>` 의 기본

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

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

Console.WriteLine($"포함 30? {nums.Contains(30)}");
Console.WriteLine($"개수: {nums.Count}");

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

**실행 결과**

text
포함 30? True
개수: 3
정렬: [10, 30, 40]

**메모:** `List<T>.Count` 는 배열의 `Length` 와 같은 역할. `Remove` 는 값으로, `RemoveAt` 는 인덱스로 지웁니다.

예제 2 — `DictBasics` : 단어 카운터

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

**실행 결과**

text
apple: 3
banana: 2
cherry: 1

**메모:** `GetValueOrDefault(key, 기본값)` 은 키가 없으면 기본값을 돌려주는 편리한 메서드. `foreach` 의 `var (k, v)` 분해 구문은 .NET 8에서 자연스럽게 동작합니다.

예제 3 — `HashSet` : 중복 제거와 집합 연산

csharp
string[] input = ["사과", "배", "사과", "감", "배"];
HashSet<string> unique = new(input);
Console.WriteLine($"중복 제거: [{string.Join(", ", unique)}]");

HashSet<string> a = ["사과", "배", "감"];
HashSet<string> b = ["배", "감", "포도"];

HashSet<string> intersection = new(a);
intersection.IntersectWith(b);
Console.WriteLine($"교집합: [{string.Join(", ", intersection)}]");

HashSet<string> union = new(a);
union.UnionWith(b);
Console.WriteLine($"합집합: [{string.Join(", ", union)}]");

**실행 결과**

text
중복 제거: [사과, 배, 감]
교집합: [배, 감]
합집합: [사과, 배, 감, 포도]

**메모:** 원본을 보존하려면 `new HashSet<T>(원본)` 으로 복사한 뒤 집합 연산을 호출합니다. `IntersectWith` 는 호출한 쪽을 직접 수정해요.

예제 4 — `QueueStack` : FIFO 와 LIFO

csharp
Queue<string> queue = new();
queue.Enqueue("첫번째");
queue.Enqueue("두번째");
queue.Enqueue("세번째");

Console.WriteLine("=== Queue (FIFO) ===");
while (queue.Count > 0)
{
    Console.WriteLine(queue.Dequeue());
}

Stack<string> stack = new();
stack.Push("첫번째");
stack.Push("두번째");
stack.Push("세번째");

Console.WriteLine("=== Stack (LIFO) ===");
while (stack.Count > 0)
{
    Console.WriteLine(stack.Pop());
}

**실행 결과**

text
=== Queue (FIFO) ===
첫번째
두번째
세번째
=== Stack (LIFO) ===
세번째
두번째
첫번째

**메모:** `Queue` 는 들어온 순서대로, `Stack` 은 거꾸로 나옵니다. 웹 브라우저의 뒤로가기 = Stack, 프린터 작업 대기열 = Queue.

전체 예제 코드 (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, 있으면 기존 값 → + 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 = ["사과", "배", "사과", "감", "배"];
HashSet<string> unique = new(input);
Console.WriteLine($"중복 제거: [{string.Join(", ", unique)}]");

HashSet<string> a = ["사과", "배", "감"];
HashSet<string> b = ["배", "감", "포도"];

// 원본을 보존하기 위해 복사한 사본에 집합 연산을 호출한다
HashSet<string> intersection = new(a);
intersection.IntersectWith(b);
Console.WriteLine($"교집합: [{string.Join(", ", intersection)}]");

HashSet<string> union = new(a);
union.UnionWith(b);
Console.WriteLine($"합집합: [{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);          // 끝에 추가
nums.Remove(20);       // 값으로 첫 번째 제거

Console.WriteLine($"포함 30? {nums.Contains(30)}");
Console.WriteLine($"개수: {nums.Count}");

nums.Sort();           // 오름차순 정렬
Console.WriteLine($"정렬: [{string.Join(", ", nums)}]");

src/QueueStack/Program.cs

csharp
#nullable enable

// FIFO — 줄 서기
Queue<string> queue = new();
queue.Enqueue("첫번째");
queue.Enqueue("두번째");
queue.Enqueue("세번째");

Console.WriteLine("=== Queue (FIFO) ===");
while (queue.Count > 0)
{
    Console.WriteLine(queue.Dequeue());
}

// LIFO — 책 더미
Stack<string> stack = new();
stack.Push("첫번째");
stack.Push("두번째");
stack.Push("세번째");

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>

자주 하는 실수

  1. `Dictionary` 의 같은 키에 `Add` 를 두 번 호출 → `ArgumentException`. 갱신은 `dict[key] = value` 로.
  2. 빈 `Queue`/`Stack` 에서 `Dequeue`/`Pop` 호출 → `InvalidOperationException`. 미리 `Count > 0` 체크.
  3. `List<T>` 를 `foreach` 로 순회하면서 `Add`/`Remove` → 컬렉션 변경 예외. `for` 역순 순회 또는 새 리스트 사용.
  4. `HashSet<T>` 가 순서를 지킨다고 착각 — 순서 보장 안 됨. 정렬이 필요하면 `SortedSet<T>`.
  5. `List<T>` 의 `Remove(value)` 는 **첫 번째** 일치만 제거. 모두 지우려면 `RemoveAll(predicate)`.

정리

  • `List<T>` 는 가변 길이 배열, 대부분의 "여러 개" 상황에서 첫 선택지.
  • `Dictionary<K,V>` 는 키-값 매핑, 조회 평균 O(1).
  • `HashSet<T>` 는 중복 제거와 집합 연산.
  • `Queue<T>`(FIFO), `Stack<T>`(LIFO) 는 처리 순서가 중요한 상황에 사용.

과제

**과제 - 12. List·Dictionary·HashSet**

문제 1 — 이름 정렬 + 중복 제거

  • 프로젝트 폴더: `Homework01/`
  • 핵심 개념: `HashSet<T>`, `List<T>`, `Sort`

요구사항

  • 다음 배열을 입력으로 사용한다.

```csharp string[] names = ["민호", "지수", "민호", "서연", "지수", "윤재"]; ```

  • `HashSet<string>` 으로 중복을 제거한다.
  • 그 결과를 `List<string>` 으로 옮긴 뒤 알파벳(가나다) 순으로 정렬해 출력한다.

예상 출력

text
중복 제거 후 정렬:
민호
서연
윤재
지수

힌트

  • `new List<string>(hashSet)` 로 변환 가능.
  • 한국어는 `Sort()` 만 호출해도 가나다 순.

---

문제 2 — 단어 카운터 (Top 3)

  • 프로젝트 폴더: `Homework02/`
  • 핵심 개념: `Dictionary<string, int>`, 정렬

요구사항

  • 문장 `"the quick brown fox jumps over the lazy dog the fox is quick"` 의 각 단어 등장 횟수를 센다.
  • 빈도 내림차순으로 정렬해 **상위 3개** 만 출력한다.

예상 출력

text
the: 3
quick: 2
fox: 2

힌트

  • `Split(' ')` 로 단어 분리.
  • `dict.OrderByDescending(kv => kv.Value).Take(3)` 로 상위 3개 (LINQ 미리 맛보기) — 또는 `List<KeyValuePair<string,int>>` 로 변환 후 `Sort` 람다 사용.

정답 확인

직접 풀어 본 후 [`answer/`](./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 = ["민호", "지수", "민호", "서연", "지수", "윤재"];

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

Console.WriteLine("중복 제거 후 정렬:");
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;
}

// 빈도 내림차순으로 정렬 후 상위 3개 (LINQ 사용 — OrderBy 는 안정 정렬)
var top3 = counts.OrderByDescending(kv => kv.Value).Take(3);

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

직접 해 보기

bash
cd src/ListBasics
dotnet run

cd ../DictBasics
dotnet run

cd ../HashSet
dotnet run

cd ../QueueStack
dotnet run

다음 단원

[13_제네릭](../13_제네릭/) — 컬렉션이 어떻게 모든 타입을 받을 수 있는지, 그 뒤의 제네릭 문법을 배웁니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗