HashSet

HashSet är den unika samlingen - som en exklusiv klubb där samma person inte kan komma in två gånger! 🌟

🎯 Efter denna artikel kommer du att:

  • Förstå unikhet - bara ett av varje element, automatisk deduplicering ✨
  • Använda Set-operationer - Union, Intersection, Except för matematiska operationer 🧮
  • Optimera prestanda - O(1) Contains istället för O(n) som List 🚀
  • Veta när HashSet är rätt val - och när det INTE är det! 💡

🌟 **HashSet - Den unika samlingen**

Grundläggande koncept

HashSet = “unik samling” - tänk som en exklusiv klubb eller ett team utan dubbletter.

  • Bara unika element - dubbletter ignoreras automatiskt
  • Ingen ordning - element är inte sorterade
  • Supersnabb sökning - O(1) prestanda för Contains, Add, Remove
  • Set-operationer - Union, Intersection, Except för matematiska operationer
using System;
using System.Collections.Generic;

HashSet<string> uniqueColors = new HashSet<string>();

// Add - lägg till element (returnerar bool)
bool added1 = uniqueColors.Add("Röd");     // true - lades till
bool added2 = uniqueColors.Add("Blå");     // true - lades till
bool added3 = uniqueColors.Add("Röd");     // false - redan finns!

Console.WriteLine($"Antal färger: {uniqueColors.Count}"); // 2 (inte 3!)

// Automatisk deduplicering
string[] colors = {"Röd", "Blå", "Grön", "Röd", "Blå", "Gul"};
HashSet<string> uniqueFromArray = new HashSet<string>(colors);
Console.WriteLine($"Unika färger: {uniqueFromArray.Count}"); // 4 (Röd, Blå, Grön, Gul)

**HashSet Methods och Properties**

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

// Basic Properties
Console.WriteLine($"Count: {numbers.Count}");        // 5

// Add - lägg till element (returnerar bool)
bool wasAdded = numbers.Add(6);                      // true
bool alreadyExists = numbers.Add(3);                 // false

// Contains - kontrollera om element finns (O(1)!)
bool contains3 = numbers.Contains(3);                // true
bool contains10 = numbers.Contains(10);              // false

// Remove - ta bort element (returnerar bool)
bool wasRemoved = numbers.Remove(5);                 // true
bool notFound = numbers.Remove(99);                  // false

// Clear - ta bort alla element
// numbers.Clear();

// CopyTo - kopiera till array
int[] array = new int[numbers.Count];
numbers.CopyTo(array);

// SetEquals - kontrollera om samma element (ordning spelar ingen roll)
HashSet<int> other = new HashSet<int>{4, 3, 2, 1, 6}; // Olika ordning
bool areEqual = numbers.SetEquals(other);            // true - samma element

// IsSubsetOf / IsProperSubsetOf
HashSet<int> subset = new HashSet<int>{1, 2, 3};
bool isSubset = subset.IsSubsetOf(numbers);          // true
bool isProperSubset = subset.IsProperSubsetOf(numbers); // true

// IsSupersetOf / IsProperSupersetOf
bool isSuperset = numbers.IsSupersetOf(subset);     // true

// Overlaps - kontrollera om några gemensamma element
HashSet<int> other2 = new HashSet<int>{3, 7, 8};
bool hasCommon = numbers.Overlaps(other2);          // true (3 är gemensam)

Set-operationer - Matematiska operationer

HashSet<string> avengers = new HashSet<string>
{
    "Iron Man", "Thor", "Hulk", "Captain America", "Black Widow"
};

HashSet<string> xmen = new HashSet<string>
{
    "Wolverine", "Storm", "Cyclops", "Jean Grey", "Iron Man" // Iron Man är i båda!
};

HashSet<string> dcHeroes = new HashSet<string>
{
    "Superman", "Batman", "Wonder Woman", "Flash"
};

// UnionWith - lägg till alla element från annan collection (modifierar original)
HashSet<string> allHeroes = new HashSet<string>(avengers);
allHeroes.UnionWith(xmen);
allHeroes.UnionWith(dcHeroes);
Console.WriteLine($"Alla hjältar: {allHeroes.Count}"); // 12 - inga dubbletter

// IntersectWith - behåll bara gemensamma element (modifierar original)
HashSet<string> commonHeroes = new HashSet<string>(avengers);
commonHeroes.IntersectWith(xmen);
Console.WriteLine($"Gemensamma: {string.Join(", ", commonHeroes)}"); // "Iron Man"

// ExceptWith - ta bort element som finns i annan collection
HashSet<string> onlyAvengers = new HashSet<string>(avengers);
onlyAvengers.ExceptWith(xmen);
Console.WriteLine($"Bara Avengers: {onlyAvengers.Count}"); // 4 - utan Iron Man

// SymmetricExceptWith - behåll element som bara finns i en av grupperna
HashSet<string> uniqueToEach = new HashSet<string>(avengers);
uniqueToEach.SymmetricExceptWith(xmen);
// Resultat: alla utom Iron Man (som finns i båda)

Praktiska användningsområden

// 1. Ta bort dubbletter från List
List<string> duplicateNames = new List<string>
{
    "Anna", "Bert", "Anna", "Cilla", "Bert", "David", "Anna"
};

HashSet<string> uniqueNames = new HashSet<string>(duplicateNames);
List<string> cleanedList = new List<string>(uniqueNames);
Console.WriteLine($"Från {duplicateNames.Count} till {cleanedList.Count} unika namn");

// 2. Snabb medlemskapstest
HashSet<string> validUsernames = new HashSet<string>
{
    "admin", "user", "guest", "moderator", "editor"
};

bool IsValidUser(string username)
{
    return validUsernames.Contains(username); // O(1) - supersnabbt!
}

// Jämför med List (O(n) - långsamt för stora listor)
List<string> validUsernamesList = new List<string>(validUsernames);
bool SlowValidation(string username)
{
    return validUsernamesList.Contains(username); // O(n) - blir långsammare
}

// 3. Tag system för blogginlägg
HashSet<string> postTags = new HashSet<string>();
postTags.Add("C#");
postTags.Add("Programming");
postTags.Add("Tutorial");
postTags.Add("C#"); // Ignoreras - redan finns

// 4. Besökarspårning
HashSet<string> todaysVisitors = new HashSet<string>();

void TrackVisitor(string userId)
{
    if (todaysVisitors.Add(userId))
    {
        Console.WriteLine($"Välkommen {userId}!");
    }
    else
    {
        Console.WriteLine($"Välkommen tillbaka {userId}!");
    }
}

// 5. Algoritmer - besökta noder i graf/träd
HashSet<int> visitedNodes = new HashSet<int>();

void DepthFirstSearch(GraphNode node)
{
    if (visitedNodes.Contains(node.Id))
        return; // Redan besökt

    visitedNodes.Add(node.Id);
    Console.WriteLine($"Besöker nod: {node.Id}");

    foreach (var neighbor in node.Neighbors)
    {
        DepthFirstSearch(neighbor);
    }
}

🆚 HashSet vs andra Collections

**HashSet vs List**

// Performance test - 10,000 element
List<int> numbersList = new List<int>();
HashSet<int> numbersSet = new HashSet<int>();

// Fyll båda med samma data
for (int i = 0; i < 10000; i++)
{
    numbersList.Add(i);
    numbersSet.Add(i);
}

// Contains test
var stopwatch = System.Diagnostics.Stopwatch.StartNew();

// List.Contains - O(n) - blir långsammare med fler element
bool foundInList = numbersList.Contains(9999);    // ~0.1ms för 10k element

stopwatch.Restart();

// HashSet.Contains - O(1) - samma tid oavsett antal element
bool foundInSet = numbersSet.Contains(9999);       // ~0.001ms

Console.WriteLine($"List: {foundInList}, HashSet: {foundInSet}");
FeatureHashSetListArray
Dubbletter❌ Nej✅ Ja✅ Ja
Ordning❌ Ingen✅ Bevaras✅ Bevaras
Indexering❌ Nej✅ Ja✅ Ja
Contains⚡ O(1)🐌 O(n)🐌 O(n)
Add/Remove⚡ O(1)⚡ O(1)*❌ N/A
Memory📈 Mer📊 Medium📉 Minst

*List.Add är O(1) amortized, men Insert/Remove kan vara O(n)

När använda HashSet vs andra

// ✅ Använd HashSet när:
// - Du behöver bara unika element
HashSet<string> uniqueEmails = new HashSet<string>();

// - Snabb Contains är viktig
HashSet<int> allowedIds = new HashSet<int>{1, 5, 10, 15, 20};
if (allowedIds.Contains(userId)) { /* snabbt! */ }

// - Set-operationer behövs
HashSet<string> permissions1 = new HashSet<string>{"read", "write"};
HashSet<string> permissions2 = new HashSet<string>{"write", "delete"};
permissions1.IntersectWith(permissions2); // gemensamma: {"write"}

// ❌ Använd INTE HashSet när:
// - Du behöver ordning
// List<string> prioritizedTasks = new List<string>(); // ordning viktig

// - Du behöver indexering
// List<int> scores = new List<int>();
// int thirdScore = scores[2]; // kan inte göra med HashSet

// - Du tillåter dubbletter
// List<string> shoppingCart = new List<string>(); // 2 mjölk är OK

Performance och Best Practices

HashSet Capacity och Performance

// ✅ Sätt initial kapacitet om du vet storleken
HashSet<int> efficientSet = new HashSet<int>(10000); // Undviker rehashing

// ✅ Använd korrekt equality comparer för custom objects
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override bool Equals(object obj)
    {
        return obj is Person p && Name == p.Name && Age == p.Age;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age); // .NET Core 2.1+
        // return Name.GetHashCode() ^ Age.GetHashCode(); // Äldre .NET
    }
}

HashSet<Person> people = new HashSet<Person>();
people.Add(new Person { Name = "Anna", Age = 25 });
people.Add(new Person { Name = "Anna", Age = 25 }); // Samma - läggs inte till

// ✅ Använd StringComparer för case-insensitive strings
HashSet<string> caseInsensitive = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
caseInsensitive.Add("hello");
caseInsensitive.Add("HELLO"); // Ignoreras - samma som "hello"

Memory och Thread Safety

// HashSet är INTE thread-safe
HashSet<int> unsafeSet = new HashSet<int>();

// ✅ För thread-safety, använd locks
private readonly object lockObject = new object();

void ThreadSafeAdd(int value)
{
    lock (lockObject)
    {
        unsafeSet.Add(value);
    }
}

// ✅ Eller använd ConcurrentDictionary som Set
var concurrentSet = new System.Collections.Concurrent.ConcurrentDictionary<int, byte>();

bool ConcurrentAdd(int value)
{
    return concurrentSet.TryAdd(value, 0); // Value är irrelevant
}

bool ConcurrentContains(int value)
{
    return concurrentSet.ContainsKey(value);
}

🎯 Advanced HashSet Patterns

// 1. Caching med expiration
HashSet<string> cachedResults = new HashSet<string>();
Dictionary<string, DateTime> cacheExpiry = new Dictionary<string, DateTime>();

void AddToCache(string item, TimeSpan expiry)
{
    cachedResults.Add(item);
    cacheExpiry[item] = DateTime.Now.Add(expiry);
}

bool IsInValidCache(string item)
{
    if (!cachedResults.Contains(item))
        return false;

    if (DateTime.Now > cacheExpiry[item])
    {
        cachedResults.Remove(item);
        cacheExpiry.Remove(item);
        return false;
    }

    return true;
}

// 2. Bloom Filter simulation (falsk-positiv möjlig, aldrig falsk-negativ)
HashSet<int> bloomFilter = new HashSet<int>();

void BloomAdd(string item)
{
    bloomFilter.Add(item.GetHashCode());
}

bool MightContain(string item)
{
    return bloomFilter.Contains(item.GetHashCode()); // Kanske finns
}

// 3. Set partitioning
HashSet<int> allNumbers = new HashSet<int>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
HashSet<int> evens = new HashSet<int>();
HashSet<int> odds = new HashSet<int>();

foreach (int num in allNumbers)
{
    if (num % 2 == 0)
        evens.Add(num);
    else
        odds.Add(num);
}

// Verify partition
bool isCompletePartition = evens.Union(odds).SetEquals(allNumbers) &&
                          !evens.Overlaps(odds);

🎓 TL;DR

  • **HashSet**: Unika element, ingen ordning, O(1) Contains/Add/Remove
  • Användning: Deduplicering, snabb membership test, set-operationer
  • Set-operationer: Union, Intersection, Except, SymmetricExcept
  • Performance: Mycket snabbare Contains än List för stora datamängder
  • Begränsningar: Ingen ordning, ingen indexering, kräver bra GetHashCode()

Obligatorisk dad-joke

Varför är HashSet som en exklusiv nattklubb?

För att de bara släpper in unika personer, och när du väl är inne kan de hitta dig superschnabbt - men du vet aldrig vilken ordning du kommer ut i! 🌟🎭


Upp

Upp


Licens: Apache 2.0 | © 2023 Marcus Medina, Campus Mölndal. Alla rättigheter förbehållna.
Du får använda och modifiera detta verk enligt villkoren i Apache License, Version 2.0. Du får inte använda detta verk för kommersiella ändamål utan tillstånd från upphovsmannen.