מבני נתונים — אוניברסיטת רייכמן

הרצאה 6 — Linear-time Sorting & Order Statistics

CountingSort, BucketSort, RadixSort, סטטיסטיקות סדר, QuickSelect ו-Median of Medians — בלי קיצורי דרך

תוכן עניינים

  1. 0. תמונה גדולה — איך עוקפים את \(\Omega(n \log n)\)?
  2. 1. Counting Sort — דוגמה מלאה צעד-אחר-צעד
  3. 2. למה Counting Sort יציב — וכיצד
  4. 3. Bucket Sort — חלוקה לפי טווח
  5. 4. Radix Sort + סדר לקסיקוגרפי
  6. 5. סיכום וטבלת השוואה לכל אלגוריתמי המיון
  7. 6. Order Statistics — דירוג, חציון
  8. 7. בעיית הסלקציה Select(A, k)
  9. 8. Quick Select — הוכחה מלאה ל-Average Θ(n)
  10. 9. Median of Medians — אלגוריתם Worst-case Θ(n)
  11. 10. בונוס — תובנות מעמיקות לכל הפרק
  12. 11. סיכום הרצאה

0. תמונה גדולה — איך עוקפים את \(\Omega(n \log n)\)?

בהרצאה הקודמת הוכחנו שכל מיון מבוסס השוואות חייב לפחות \(\Omega(n \log n)\) השוואות. השאלה הגדולה היום: איך לעקוף את החסם?

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

בהרצאה: שלושה אלגוריתמי מיון לינאריים שונים, ואז נושא בלתי-קשור לכאורה אבל קרוב אלגוריתמית — מציאת סטטיסטיקות סדר (חציון, איבר ה-k בגודלו).

המסקנה לקראת המבחן
  • אין סתירה בין החסם של הרצאה 5 לבין אלגוריתמי הרצאה 6. החסם מדבר על מודל מסוים, ומיוני הרצאה 6 חיים במודל אחר.
  • השאלה במבחן לא תהיה "האם אפשר ב-\(O(n)\)?" אלא "בהינתן מבנה כזה של קלט, איזה אלגוריתם הכי מתאים?".

1. Counting Sort — דוגמה מלאה צעד-אחר-צעד

הנחות
  • קלט: \(A[1..n]\) של שלמים בטווח \(\{1, 2, \ldots, k\}\).
  • פלט: \(B[1..n]\), פרמוטציה ממוינת של \(A\).
  • עזר: \(C[1..k]\).

אלגוריתם (ארבעה שלבים)

CountingSort(A, n, k):
  // שלב 1: אתחול C, Θ(k)
  for i ← 1 to k:
    C[i] ← 0

  // שלב 2: ספירה — C[i] = |{j : A[j] = i}|, Θ(n)
  for j ← 1 to n:
    C[A[j]] ← C[A[j]] + 1

  // שלב 3: סכומים מצטברים — C[i] = |{j : A[j] ≤ i}|, Θ(k)
  for i ← 2 to k:
    C[i] ← C[i] + C[i−1]

  // שלב 4: הצבה מהסוף להתחלה, Θ(n)
  for j ← n downto 1:
    B[C[A[j]]] ← A[j]
    C[A[j]] ← C[A[j]] − 1

דוגמה מפורטת: n=5, k=4, \(A = [4, 1, 3, 4, 3]\)

צעד 1: אתחול C
C = [0, 0, 0, 0] (אינדקסים 1..4)
צעד 2: ספירה
  • j=1: A[1]=4 ⟹ C[4]++ ⟹ C = [0, 0, 0, 1]
  • j=2: A[2]=1 ⟹ C[1]++ ⟹ C = [1, 0, 0, 1]
  • j=3: A[3]=3 ⟹ C[3]++ ⟹ C = [1, 0, 1, 1]
  • j=4: A[4]=4 ⟹ C[4]++ ⟹ C = [1, 0, 1, 2]
  • j=5: A[5]=3 ⟹ C[3]++ ⟹ C = [1, 0, 2, 2]
אינטרפרטציה: יש מופע אחד של 1, שני מופעים של 3, שני מופעים של 4.
צעד 3: סכומים מצטברים
  • i=2: C[2] = 0+1 = 1 ⟹ C = [1, 1, 2, 2]
  • i=3: C[3] = 2+1 = 3 ⟹ C = [1, 1, 3, 2]
  • i=4: C[4] = 2+3 = 5 ⟹ C = [1, 1, 3, 5]
אינטרפרטציה חדשה: יש איבר אחד ≤1, איבר אחד ≤2, שלושה איברים ≤3, חמישה איברים ≤4.
זה הסוד של האלגוריתם: אם A[j]=3, אז ערכו ב-B צריך להיות במיקום שמספרו = "כמה איברים ≤3 יש?" = 3 (זו תהיה הופעה האחרונה של 3).
צעד 4: הצבה מהסוף להתחלה
  • j=5: A[5]=3, C[3]=3 ⟹ B[3]=3, C[3]→2 ⟹ B = [_, _, 3, _, _]
  • j=4: A[4]=4, C[4]=5 ⟹ B[5]=4, C[4]→4 ⟹ B = [_, _, 3, _, 4]
  • j=3: A[3]=3, C[3]=2 ⟹ B[2]=3, C[3]→1 ⟹ B = [_, 3, 3, _, 4]
  • j=2: A[2]=1, C[1]=1 ⟹ B[1]=1, C[1]→0 ⟹ B = [1, 3, 3, _, 4]
  • j=1: A[1]=4, C[4]=4 ⟹ B[4]=4, C[4]→3 ⟹ B = [1, 3, 3, 4, 4]
מסקנה
סיבוכיות: \(\Theta(n + k)\). כאשר \(k = O(n)\) — לינארי. מקום: \(\Theta(n + k)\). לא in-place (יש מערך B), יציב (סעיף הבא).

2. למה Counting Sort יציב — וכיצד

זה נושא חשוב במיוחד כי Radix Sort דורש מיון יציב, וזו הסיבה שמשתמשים ב-Counting Sort בכל שלב של Radix.

תזכורת: מיון יציב
מיון נקרא יציב אם שני איברים שווי-מפתח שומרים על הסדר היחסי שלהם בין הקלט לפלט.
משפט
Counting Sort הוא יציב — ובלבד שלולאה (4) רצה מ-n ל-1 (downto), לא 1 ל-n.
למה? הוכחה אינטואיטיבית
נניח \(A[j_1] = A[j_2] = v\) ו-\(j_1 < j_2\) (כלומר \(A[j_1]\) הופיע קודם).

כשרצים מהסוף להתחלה, מטפלים קודם ב-\(A[j_2]\):

  • \(B[C[v]] = A[j_2]\) במיקום \(m = C[v]\).
  • \(C[v]\) קטן ל-\(m-1\).
  • אחר כך מטפלים ב-\(A[j_1]\): \(B[m-1] = A[j_1]\).
תוצאה: \(A[j_1]\) ב-\(B[m-1]\), \(A[j_2]\) ב-\(B[m]\) — \(j_1 < j_2\) → \(A[j_1]\) מופיע ראשון ב-B. ✓

ואם היינו רצים מ-1 ל-n? אז היינו שמים את \(A[j_1]\) ראשון, אחר כך \(C[v]\) קטן, ו-\(A[j_2]\) היה הולך למיקום נמוך יותר. הסדר היה מתהפך — ולא היה יציב. ❌

💡 למה חשוב שיציבות תהיה במיוחד כאן
כשמשתמשים ב-Counting Sort בתוך Radix Sort: כל ספרה ממוינת בנפרד. אם המיון על הספרה הראשונה לא יציב, כל המאמץ על הספרות הקודמות מתפזר. היציבות זה לא נחמדות — זה הכרחי ל-Radix.

3. Bucket Sort — חלוקה לפי טווח

הנחות
  • \(n\) איברים, מספרים ממשיים (לא בהכרח שלמים) בטווח \([0, n)\).
  • המפתחות מתפלגים אחיד — הנחה חיונית!

הרעיון

מחלקים את הטווח \([0, n)\) ל-\(n\) דליים בגודל 1 כל אחד. הדלי של המספר \(k\) הוא \(\lfloor k \rfloor\). כל דלי = רשימה מקושרת.

BucketSort(A):
  n ← length(A)
  B ← array of n empty linked lists
  for i ← 0 to n−1:
    insert A[i] into B[⌊A[i]⌋]      // חלוקה
  for j ← 0 to n−1:
    SortList B[j]                    // מיון כל דלי
  Concatenate B[0], B[1], …, B[n−1]  // שרשור

דוגמה: n=20, מפתחות ב-[0, 20)

קלט A
[1.7, 3.2, 3.56, 18.3, 0.87, 1.1, 4.8, 3.1, …, 18.2, 5.1, 0.38, 16.67]
פלט הדליים B (כל דלי = LL)
  • B[0] = {0.87, 0.38}
  • B[1] = {1.7, 1.1}
  • B[3] = {3.2, 3.56, 3.1}
  • B[4] = {4.8}
  • B[5] = {5.1}
  • … (יתר ריקים)
  • B[16] = {16.67}
  • B[18] = {18.3, 18.2}
אחרי מיון כל דלי
  • B[0] = {0.38, 0.87}
  • B[1] = {1.1, 1.7}
  • B[3] = {3.1, 3.2, 3.56}
  • ...
ועכשיו שרשור מ-B[0] עד B[19] → תוצאה ממוינת.

ניתוח Worst case ו-Average

סיבוכיות
  • Best/Average: \(\Theta(n)\) (בהנחת התפלגות אחידה).
  • Worst case: \(\Theta(n^2)\) — אם כל האיברים נופלים לדלי אחד, ה-BubbleSort בו מבצע \(O(n^2)\).
הוכחה ל-Average Θ(n)
תהי \(n_i\) מספר האיברים בדלי \(i\). זמן כולל: \[T = \Theta(n) + \sum_i O(n_i^2)\] צריך להראות שתוחלת הסכום היא \(O(n)\). בהתפלגות אחידה, \(\mathbb{E}[n_i^2] = O(1)\) (זה לא מובן מאליו — דורש הוכחה דרך משתנים אינדיקטוריים), ולכן \(\mathbb{E}\!\left[\sum n_i^2\right] = O(n)\). ∎
הכללה — General Bucket Sort
הטווח \([0, n)\) לא חיוני. ניתן:
  • לשנות-קנה-מידה: מפתחות ב-\([l, h)\) → דליים בגודל \((h - l)/n\).
  • להשתמש בהתפלגויות לא-אחידות ידועות (גובה, גיל, מהירות נסיעה) — צריך פשוט לחלק נכון.
💡 שיפור worst case
אם משתמשים ב-MergeSort במקום BubbleSort בתוך הדלי: worst case יורד מ-\(\Theta(n^2)\) ל-\(\Theta(n \log n)\) — אבל קבוע גדול יותר. בפועל ב-Average זה לא משנה.

4. Radix Sort + סדר לקסיקוגרפי

הרעיון: כל מפתח מורכב מ-\(d\) שדות (ספרות). ממיינים \(d\) פעמים, פעם לכל שדה, החל מהשדה הפחות משמעותי.

RadixSort(A, d):
  for i ← 1 to d:
    use a stable linear sort to sort A by field i
שני דברים קריטיים שאנשים שוכחים
  1. הסדר LSD → MSD (Least → Most Significant Digit). אם נעשה הפוך — לא יעבוד.
  2. המיון בכל שלב חייב להיות יציב. אחרת הסדר משדה קודם נשבר.

דוגמה: d=3, מספרים תלת-ספרתיים

קלט: 329, 457, 657, 839, 436, 720, 355
שלבאיזה שדה?אחרי המיון
1ספרה אחרונה (1s)720, 355, 436, 457, 657, 329, 839
2ספרה אמצעית (10s)720, 329, 436, 839, 355, 457, 657
3ספרה ראשונה (100s)329, 355, 436, 457, 657, 720, 839

למה זה עובד? — אינטואיציה

למה LSD-first עם stable sort עובד

אחרי שלב 1: המספרים ממוינים לפי ספרה אחרונה.

בשלב 2: נמיין לפי הספרה האמצעית. אבל מה קורה למספרים שיש להם אותה ספרה אמצעית? בזכות היציבות, הם נשארים בסדר שלהם משלב 1 — כלומר ממוינים לפי הספרה האחרונה.

אז אחרי שלב 2: מספרים ממוינים קודם לפי הספרה האמצעית, ובמקרה של שוויון — לפי האחרונה.

אחרי שלב 3 (אותו רעיון): ממוינים קודם לפי הספרה הראשונה, ובמקרה של שוויון — לפי הקודמות. זה בדיוק סדר לקסיקוגרפי = הסדר המספרי. ∎

סיבוכיות

Time complexity
יהי \(T_{ss}(n) = O(n)\) זמן המיון היציב על כל שדה (בדרך כלל Counting Sort עם k=10 או k=n). \[T_{\text{total}}(n) = \Theta(d \cdot T_{ss}(n)) = \Theta(d \cdot n)\]
  • אם \(d = O(1)\): \(\Theta(n)\) — לינארי!
  • אם המספרים בטווח \([1, n^d]\) ומיוצגים בבסיס \(n\): \(d\) ספרות → סיבוכיות \(\Theta(d n)\), עם \(k = n\) בכל ספרה.

סדר לקסיקוגרפי של מילים

הגדרה
עבור מילים \(x = (x_d \ldots x_1)\) ו-\(y = (y_d \ldots y_1)\): \[x < y \iff \exists t : x_t < y_t \text{ וגם } x_k = y_k \text{ לכל } k > t\] למילים באורכים שונים: ממלאים את המילה הקצרה ב-blank \(\_\) (קטן מכל תו רגיל).
דוגמה
\(\text{safe}\_ < \text{sail}\_ < \text{salt}\_ < \text{salty}\)

(שווה ב-s, שווה ב-a, אז משווה בתו השלישי: f < i < l ⟹ safe < sail, salt. בין salt ל-salty: שווה עד "salt", אז blank < y, אז salt < salty.)

💡 שאלת מבחן קלאסית — Radix Sort לבחירה חכמה של בסיס
"נתון מערך של \(n\) מספרים שלמים ב-\([0, n^3 - 1]\). מיין ב-\(O(n)\)."

אחת השאלות הכי שכיחות במבחנים. פתרון: לייצג כל מספר בבסיס \(n\) ⟹ יש \(\log_n(n^3) = 3 = O(1)\) ספרות. Radix Sort עם Counting Sort על כל ספרה: \(\Theta(3 \cdot (n + n)) = \Theta(n)\). ✓

5. סיכום וטבלת השוואה לכל אלגוריתמי המיון

AlgorithmWorstAverageStableIn-placeתנאי
Insertion sort\(\Theta(n^2)\)\(\Theta(n^2)\)כןכן
Heap sort\(\Theta(n \lg n)\)\(\Theta(n \lg n)\)לאכן
Merge sort\(\Theta(n \lg n)\)\(\Theta(n \lg n)\)כןלא
Quick sort\(\Theta(n^2)\)\(\Theta(n \lg n)\)לאכן
Counting sort\(\Theta(n+k)\)\(\Theta(n+k)\)כןלאשלמים ב-[1,k]
Bucket sort\(\Theta(n^2)\)\(\Theta(n)\)תלוילאאחיד ב-[0,n)
Radix sort\(\Theta(d(n+k))\)\(\Theta(d(n+k))\)כןלאd שדות, stable per field
כיצד לבחור?
  • שלמים ב-\(\{1..n\}\)? → Counting Sort. \(\Theta(n)\).
  • מספרים ממשיים אחידים? → Bucket Sort. \(\Theta(n)\) ממוצע.
  • שלמים ב-\([1, n^d]\)? → Radix Sort בבסיס n. \(\Theta(dn)\).
  • אחרת (כללי)? → QuickSort/MergeSort. \(\Theta(n \log n)\).

6. Order Statistics — דירוג וחציון

נושא חדש לגמרי — לא מיון, אבל קשור: לפעמים לא צריך לסדר הכל, רק למצוא איבר מסוים.

הגדרות יסודיות
תהי \(S\) קבוצה עם \(n\) איברים, וסידור ממוין \(X(1) \leq X(2) \leq \ldots \leq X(n)\):
  • ה-i-th order statistic של \(S\) הוא \(X(i)\).
  • ה-rank של \(x \in S\) הוא מיקומו ברשימה הממוינת.
  • Minimum: rank = 1. Maximum: rank = n.
  • Median: rank = \(\lceil(n+1)/2\rceil\) (אי-זוגי), או שתי חציונים (זוגי): \(n/2\) ו-\(n/2 + 1\).
דוגמה
ב-\(\{8, 2, 7, 5\}\) הסידור הממוין: \(2 < 5 < 7 < 8\).
  • rank של 5 = 2 (השני).
  • 1-st order stat = 2 (מינ').
  • חציון = שני איברים: 5 ו-7.

7. בעיית הסלקציה Select(A, k)

הגדרת הבעיה
בהינתן מערך \(A\) לא ממוין ו-\(k\) (\(1 \leq k \leq n\)), מצא את האיבר עם rank \(k\) ב-\(A\).

שיטה 1 — מיון מלא

שיטה 2 — k פעמים min

שיטה 3 — Min/Max ב-Θ(n) הפשוט ביותר

Minimum(A):
  min ← A[1]
  for i ← 2 to length(A):
    if min > A[i] then min ← A[i]
  return min
השאלה הגדולה
האם ניתן להגיע ל-\(\Theta(n)\) לכל k, ובמיוחד לחציון?

תשובה: כן! זה QuickSelect (ממוצע) ו-MM-Select (worst-case). הסעיפים הבאים.

8. Quick Select — הוכחה מלאה ל-Average Θ(n)

הרעיון: גרסה של QuickSort שעוצרת מוקדם. אחרי Partition יודעים איפה ה-pivot — אם זה k, מצאנו. אחרת, יודעים באיזה צד k נמצא וממשיכים רק שם.

QuickSelect(A, left, right, k):
  if (left == right): return A[left]
  q = Partition(A, left, right)
  if (q == k): return A[q]
  if (k < q): return QuickSelect(A, left, q−1, k)
  else:        return QuickSelect(A, q+1, right, k)

שימושים

ניתוח Best/Worst

ניתוח Average — הוכחה רשמית

משפט: T_avg(n) = O(n)

שלב 1. נסחים את ה-recurrence. תחת ההנחה הגרועה ביותר שאיבר rank(k) נופל בצד הגדול:

\[T_{\text{avg}}(n) \leq \frac{1}{n}\sum_{q=1}^{n} T_{\text{avg}}(\max(q-1, n-q)) + an\]

שלב 2. החלפת אינדקס \(q \to q' = q - 1\):

\[T_{\text{avg}}(n) \leq \frac{1}{n}\sum_{q=0}^{n-1} T_{\text{avg}}(\max(q, n-q-1)) + an\]

שלב 3. סימטריה — \(\max(q, n-q-1) = q\) כאשר \(q \geq n/2\), והפונקציה סימטרית סביב \(n/2\). לכן הסכום מ-\(n/2\) ל-\(n-1\) שווה למחצית הסכום הכולל:

\[T_{\text{avg}}(n) \leq \frac{2}{n}\sum_{q=n/2}^{n-1} T_{\text{avg}}(q) + an\]

שלב 4. הצבה של הנחת אינדוקציה \(T_{\text{avg}}(q) \leq cq\):

\[T_{\text{avg}}(n) \leq \frac{2}{n}\sum_{q=n/2}^{n-1} cq + an = \frac{2c}{n} \cdot \frac{(n/2 + n-1)(n/2)}{2} + an\]

(זהו טור אריתמטי עם \(n/2\) איברים, המינימום \(cn/2\), המקסימום \(c(n-1)\).)

שלב 5. פשטה אלגברית:

\[\leq c \cdot \frac{3n - 2}{4} + an = n\!\left(\frac{3c}{4} + a\right) - \frac{c}{2}\]

שלב 6. בחירת \(c\). נצליח להראות \(\leq cn\) אם:

\[\frac{3c}{4} + a \leq c \iff a \leq \frac{c}{4} \iff c \geq 4a\]

בחירה: \(c = \max\{b, 4a\}\) (כאשר \(b = T_{\text{avg}}(1)\), לבסיס האינדוקציה).

סיום:

\[T_{\text{avg}}(n) \leq cn - \frac{c}{2} \leq cn \quad \blacksquare\]
מסקנה
QuickSelect: Best/Avg \(\Theta(n)\), Worst \(\Theta(n^2)\). בפועל מהיר מאוד — קבועים קטנים, פשוט לקודד.

אבל ה-worst case \(\Theta(n^2)\) מטריד כשרוצים ערובה. למשל בקוד שמשרת לקוחות זרים — אסור שיהיה DoS.

9. Median of Medians — אלגוריתם Worst-case Θ(n)

בעיה מפורסמת ב-CS: מציאת חציון ב-worst-case לינארי. הפתרון נקרא BFPRT (Blum-Floyd-Pratt-Rivest-Tarjan, 1973). פלא של ניתוח אלגוריתמי.

הרעיון המרכזי

למה QuickSelect לא מספיק טוב?
כי הוא בוחר pivot שרירותי. אם בכל פעם נבחר pivot שמסומן קרוב לחציון — אז כל partition יחתוך כמעט חצי, וסיבוכיות = \(\Theta(n)\). הקושי: איך לבחור pivot טוב בלי לפתור את הבעיה המקורית?

Genius idea: נתפשר על pivot שטוב "מספיק" — מבטיח לפחות חיתוך 30%:70%. מבצעים ב-\(\Theta(n)\) רקורסיבי.

האלגוריתם

MM-Select(A, k):
  1. if n < 10: compute median naïvely; return it
  2. divide n elements into ⌊n/5⌋ groups Sᵢ of 5 each
  3. find median sᵢ of each group Sᵢ naïvely         // Θ(1) per group
  4. x ← MM-Select({s₁, …, s_{n/5}}, n/10)            // median of medians
  5. q ← Partition(A) with x as pivot
  6. if (q == k): return A[q]
  7. if (k < q): return MM-Select(A[1..q−1], k)
     else:        return MM-Select(A[q+1..n], k − q)

למה pivot x מבטיח 30%/70%?

משפט — ערובת ה-pivot
ה-pivot \(x\) מבטיח לפחות \(3n/10\) איברים \(\leq x\) ולפחות \(3n/10\) איברים \(\geq x\).
הוכחה ויזואלית
  1. יש \(n/5\) קבוצות של 5 איברים, וחציוני הקבוצות \(s_1, \ldots, s_{n/5}\).
  2. בחרנו \(x\) כחציון של החציונים. אז חצי מהחציונים (\(n/10\)) קטנים-שווים ל-\(x\), וחצי גדולים-שווים.
  3. קח קבוצה \(S_i\) שבה \(s_i \leq x\). בקבוצה הזו, \(s_i\) הוא החציון של 5 איברים — אז 3 איברים בה (\(s_i\) ושני איברים קטנים ממנו) הם \(\leq s_i \leq x\).
  4. סך הכל: לפחות \((n/10) \times 3 = 3n/10\) איברים \(\leq x\).
  5. סימטרית: לפחות \(3n/10\) איברים \(\geq x\). ∎

מסקנה: ה-partition אחרי שלב 5 מבטיח שכל צד מכיל לכל היותר \(n - 3n/10 = 7n/10\) איברים. הקריאה הרקורסיבית בשלב 7 פועלת על גודל \(\leq 7n/10\).

ניתוח הזמן

משפט — MM-Select בזמן Θ(n)
\[T(n) \leq \begin{cases} T(n/5) + T(7n/10) + \Theta(n) & n \geq 10 \\ b & n < 10 \end{cases}\] ⟹ \(T(n) = O(n)\).
הוכחה (עץ רקורסיה)

הסיבה ש-\(T(n) = O(n)\): \(\frac{1}{5} + \frac{7}{10} = \frac{2 + 7}{10} = \frac{9}{10} < 1\).

בעץ הרקורסיה, בכל רמה \(i\), סך העבודה \(\leq (9/10)^i \cdot n\) (העבודה הלא-רקורסיבית מסכמת לטור גאומטרי).

\[T(n) \leq n \cdot \sum_{i=0}^{\infty} \left(\frac{9}{10}\right)^i = n \cdot \frac{1}{1 - 9/10} = 10n = O(n). \quad \blacksquare\]
למה דווקא קבוצות של 5?
  • קבוצות של 3: \(T(n/3) + T(2n/3) + n = T(n) \cdot \log\)... ⟹ \(T(n) = \Theta(n \log n)\). לא מספיק!
  • קבוצות של 5: \(\frac{1}{5} + \frac{7}{10} = 0.9 < 1\) ⟹ לינארי.
  • קבוצות של 7: גם עובד, אבל הקבוע גדול יותר.
5 זה ה-sweet spot. זו בחירה לא טריוויאלית — המאמר המקורי הקדיש לזה ניתוח שלם.
💡 הקבוע ב-Θ(n) של MM-Select גדול בפועל
\(T(n) \leq 10n\) זה רק החסם העליון. הקבוע האמיתי כולל גם את העבודה של מציאת חציון של 5 איברים (לא חינם), והעבודה הרקורסיבית. בפועל QuickSelect מהיר פי כמה מ-MM-Select כי הקבועים שלו קטנים.

MM-Select חשוב בעיקר תיאורטית: הוכחה לקיומו של אלגוריתם סלקציה ב-worst-case לינארי. בייצור משתמשים ב-QuickSelect עם בחירת pivot חכמה (random/median-of-three).

10. בונוס — תובנות מעמיקות לכל הפרק

💡 1. למה ה-Counting Sort דורש שלב הסכומים המצטברים?
אחרי שלב 2, יודעים כמה מכל ערך יש — אבל לא איפה כל אחד צריך להיכנס ב-B. הסכומים המצטברים נותנים את המיקום הסופי האחרון של כל ערך. למשל C[3]=3 אומר "האחרון מבין השלשות יילך למיקום 3 ב-B".

בלי הסכומים — היינו צריכים סריקה נוספת לכל ערך, וזה כבר לא Θ(n).

💡 2. הקשר בין Counting Sort ו-Radix Sort
ב-Radix Sort, כל "שלב מיון לפי ספרה" משתמש ב-Counting Sort עם \(k\) קטן (לדוגמה 10 לעשרוני, 2 לבינארי, או \(n\) לבחירה אופטימלית). זה למה Radix Sort דורש Stable Sort — וזה למה Counting Sort הוא הבחירה הטבעית.
💡 3. למה Bucket Sort לא מספיק?
למרות שהוא Θ(n) ממוצע, ההנחה של "התפלגות אחידה" קשה לקיים. נתונים מהעולם האמיתי כמעט אף פעם לא אחידים. לכן בפועל RadixSort/MergeSort יותר אמינים.
💡 4. QuickSelect vs MM-Select בפועל
QuickSelectMM-Select
Worst\(\Theta(n^2)\)\(\Theta(n)\)
Average\(\Theta(n)\)\(\Theta(n)\)
קבועקטןגדול (≈ 10×)
שימושייצור (random pivot)גארנטיות תיאורטיות
ההמלצה: QuickSelect עם random pivot — מעניק worst case \(O(n^2)\) בהסתברות חסרת משמעות, וקבועים קטנים. זה מה ש-C++'s std::nth_element משתמש.
💡 5. הקשר העמוק בין Sorting ל-Selection
סלקציה היא לפחות קלה כמו מיון (אפשר תמיד למיין ואז לבחור). השאלה: האם היא קלה יותר?
  • במודל ההשוואות: כן! Sorting דורש \(\Theta(n \log n)\), Selection \(\Theta(n)\).
  • אבל: מציאת \(k\) האיברים הקטנים ביותר (לא רק ה-k-בגודלו) דורשת \(\Theta(n \log k)\) — בין סלקציה למיון.
💡 6. שאלת מבחן: למה לא ניתן ל-bound לעולם את QuickSelect ל-O(n) worst-case בלי MM-Select?
כל אלגוריתם שבוחר pivot ב-\(O(1)\) זמן (קצה / אקראי / כל בחירה לא-תלויה בערכי המערך) — יש קלט אדברסרי שגורם לפיצול גרוע. רק בחירת pivot חכמה שעצמה דורשת \(\Omega(n)\) (כמו median-of-medians) יכולה לערוב partition מאוזן.

זו אבסטרקציה יפה: הזמן הלינארי של MM-Select מגיע "במחיר" עבודה לינארית בבחירת pivot — וזה משתלם רק כי בכל פעם הצמצום בגודל הבעיה הוא גם פקטור קבוע.

11. סיכום הרצאה

מה הכי חשוב לזכור?
  1. Counting Sort: שלמים ב-{1..k}. שלושה שלבים: ספירה ⟹ סכומים מצטברים ⟹ הצבה מהסוף. \(\Theta(n+k)\). יציב.
  2. Bucket Sort: מפתחות אחידים ב-[0, n). Avg \(\Theta(n)\), Worst \(\Theta(n^2)\).
  3. Radix Sort: d מיונים יציבים LSD→MSD. \(\Theta(dn)\). שאלת מבחן: בסיס \(n\) לטווח \([0, n^d]\).
  4. Order Stats: rank, median (rank ≈ n/2), Select(A, k).
  5. QuickSelect: Avg \(\Theta(n)\), Worst \(\Theta(n^2)\). הוכחה באינדוקציה עם \(c = \max\{b, 4a\}\).
  6. MM-Select: Worst \(\Theta(n)\) ע"י median-of-medians. ערובת partition 30%/70%. \(T(n) \leq T(n/5) + T(7n/10) + \Theta(n)\) ⟹ \(\leq 10n\). הקבוע גדול בפועל.

טבלת השוואה — לאלוורית' המיון הכוללת

AlgorithmWorstAverageהשוואות?
Insertion / Bubble\(\Theta(n^2)\)\(\Theta(n^2)\)כן
Heap / Merge\(\Theta(n \log n)\)\(\Theta(n \log n)\)כן
Quick\(\Theta(n^2)\)\(\Theta(n \log n)\)כן
Counting\(\Theta(n + k)\)\(\Theta(n + k)\)לא
Bucket\(\Theta(n^2)\)\(\Theta(n)\)לא
Radix\(\Theta(d(n+k))\)\(\Theta(d(n+k))\)לא

ההרצאה הבאה: Binary Search Trees — מבנה נתונים חדש לחיפוש דינמי.