$ sudo teach IT
Модуль 5 · Коллекции

Урок 5.4 — Множества (HashSet)

Коллекция уникальных элементов с операциями пересечения, объединения и разности.

HashSet<T> — это коллекция уникальных элементов без определённого порядка. Внутри, как и Dictionary, использует хэш-таблицу. Главное преимущество — мгновенная проверка Contains() (O(1)) и мощные теоретико-множественные операции: объединение, пересечение, разность. Если тебе нужно просто «хранить уникальные значения и быстро проверять наличие» — HashSet твой выбор.

🏗️

Создание HashSet<T>

Создаётся так же, как и другие коллекции. Можно создать пустое множество, с элементами, или из другой коллекции:

C# · Создание HashSet
using System.Collections.Generic;

// Пустое множество
HashSet<int> empty = new HashSet<int>();

// С начальными элементами (дубликаты игнорируются)
HashSet<int> numbers = new HashSet<int> { 1, 2, 3, 4, 5 };

// Из другой коллекции (например, из массива с дубликатами)
int[] withDuplicates = { 1, 2, 2, 3, 3, 3 };
HashSet<int> unique = new HashSet<int>(withDuplicates);
Console.WriteLine(unique.Count);  // 3 — дубликаты отброшены

// Target-typed new (C# 9+)
HashSet<string> fruits = new();
💡 Ключевая особенность: HashSet не гарантирует порядок элементов. Не полагайся на то, что элементы расположены в том же порядке, в котором ты их добавил. Если нужен порядок — используй SortedSet<T> или List<T>.
➕➖

Добавление и удаление: Add, Remove

Add() — добавить элемент

Добавляет элемент, если его ещё нет. Возвращает true — элемент добавлен, false — элемент уже был:

C# · Add
HashSet<string> fruits = new HashSet<string>();

bool added1 = fruits.Add("яблоко");     // true
bool added2 = fruits.Add("банан");      // true
bool added3 = fruits.Add("яблоко");     // false — уже есть!

Console.WriteLine(fruits.Count);        // 2

// Возвращаемый bool можно использовать в условиях
if (fruits.Add("вишня"))
{
    Console.WriteLine("Вишня добавлена");
}

Remove() — удалить элемент

Удаляет элемент. Возвращает true, если элемент был:

C# · Remove
bool removed = fruits.Remove("банан");   // true
bool removedAgain = fruits.Remove("банан"); // false

// RemoveWhere — удалить по условию
fruits.RemoveWhere(f => f.StartsWith("я"));
// Удалит все фрукты, начинающиеся на 'я'

Clear() — очистить

Удаляет все элементы:

C# · Clear
fruits.Clear();
Console.WriteLine(fruits.Count);  // 0
🔍

Проверка наличия: Contains()

Это главная суперсила HashSet. Contains() работает за O(1) — мгновенно, независимо от размера коллекции. Для сравнения: у List<T> Contains() — O(n) (линейный поиск):

C# · Contains
HashSet<int> numbers = new HashSet<int> { 1, 2, 3, 4, 5 };

bool hasThree = numbers.Contains(3);   // true — O(1)
bool hasNine = numbers.Contains(9);    // false

// Мгновенная проверка даже для миллионов элементов
// HashSet из 1 млн элементов: Contains ≈ 0.000001 сек
// List из 1 млн элементов: Contains ≈ 0.01-0.1 сек

// Contains — идеальная замена List.Contains для частых проверок
🔄

Теоретико-множественные операции

Вот где HashSet раскрывается полностью. Ты можешь выполнять операции над множествами из школьной математики, но в коде:

UnionWith() — объединение

Добавляет все элементы из другой коллекции (или множества). Результат — элементы, которые есть хотя бы в одном из множеств:

C# · UnionWith
HashSet<int> a = new() { 1, 2, 3, 4, 5 };
HashSet<int> b = new() { 4, 5, 6, 7, 8 };

a.UnionWith(b);  // a = { 1, 2, 3, 4, 5, 6, 7, 8 }
// В математике: A ∪ B

