Menu

HashSet ב-C#: פריטים ייחודיים, Contains ופעולות על קבוצות

HashSet<T> מחזיק פריטים ייחודיים ועונה על Contains בזמן קבוע. למדו איך Add מדווח על כפילויות, איך מסירים כפילויות מרשימה, איך מאחדים, חותכים ומחסירים קבוצות, ואיך גורמים לקבוצה להשוות אובייקטים שלכם לפי ערך.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

HashSet<T> הוא אוסף שבו כל פריט מופיע לכל היותר פעם אחת. אין לו אינדקס ואין לו סדר מובטח, ובתמורה הוא בודק שייכות בזמן קבוע בקירוב: מציאת תגית אחת מתוך מיליון לוקחת בערך אותו זמן כמו מציאתה מתוך עשר.

הוספת פריטים: Add מחזיר bool

פלט:

True
False
3
True
True
False
2

הוספה של כפילות היא לא שגיאה; Add פשוט מחזיר false והקבוצה לא משתנה. ערך ההחזרה הזה הוא הדבר השימושי ביותר במתודה. הוא משלב "ראיתי את זה כבר?" ו"תזכור את זה" בקריאה אחת:

פלט:

Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique

למה Contains מהיר

List<T>.Contains משווה את הערך לכל איבר בתורו, ולכן העלות שלו גדלה עם הרשימה. HashSet<T> מחשב את ה-hash code של הפריט, קופץ לדלי של הקוד הזה, ומשווה רק את הפריטים המעטים ששמורים שם. בבדיקת שייכות בתוך לולאה, זה הופך צעד של O(n) לצעד של O(1), ולולאה מקוננת על שתי רשימות למעבר יחיד:

// Slow on large inputs: Contains scans bannedList for every order.
var flagged = orders.Where(o => bannedList.Contains(o.CustomerId));

// Fast: build the set once, then each lookup is constant time.
var banned = new HashSet<int>(bannedList);
var flagged2 = orders.Where(o => banned.Contains(o.CustomerId));

בניית הקבוצה עולה מעבר אחד על הרשימה, ולכן היא משתלמת רק כשמחפשים דברים יותר מכמה פעמים.

הסרת כפילויות מרשימה

יש שלוש דרכים נפוצות, והן שונות במה שקורה לסדר:

פלט:

Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv

Distinct היא ברירת המחדל הנכונה כשרוצים לקבל בחזרה רשימה. הגרסה שעובדת במקום עובדת כי RemoveAll קורא לפרדיקט פעם אחת לכל איבר לפי הסדר: seen.Add מחזיר false לעותק השני ולעותקים שאחריו, ולכן בדיוק הם נמחקים.

פעולות על קבוצות: איחוד, חיתוך, הפרש

ל-HashSet<T> יש את הפעולות מתורת הקבוצות. המתודות ...With משנות את הקבוצה שעליה הן נקראות ולא מחזירות כלום.

פלט:

Union:     Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except:    Ana
Symmetric: Ana, Dev
True
True
False
True

הארגומנט יכול להיות כל IEnumerable<T>: מערך, רשימה או קבוצה אחרת. SetEquals מתעלם מסדר ומכפילויות בארגומנט. פונקציית העזר Show ממיינת לפני ההדפסה, כי על סדר המעבר של קבוצה אי אפשר לסמוך.

ל-LINQ יש מתודות מקבילות שמחזירות רצף חדש ולא נוגעות בקלטים: monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). השתמשו בהן כשלא רוצים לשנות קבוצה, או כשהקלטים הם רשימות.

השוואה מותאמת למחלקות שלכם

קבוצה מחליטה מה זה "אותו פריט" בעזרת GetHashCode ו-Equals. במחלקה שלא דורסת אותן, שתיהן מבוססות על הזהות של האובייקט, ולכן שני אובייקטים עם שדות שווים הם שני פריטים שונים:

פלט:

2
1
True

הכלל: אובייקטים שהם Equals חייבים להחזיר את אותו GetHashCode. אם דורסים רק את Equals, הקבוצה מחפשת בדלי הלא נכון ועדיין מדווחת על כפילויות. ב-.NET Core 2.1 ואילך, HashCode.Combine(X, Y) בונה hash code טוב בלי חישוב שנכתב ביד.

כשאי אפשר לשנות את המחלקה, או כשצריך הגדרה אחרת של "אותו דבר" עבור קבוצה אחת, העבירו IEqualityComparer<T> לבנאי. למחרוזות יש משווים מוכנים:

