Menu

std::map ב-C++: מפתחות, ערכים, חיפוש והכנסה

std::map ב-C++ בהסבר ברור: מכולה ממוינת של זוגות מפתח-ערך עם חיפוש לוגריתמי. הכנסה, חיפוש, מעבר בלולאה, והימנעות מהמלכודת הקלאסית של operator[] שמכניס מפתחות בשקט.

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

חיפוש לפי מפתח

vector מצוין כשניגשים לפי מיקום: איבר 0, איבר 1 וכן הלאה. אבל לעיתים קרובות אין לכם מיקום: יש לכם שם, ואתם רוצים את מה שמשויך אליו: שם משתמש והניקוד שלו, מילה ומספר ההופעות שלה, קוד מדינה והבירה שלה. סריקה של vector כדי למצוא התאמה היא O(n), והיא נעשית איטית מהר.

std::map פותר את זה. הוא שומר זוגות מפתח-ערך, מחזיק אותם ממוינים לפי המפתח ומאפשר לחפש ערך לפי המפתח שלו בזמן O(log n). כללו את <map> כדי להשתמש בו:

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

הכנסת איברים

יש כמה דרכים להכניס רשומות, וההבדל ביניהן חשוב. הנפוצה ביותר היא operator[], שיוצר את המפתח אם הוא לא קיים ומחזיר הפניה שאפשר להשים אליה:

אם רוצים הכנסה שמסרבת לדרוס מפתח קיים, משתמשים ב-insert או ב-emplace. שניהם מחזירים pair שה-.second שלו הוא bool שאומר אם ההכנסה באמת קרתה:

השתמשו ב-[] כשאתם רוצים ש"הכתיבה האחרונה תנצח", וב-insert/emplace כשמפתח קיים צריך להישאר בלי שינוי.

המלכודת של operator[]: הוא מכניס בזמן קריאה

זה באג ה-map הנפוץ ביותר. operator[] אינו קריאה טהורה: אם המפתח חסר, הוא מכניס אותו בשקט עם ערך שנבנה כברירת מחדל (0 ל-int, "" ל-string וכן הלאה) ומחזיר הפניה אליו. כך שעצם הבדיקה של מפתח עם [] משנה את ה-map:

map<string, int> m;
if (m["maybe"] == 0) {   // באג: זה עתה יצר את "maybe" -> 0
    // ...
}
cout << m.size();        // 1, לא 0: הכנסתם מפתח בטעות

זה נושך אתכם גם עם const map, שבו operator[] אפילו לא יתקמפל כי ייתכן שיצטרך להכניס. כדי לקרוא בלי להכניס, השתמשו ב-find, ב-count/contains או ב-at (שזורק חריגה במקום להכניס כשהמפתח חסר):

כלל אצבע: אם אתם מתכוונים לקרוא, לעולם אל תשתמשו ב-[]. השתמשו ב-at כשהמפתח חייב להתקיים, וב-find/contains כשייתכן שלא.

מעבר בסדר ממוין

מכיוון ש-map מבוסס עץ, מעבר עליו מבקר במפתחות בסדר עולה ממוין, תמיד ובחינם. כל איבר הוא pair<const Key, Value>, ולכן השתמשו ב-structured binding כדי לפרק בצורה נקייה את המפתח ואת הערך:

שימו לב שהמפתח ב-binding הוא const: אפשר לשנות ערך דרך הלולאה עם auto&, אבל אי אפשר לשנות מפתח במקומו (זה היה שובר את סדר המיון). הפלט יוצא בסדר אלפביתי (blue, sea, sky) בלי שום שלב מיון, וזו בדיוק הסיבה שאנשים בוחרים ב-map על פני טבלת גיבוב כשמעבר מסודר חשוב.

ה-idiom של wordCount[w]++ הוא גם השימוש הקנוני בהתנהגות של הכנסה בזמן גישה: כאן אתם רוצים שמפתח חסר יתחיל ב-0, ולכן [] הוא הכלי הנכון.

הסרת איברים וגודל

מוחקים לפי מפתח עם erase, שמחזיר כמה איברים הוסרו (0 או 1 ב-map). אפשר גם למחוק דרך איטרטור מ-find. בודקים את מצב המכולה עם size() ו-empty():

מלכודת אחת כשמוחקים בתוך לולאה: m.erase(it) מבטל את האיטרטור it, ולכן אי אפשר לעשות אחר כך ++it. הדפוס הבטוח מאז C++11 הוא it = m.erase(it), שמחזיר איטרטור לאיבר הבא. להסרה מותנית חד-פעמית לאורך כל ה-map, std::erase_if(m, predicate) (C++20) נקי יותר.

הבא בתור: unordered_map

std::map נותן לכם מפתחות ממוינים ופעולות O(log n) צפויות, אבל משלמים על הסדר הזה בכל חיפוש. כשלא אכפת לכם מסדר המפתחות ורק רוצים את החיפושים המהירים ביותר, unordered_map מחליף את העץ הממוין בטבלת גיבוב: גישה של O(1) בממוצע. בהמשך נראה איך הוא עובד, מתי הממוצע בזמן קבוע שלו מנצח את הלוגריתם של map, ואת מלכודות הגיבוב (טיפוסי מפתח מותאמים, התנגשויות במקרה הגרוע) שמגיעות איתו.

שאלות נפוצות

מהו std::map ב-C++?

std::map היא מכולה אסוציאטיבית ששומרת זוגות מפתח-ערך ממוינים לפי המפתח. חיפוש, הכנסה ומחיקה הם כולם O(log n), כי היא ממומשת כעץ חיפוש בינארי מאוזן. כל מפתח ייחודי: הכנסה של מפתח כפול לא משנה את הערך הקיים.

מה ההבדל בין operator[] ל-at() ב-map של C++?

map[key] מחזיר הפניה לערך, ואם המפתח חסר הוא מכניס אותו בשקט עם ערך שנבנה כברירת מחדל. גם map.at(key) מחזיר הפניה, אבל אם המפתח חסר הוא זורק std::out_of_range במקום להכניס. השתמשו ב-at() (או ב-find()) כשאתם מתכוונים רק לקרוא.

איך בודקים אם מפתח קיים ב-map של C++?

השתמשו ב-m.contains(key) (C++20), ב-m.count(key) שמחזיר 0 או 1, או ב-m.find(key) != m.end(). הימנעו מבדיקה עם m[key]: היא מכניסה את המפתח אם הוא חסר, וזה כמעט אף פעם לא מה שרוצים.

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

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

להתחיל