IntersectWith() — пересечение

Оставляет только те элементы, которые есть в обоих множествах:

C# · IntersectWith
HashSet<int> a = new() { 1, 2, 3, 4, 5 };
HashSet<int> b = new() { 4, 5, 6, 7, 8 };

a.IntersectWith(b);  // a = { 4, 5 }
// В математике: A ∩ B

ExceptWith() — разность

Удаляет из первого множества все элементы, которые есть во втором:

C# · ExceptWith
HashSet<int> a = new() { 1, 2, 3, 4, 5 };
HashSet<int> b = new() { 4, 5, 6, 7, 8 };

a.ExceptWith(b);  // a = { 1, 2, 3 }
// В математике: A \ B

SymmetricExceptWith() — симметрическая разность

Оставляет элементы, которые есть ровно в одном из множеств (но не в обоих):

C# · SymmetricExceptWith
HashSet<int> a = new() { 1, 2, 3, 4, 5 };
HashSet<int> b = new() { 4, 5, 6, 7, 8 };

a.SymmetricExceptWith(b);  // a = { 1, 2, 3, 6, 7, 8 }
// В математике: A △ B (или (A\B) ∪ (B\A))

💡 Визуализация для множеств a = {1,2,3,4,5} и b = {4,5,6,7,8}:
• UnionWith → {1,2,3,4,5,6,7,8} (всё из обоих)
• IntersectWith → {4,5} (только общее)
• ExceptWith → {1,2,3} (только из первого, без второго)
• SymmetricExceptWith → {1,2,3,6,7,8} (всё, кроме общего)

IsSubsetOf и IsSupersetOf

Проверяют, является ли одно множество подмножеством или надмножеством другого:

C# · Подмножества
HashSet<int> big = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
HashSet<int> small = new() { 2, 4, 6, 8 };

bool isSubset = small.IsSubsetOf(big);       // true — small ⊆ big
bool isSuperset = big.IsSupersetOf(small);     // true — big ⊇ small

// Строгое подмножество
bool properSubset = small.IsProperSubsetOf(big);    // true
bool properSuperset = big.IsProperSupersetOf(small); // true

// Другие проверки:
HashSet<int> a = new() { 1, 2, 3 };
HashSet<int> b = new() { 4, 5, 6 };
bool overlaps = a.Overlaps(b);  // false — нет общих элементов
bool equal = a.SetEquals(new() { 3, 2, 1 }); // true — порядок не важен
🧹

Практическое применение: дедупликация

Самое частое применение HashSet — удаление дубликатов из коллекции. Просто передай любую коллекцию в конструктор HashSet, а потом преобразуй обратно:

C# · Дедупликация
int[] numbersWithDuplicates = { 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5 };

// Убираем дубликаты через HashSet
int[] unique = new HashSet<int>(numbersWithDuplicates).ToArray();
Console.WriteLine(string.Join(", ", unique)); // 1, 2, 3, 4, 5

// С сохранением порядка (если нужен) — Distinct из LINQ
int[] orderedUnique = numbersWithDuplicates.Distinct().ToArray();

Быстрая проверка членства

Когда нужно много раз проверять, есть ли элемент в наборе — HashSet незаменим:

C# · Быстрая проверка
// Фильтр спам-слов
HashSet<string> spamWords = new(StringComparer.OrdinalIgnoreCase)
{
    "спам", "реклама", "акция", "скидка", "бесплатно"
};

string message = "КУПИ СО СКИДКОЙ БЕСПЛАТНО!";
bool isSpam = false;

// Разбиваем сообщение на слова и проверяем
foreach (string word in message.Split(' '))
{
    if (spamWords.Contains(word.Trim('!', '?')))
    {
        isSpam = true;
        break;
    }
}
Console.WriteLine(isSpam);  // True

// Сравните с List: spamWords.Contains() был бы O(n) для каждой проверки

Поиск пересечения — общие друзья

Классический пример социальной сети:

C# · Общие друзья
HashSet<string> annasFriends = new()
{
    "Борис", "Вика", "Глеб", "Дарья", "Егор"
};

HashSet<string> borissFriends = new()
{
    "Анна", "Вика", "Дарья", "Жанна", "Захар"
};

// Общие друзья — пересечение множеств
HashSet<string> mutual = new(annasFriends);
mutual.IntersectWith(borissFriends);
Console.WriteLine($"Общие друзья: {string.Join(", ", mutual)}"); // Вика, Дарья

// Друзья только Анны (которых нет у Бориса)
HashSet<string> onlyAnna = new(annasFriends);
onlyAnna.ExceptWith(borissFriends);
Console.WriteLine($"Только друзья Анны: {string.Join(", ", onlyAnna)}"); // Борис, Глеб, Егор

// Все друзья обоих (объединение)
HashSet<string> allFriends = new(annasFriends);
allFriends.UnionWith(borissFriends);
Console.WriteLine($"Всего друзей: {allFriends.Count}"); // 8
🧩

Большой пример — система тегов

C# · Система тегов для статей
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Статья с тегами
        HashSet<string> articleTags = new(StringComparer.OrdinalIgnoreCase)
        {
            "C#", "коллекции", "HashSet", "производительность", "LINQ"
        };
        
        // Стандартные теги платформы
        HashSet<string> platformTags = new(StringComparer.OrdinalIgnoreCase)
        {
            "C#", "Java", "Python", "LINQ", "SQL", "базы данных"
        };
        
        // Какие теги статьи уже есть на платформе?
        HashSet<string> matched = new(articleTags);
        matched.IntersectWith(platformTags);
        Console.WriteLine($"Совпавшие теги: {string.Join(", ", matched)}");
        
        // Какие теги статьи новые (нет на платформе)?
        HashSet<string> newTags = new(articleTags);
        newTags.ExceptWith(platformTags);
        Console.WriteLine($"Новые теги: {string.Join(", ", newTags)}");
        
        // Добавляем новый тег
        bool added = articleTags.Add("производительность");
        Console.WriteLine($"Тег 'производительность' добавлен: {added} (уже был)");
        
        added = articleTags.Add("Entity Framework");
        Console.WriteLine($"Тег 'Entity Framework' добавлен: {added}");
        
        // Проверка: относится ли статья к C#?
        if (articleTags.Contains("C#"))
            Console.WriteLine("Статья про C#!");
        
        Console.WriteLine($"Всего тегов у статьи: {articleTags.Count}");
        foreach (string tag in articleTags)
            Console.WriteLine($"  - {tag}");
    }
}
📊

Сравнение: HashSet vs List vs Dictionary

Характеристика
List<T>
HashSet<T>
Уникальность
Нет (могут быть дубликаты)
Да (только уникальные)
Порядок
Гарантирован (порядок вставки)
Не гарантирован
Доступ по индексу
Да (list[i])
Нет
Contains()
O(n) — медленно
O(1) — мгновенно
Множественные операции
Нет
Да (UnionWith, IntersectWith...)
Использование
Динамический список с порядком
Уникальные элементы, быстрые проверки
📌

Что важно запомнить

1️⃣
HashSet<T> — это множество уникальных элементов. Дубликаты не допускаются. Add() возвращает false, если элемент уже есть.
2️⃣
Contains() — O(1). Мгновенная проверка, незаменимо для больших наборов данных. В отличие от List (O(n)).
3️⃣
UnionWith — объединение (∪). IntersectWith — пересечение (∩). ExceptWith — разность (\). SymmetricExceptWith — симметрическая разность (△).
4️⃣
IsSubsetOf / IsSupersetOf — проверка подмножества и надмножества. SetEquals — равенство множеств (независимо от порядка).
5️⃣
Порядок не гарантируется. Не рассчитывай на порядок вставки. Если нужен порядок — SortedSet<T> или сохрани порядок отдельно.
6️⃣
Дедупликация: new HashSet<T>(collection) убирает дубликаты. Быстрая проверка: идеально для стоп-слов, тегов, фильтров.

Тест: 5.4: Множества (HashSet<T>)

3 вопроса

HashSet

Premium