פלט:

True
False
2

ב-C# 9 ואילך, record מייצר בשבילכם Equals ו-GetHashCode מבוססי ערך, כך ש-record Point(int X, int Y); עובד בקבוצה בלי קוד נוסף.

לעולם אל תשנו שדה שמשפיע על GetHashCode בזמן שהאובייקט נמצא בקבוצה. האובייקט נשאר בדלי של ה-hash הישן שלו, ו-Contains ו-Remove מפסיקים למצוא אותו.

סדר ו-SortedSet

HashSet<T> עובר על הפריטים בסדר שכדאי להתייחס אליו כשרירותי. אם צריך את הפריטים ממוינים, מיינו בזמן ההדפסה (set.OrderBy(x => x)) או השתמשו ב-SortedSet<T>, ששומר על הפריטים ממוינים כל הזמן ומוסיף Min, Max ושאילתות טווח ב-O(log n) לכל פעולה:

var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min);                // 10

HashSet מול List מול Dictionary

צורךהשתמשו ב
פריטים ייחודיים, "האם זה שם?" מהירHashSet<T>
פריטים ייחודיים, תמיד ממויניםSortedSet<T>
סדר, כפילויות, גישה לפי אינדקסList<T>
ערך שנשמר תחת כל מפתחDictionary<TKey, TValue>

קבוצה היא מילון עם מפתחות ובלי ערכים. אם אתם מוצאים את עצמכם כותבים Dictionary<string, bool> רק כדי לעקוב אחרי שייכות, HashSet<string> אומר את אותו הדבר בצורה ברורה יותר.

טעויות נפוצות

  • ציפייה לסדר. לקבוצה אין סדר שאפשר לסמוך עליו; מיינו, או השתמשו ב-SortedSet<T>.
  • מחלקות מותאמות בלי Equals ו-GetHashCode. אובייקטים שנראים שווים הופכים לפריטים נפרדים.
  • דריסה של Equals לבד. תמיד דרסו יחד איתה גם את GetHashCode.
  • שינוי של פריט אחרי שהוספתם אותו. הקבוצה כבר לא יכולה למצוא אותו.
  • גישה לקבוצה לפי אינדקס. set[0] לא מתקמפל; אין אינדקס. המירו עם ToList() אם צריך מיקומים.

שאלות נפוצות

מה זה HashSet ב-C#?

HashSet<T> הוא אוסף של פריטים ייחודיים בלי סדר מוגדר. הוספה של פריט שכבר קיים לא עושה כלום, ו-Contains עונה בזמן קבוע בקירוב, לא משנה כמה גדולה הקבוצה, כי הפריטים נשמרים לפי hash code כמו המפתחות של Dictionary.

מה HashSet.Add מחזיר?

Add מחזיר true כשהפריט נוסף ו-false כשהוא כבר היה בקבוצה. זה הופך את if (!seen.Add(x)) לבדיקת כפילות בשורה אחת: היא מוסיפה פריטים חדשים ומספרת לכם על פריטים חוזרים באותה קריאה.

איך מסירים כפילויות מ-List ב-C#?

list.Distinct().ToList() מחזיר רשימה חדשה בלי כפילויות ושומר את המופע הראשון של כל פריט בסדר המקורי שלו. גם new HashSet<T>(list) מסיר כפילויות, אבל לקבוצה אין סדר מובטח. כדי להסיר כפילויות במקום, השתמשו ב-var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.

מתי כדאי להשתמש ב-HashSet במקום ב-List?

השתמשו ב-HashSet<T> כשרוב הזמן שואלים "האם הפריט הזה נמצא באוסף?" או כשהפריטים צריכים להיות ייחודיים. List<T>.Contains סורק כל איבר, ולכן הוא נהיה איטי יותר ככל שהרשימה גדלה, בעוד ש-HashSet<T>.Contains לא. השתמשו ב-List<T> כשסדר, כפילויות או גישה לפי אינדקס חשובים.

למה ה-HashSet שלי מכיל אובייקטים כפולים?

המחלקה שלכם לא דורסת את Equals ואת GetHashCode, ולכן הקבוצה משווה הפניות, ושני אובייקטים עם אותם ערכי שדות נחשבים שונים. דרסו את שתי המתודות (תמיד יחד), או העבירו IEqualityComparer<T> לבנאי של הקבוצה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל