מיון הוא עוד אלגוריתם
בעמוד הקודם ראיתם שהספרייה הסטנדרטית מגיעה עם אלגוריתמים מוכנים שעובדים על כל טווח דרך איטרטורים. מיון הוא זה שתשתמשו בו הכי הרבה, והוא מקבל עמוד משלו כי יש לו כמה קצוות חדים: סדרים מותאמים, יציבות, וכלל שאם שוברים אותו מקבלים התנהגות לא מוגדרת במקום תשובה שגויה.
סוס העבודה הוא std::sort מ-<algorithm>. נותנים לו את ההתחלה ואת הסוף של טווח, והוא מסדר מחדש את האיברים במקומם בסדר עולה:
לא נוצר עותק: ה-vector עצמו מסודר מחדש. מאחורי הקלעים std::sort הוא בדרך כלל introsort (מיון מהיר שנופל חזרה למיון ערימה), שנותן O(n log n) בממוצע. זה כמעט תמיד מהיר יותר ומועד לטעויות הרבה פחות מכתיבת מיון משלכם.
זה עובד גם על מערכי C רגילים: פשוט מתארים את הטווח עם מצביעים:
סדר מותאם עם פונקציית השוואה
כברירת מחדל std::sort מסדר איברים עם operator<. כדי למיין אחרת, מעבירים ארגומנט שלישי: פונקציית השוואה (comparator) שמקבלת שני איברים ומחזירה true אם הראשון צריך לבוא לפני השני.
lambda היא ההתאמה הטבעית. סדר יורד הוא פשוט a > b:
למקרה הנפוץ של סדר יורד פשוט על טיפוסים מובנים, הספרייה אפילו מספקת פונקציית השוואה מוכנה, greater<T>() מ-<functional>:
#include <functional>
sort(nums.begin(), nums.end(), greater<int>()); // זהה ל-a > b
פונקציית ההשוואה היא גם הדרך למיין לפי משהו אחר מהערך עצמו, למשל מיון מחרוזות לפי אורך במקום לפי סדר אלפביתי:
קבלו את הפרמטרים של פונקציית ההשוואה כ-const& לכל דבר שגדול מכמה בתים (כמו string): העתקה של כל איבר בכל השוואה היא בזבוז טהור.
מיון structs לפי שדה
בתוכניות אמיתיות בדרך כלל ממיינים אוספים של structs לפי אחד השדות שלהם. פונקציית ההשוואה פשוט ניגשת לשדה שמעניין אתכם. כאן אנחנו ממיינים אנשים לפי גיל, מהצעיר ביותר:
שימו לב שגם ל-Linus וגם ל-Dennis יש גיל 25. כאן הם יצאו בסדר היחסי המקורי שלהם, אבל std::sort לא מבטיח את זה. אם הסדר היחסי של איברים שווים חשוב, השתמשו ב-std::stable_sort, ששומר עליו (במחיר ביצועים קטן):
כדי להכריע בשוויון באופן מכוון, למשל למיין לפי גיל ואז לפי שם בסדר אלפביתי, משווים את המפתח המשני רק כשהמפתחות הראשיים שווים. std::tie הופך את זה לנקי:
מלכודת ה-strict weak ordering
זו הטעות המסוכנת ביותר במיון ב-C++, כי היא לא נותנת תשובה שגויה: היא נותנת התנהגות לא מוגדרת, שלעיתים קרובות פירושה קריסה או קריאה מחוץ לגבולות.
std::sort דורש שפונקציית ההשוואה תגדיר strict weak ordering. הכלל המעשי: comp(x, x) חייב להיות false לכל איבר x. במילים אחרות, איבר אף פעם לא בא "לפני" עצמו. זה בדיוק מה ש-< ו-> נותנים, ובדיוק מה ש-<= ו->= שוברים:
// באג: מחזיר true כש-a == b, ומפר את ה-strict weak ordering.
sort(v.begin(), v.end(), [](int a, int b) {
return a <= b; // התנהגות לא מוגדרת: עלול לקרוס עם קלטים מסוימים
});
עם <=, פונקציית ההשוואה טוענת ש-5 בא לפני 5 אחר, וזו סתירה. std::sort עלול אז להוליך מצביע אל מעבר לסוף הטווח. קלטים זעירים לפעמים נראים כאילו הם עובדים, וזה מה שהופך את הבאג הזה למפחיד: הוא יכול לעבור את הבדיקות שלכם ולקרוס בייצור. הפתרון הוא פשוט <:
מלכודת קלאסית שנייה: מיון מבטל כל דבר שמצביע לתוך הטווח. איטרטורים, מצביעים ואינדקסים ששמרתם לפני המיון כבר לא מתייחסים לאותו איבר לוגי אחריו, כי האיברים זזו. חשבו מחדש כל מיקום שאתם צריכים אחרי המיון, אף פעם לא לפניו.
מיון של חלק מטווח
לפעמים לא צריך שהכול יהיה ממוין: רוצים רק את הכמה הראשונים. מיון של כל ה-vector כדי לקרוא את שלושת הראשונים הוא בזבוז. std::partial_sort מסדר רק את האיברים שביקשתם ומשאיר את השאר בסדר לא מוגדר, וזה זול יותר:
ואם צריך רק את האיבר היחיד שהיה יושב במיקום מסוים, כמו החציון, std::nth_element עושה עוד פחות עבודה: הוא שם את האיבר הנכון באינדקס הזה, עם כל הקטנים ממנו לפניו וכל הגדולים ממנו אחריו, והכול ב-O(n) בממוצע.
השתמשו בהם כש"ממוין לגמרי" זה יותר ממה שהבעיה באמת צריכה: הם חוסכים זמן של ממש על נתונים גדולים.
הבא בתור: תבניות
שמתם לב שאותו std::sort טיפל ב-int, ב-string וב-struct Person שלכם, ושאת greater<int>() אפשר היה באותה קלות להחליף ב-greater<string>()? הכלליות הזו היא לא קסם: אלה תבניות (templates), המנגנון שמאפשר לפיסת קוד אחת לעבוד עם כל טיפוס שהקורא מכניס. בעמוד הבא נראה איך לכתוב פונקציות ומחלקות תבנית משלכם, כדי שהקוד שלכם יהיה בלתי תלוי בטיפוס בדיוק כמו האלגוריתמים שהשתמשתם בהם.
שאלות נפוצות
איך ממיינים vector ב-C++?
כללו את <algorithm> וקראו ל-sort(v.begin(), v.end()). זה ממיין את האיברים במקומם בסדר עולה בעזרת operator<. כדי למיין מערך, העבירו את arr ואת arr + n (או begin(arr) / end(arr)).
איך ממיינים בסדר יורד ב-C++?
העבירו פונקציית השוואה שמחזירה a > b: sort(v.begin(), v.end(), [](int a, int b){ return a > b; });. אפשר גם להשתמש במובנה sort(v.begin(), v.end(), greater<int>()); מ-<functional>.
למה פונקציית ההשוואה שלי ב-C++ גורמת ל-std::sort לקרוס?
פונקציית ההשוואה חייבת להיות strict weak ordering: היא צריכה להחזיר false כששני הארגומנטים שווים. שימוש ב-<= או ב->= (שמחזירים true לאיברים שווים) שובר את הכלל הזה והוא התנהגות לא מוגדרת: std::sort עלול לקרוא מחוץ לגבולות ולקרוס. השוו תמיד עם < או >.