Урок 5.4 — Множества (HashSet)
Коллекция уникальных элементов с операциями пересечения, объединения и разности.
HashSet<T> — это коллекция уникальных элементов без определённого порядка. Внутри, как и Dictionary, использует хэш-таблицу. Главное преимущество — мгновенная проверка Contains() (O(1)) и мощные теоретико-множественные операции: объединение, пересечение, разность. Если тебе нужно просто «хранить уникальные значения и быстро проверять наличие» — HashSet твой выбор.
Создание HashSet<T>
Создаётся так же, как и другие коллекции. Можно создать пустое множество, с элементами, или из другой коллекции:
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();
SortedSet<T> или List<T>.
Добавление и удаление: Add, Remove
Add() — добавить элемент
Добавляет элемент, если его ещё нет. Возвращает true — элемент добавлен, false — элемент уже был:
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, если элемент был:
bool removed = fruits.Remove("банан"); // true
bool removedAgain = fruits.Remove("банан"); // false
// RemoveWhere — удалить по условию
fruits.RemoveWhere(f => f.StartsWith("я"));
// Удалит все фрукты, начинающиеся на 'я'
Clear() — очистить
Удаляет все элементы:
fruits.Clear();
Console.WriteLine(fruits.Count); // 0
Проверка наличия: Contains()
Это главная суперсила HashSet. Contains() работает за O(1) — мгновенно, независимо от размера коллекции. Для сравнения: у List<T> Contains() — O(n) (линейный поиск):
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() — объединение
Добавляет все элементы из другой коллекции (или множества). Результат — элементы, которые есть хотя бы в одном из множеств:
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() — пересечение
Оставляет только те элементы, которые есть в обоих множествах:
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() — разность
Удаляет из первого множества все элементы, которые есть во втором:
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() — симметрическая разность
Оставляет элементы, которые есть ровно в одном из множеств (но не в обоих):
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
Проверяют, является ли одно множество подмножеством или надмножеством другого:
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, а потом преобразуй обратно:
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 незаменим:
// Фильтр спам-слов
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) для каждой проверки
Поиск пересечения — общие друзья
Классический пример социальной сети:
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
Большой пример — система тегов
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
Что важно запомнить
Add() возвращает false, если элемент уже есть.new HashSet<T>(collection) убирает дубликаты. Быстрая проверка: идеально для стоп-слов, тегов, фильтров.Тест: 5.4: Множества (HashSet<T>)
3 вопроса