מבני נתונים ואלגוריתמים - מחברת קורס/מבני נתונים/תורי קדימויות/תרגילים
מבנה נתונים לחציון דינאמי
עריכהשאלה
עריכה
הגדרה: החציון של קבוצת מספרים מוגדר כך: נגדיר כסדרה ממוינת המורכבת מאיברי ; אם אי זוגי, החציון הוא האיבר האמצעי, ואם זוגי, אז החציון הוא ממוצע שני האיברים האמצעיים. |
דוגמה:
|
רוצים לממש ביעילות מבנה נתונים Median
בעל הממשק הבא:
# Makes a Median data-structure.
Make-Median()
# Adds another value (v) to a Median data-structure (m).
Insert(m, v)
# Returns the median of all the values in
# a Median data-structure (m).
Med(m)
להלן דוגמה לשימוש במבנה הנתונים:
# Makes a median data-structure.
m = Make-Median()
# Inserts 1.
Insert(m, 1)
# Inserts 9.
Insert(m, 9)
# Inserts 2.
Insert(m, 2)
# Prints 2 (the median of {1, 9, 2}).
Print( Med(m) )
# Inserts 100.
Insert(m, 100)
# Prints 4.5 (the median of {1, 9, 2, 100}).
Print( Med(m) )
אנא הסבר כיצד לממש את מבנה הנתונים ביעילות.
תשובה
עריכהלהלן מימוש יעיל אפשרי:
Median
# A min binary heap.
# This will be used to store the half of the larger values.
1 min-bh
# A max binary heap.
# This will be used to store the half of the smaller values.
2 max-bh
# Makes a Median data-structure.
Make-Median()
1 m = Median()
2 m.min-bh = Make-Binary-Heap()
3 m.max-bh = Make-Binary-Heap()
# Adds another value (v) to a Median data-structure (m).
Insert(m, v)
1 if Size(m.max-bh) == 0 or v < Med(m)
2 Insert(m.max-bh)
3 else
4 Insert(m.min-bh)
5 if Size(m.max-bh) > Size(m.min-bh) + 1
6 Insert(m.min-bh, Delete-Max(m.max-bh))
7 else if Size(m.min-bh) > Size(m.max-bh) + 1
8 Insert(m.max-bh, Delete-Min(m.min-bh))
# Returns the median of all the values in a Median data-structure (m).
Med(m)
1 if Size(m.min-bh) == Size(m.max-bh)
2 return (Min(m.min-bh) + Max(m.max-bh)) / 2
3 if Size(m.min-bh) > Size(m.max-bh)
4 return Min(m.min-bh)
5 return Max(m.max-bh)
הנה הסבר לפתרון.
מבנה הנתונים כולל שני תורי קדימויות :max-bh
וmin-bh
(שורות 1-2 של Median
ו2-3 של Make-Median
). הראשונה מיועדת לשמירת חצי האיברים הקטנים יותר, והיא ערימה בינרית השומרת על האיבר הגדול ביותר בראש הערימה; השנייה מיועדת לשמירת חצי האיברים הגדולים יותר, והיא ערימה בינרית השומרת על האיבר הקטן ביותר בראש הערימה.
אם הפרש הגדלים בין גדלי שתי הערימות הוא (כלומר, שתי הערימות מחלקות את כל
האיברים לשני חלקים שווים כמעט בגדלם), אז החציון הוא בהכרח האיבר בראש אחת מהערימות, או ממוצע ראשי האיברים (שורות 1-5 של Med
). זאת אומרת שמציאת החציון דורשת לכל היותר שתי קריאות לSize
, Min
, וMax
של ערימה בינרית - פעולות שהן כל אחת.
כעת נשאר רק לדאוג שהפרש הגדלים בין שתי הערימות הוא אכן . כאשר מכניסים איבר חדש, בוחרים לאיזו ערימה להכניס אותו, על ידי השוואתו לחציון (שורות 1-4 של Insert
). אם אחת הערימות נהיית גדולה מדי (לדוגמה max-bh
), אז שולפים ממנה את איבר הראש, ודוחפים אותו לערימה השנייה.
מבדיקת פעולות הערימה הבינרית שנעשות, קל להווכח שסיבוכיות Insert
היא , וסיבוכיות Med
היא .
מיזוג k-ארי
עריכהשאלה
עריכהבמיון מיזוג ראינו את הפונקציה Merge, המקבלת שני מערכים ממוינים ומחזירה מערך ממוין של איחוד איבריהם בסיבוכיות לינארית. בשאלה זו נרחיב זאת למיזוג מערכים ממוינים.
נניח שברשותנו מערך Values-Arrays = [Values_1, ..., Values_k]
(שים לב שזהו מערך של מערכים). אנא כתוב פונקציה יעילה K-Merge(Values-Array)
המקבלת את מערך המערכים הממוינים, ומחזירה מערך ממוין של איחוד איבריהם.
לכל , נגדיר Length(Values_i)
, ונניח ש . אנא נתח תשובתך במונחי ו
תשובה
עריכהגרסה מפושטת
עריכהרעיון כללי ופסוודו-קוד
עריכהנפתור ראשית גרסה מפושטת מעט של הבעיה:
- נניח שהקלט הוא מערך של רשימות מקושרות ממויינות (ולא מערך של מערכים ממויינים)
- האלגוריתם שנכתוב ידפיס את הערכים הממוזגים (ולא יחזיר מערך של הערכים הממוזגים)
השינויים הללו אינם משנים מהותית את האלגוריתם. עיקר תרומתם בפישוט הפסוודו-קוד (מעט).
דוגמה: |
נבנה תור קדימות שבו מקום ל איברים. תור קדימויות זה יכיל זוגות: האיבר הראשון בכל זוג מתאר ערך איבר שנלקח מרשימה כלשהי, והאיבר השני בכל זוג מתאר את מספר הרשימה ממנו הוא נלקח. קריטריון ההשוואה בין הזוגות הוא עפ"י איבריהן הראשונים (האיברים שנלקחו מהמהערכים).
נתחיל בכך שנכניס את האיבר הראשון מכ"א מהמערכים. להלן הפסוודו-קוד לכך:
# Takes an array of sorted linked lists (Values-Array)
# Returns a sorted array whose entries are the those of the union
# of the arrays in Values-Array.
K-Merge(Values-Array)
1 k = Length(Values-Array)
# Make a binary heap (that can hold up to k elements).
2 bh = Make-Binary-Heap()
3 for i in [1, ..., k]
4 p = ( Delete-Front( Value-Array[i] ), i)
5 Insert(bh, p)
...
דוגמה: נניח שמדובר ברשימות המקושרות מהתרשים הקודם. לאחר הפעלת השורות הראשונות, כך ייראו הרשימות ותור הקדימויות: כל אחת מהרשימות "איבדה" חוליה, ונוספו שלושה זוגות לתור הקדימויות: הזוג מציין שהאיבר שנלקח מרשימה 2 הוא 1.07, הזוג מציין שהאיבר שנלקח מרשימה 1 הוא 1.08, והזוג מציין שהאיבר שנלקח מרשימה 3 הוא 1.1. הזוג הקטן ביותר בתור הקדימויות כעת הוא , מפני ש , ורק האיבר הראשון של כל זוג רלוונטי לסדר מבחינת תור הקדימויות. |
כעת נעבוד בלולאה כל עוד תור הקדימויות אינו ריק. נשלוף את הזוג הקטן ביותר. את איברו הראשון נדפיס, ובאיברו השני נשתמש כדי להחליט מאיזו רשימה מקושרת לשלוף את איבר הראש הבא. אם הרשימה אינה ריקה, נקח עוד איבר, ניצור ממנו זוג, ונכניס אותו לתור הקדימויות.
דוגמה: באיטרציה הראשונה, הזוג הקטן ביותר הוא . מדפיסים 1.07, ולוקחים את איבר הראש הבא מרשימה 1. כעת ייראו הרשימות והתור כך: |
עכשיו תורכם: מה יקרה באיטרציה השניה? |
באיטרציה השניה הזוג הקטן ביותר הוא . מדפיסים 1.08, ולוקחים את איבר הראש הבא מרשימה 2. כעת ייראו הרשימות והתור כך:
להלן הפסוודו-קוד המלא לגרסה זו:
# Takes an array of sorted linked lists (Values-Array)
# Returns a sorted array whose entries are the those of the union
# of the arrays in Values-Array.
K-Merge(Values-Array)
1 k = Length(Values-Array)
# Make a binary heap (that can hold up to k elements).
2 bh = Make-Binary-Heap()
3 for i in [1, ..., k]
4 p = ( Delete-Front( Value-Array[i] ), i)
5 Insert(bh, p)
6 while Size(bh) > 0
7 (v, i) = Delete-Min(bh)
8 Print(v)
9 if not Empty( Value-Array[i] )
10 p = ( Delete-Front( Value-Array[i] ), i )
11 Insert(bh, p)
נכונות
עריכההוכחת הנכונות דומה מאד לזו של Merge
(הממזגת שני מערכים). אפשר להוכיח (פורמאלית באינדוקציה), שהאיבר הקטן ביותר שטרם הודפס הוא אחד מ(לכל היותר) האיברים הראשונים בכל רשימה מקושרת. בנוסף, הוכחנו שהתור בוחר את האיבר הקטן ביותר, מה שאומר שבחרנו את האיבר הקטן ביותר בקבוצת האיברים המכילה בהכרח את האיבר הקטן ביותר.
ניתוח סיבוכיות
עריכהקל להווכח שהשורות המשפיעות על הסיבוכיות הן הלולאות המתפעלות את התור. נשים לב שכל איבר ברשימות המקושרות יכול להכנס ולצאת לתור בדיוק פעם אחת, ולכן יש פעולות כאלה. נשים גם לב שהתור לעולם לא יכיל יותר מ איברים, ולכן כל פעולה כזו היא . הסיבוכיות, לכן, היא .
עכשיו תורכם: הראה שקיימים איברים כך שהסיבוכיות היא , והסק מכאן שסיבוכיות המקרה הגרוע הנה. |
הגרסה לבעיה המקורית
עריכהבעזרת מספר שינויים, אפשר לפתור את הבעיה המקורית.
- כעת תור הקדימויות יכיל שלישיות. האיבר הראשון מכל שלישיה הוא ערך מהמערך, האיבר השני הוא מספר המערך, והשלישי הוא האינדקס הבא ממנו יש לקחת ערך מהמערך.
- במקום להדפיס את הערכים הממוזגים, נשמור אותם למערך ונחזיר אותו.
# Takes an array of sorted arrays (Values-Array)
# Returns a sorted array whose entries are the those of the union
# of the arrays in Values-Array.
K-Merge(Values-Array)
1 k = Length(Values-Array)
# Make a binary heap (that can hold up to k elements).
2 bh = Make-Binary-Heap()
3 total-size = 0
4 for i in [1, ..., k]
5 total-size = total-size + Length(Value-Array[i])
6 p = (Value-Array[i][1], i, 2)
7 Insert(bh, p)
8 Merged = Make-Array(total-size)
9 m = 1
10 while Size(bh) > 0
11 (v, i, j) = Delete-Min(bh)
12 Values[m++] = v
13 if j ≤ Length(Value-Arrays[i])
14 p = (Value-Array[i][j], i, j + 1)
15 Insert(bh, p)
16 return Merged
Heapsort
עריכהשאלה
עריכהלהלן אלגוריתם הידוע בשם Heapsort
(מיון תור קדימות):
# Heap sort.
# Takes an array (Values), and sorts it in increasing order.
Heapsort(Values)
1 bh = Array-To-Heap(Values)
2 for i in [1, ..., Length(Values)]
3 Values[i] = Delete-Min(bh)
- אנא הוכח שהאלגוריתם עובד
- אנא נתח את סיבוכיות האלגוריתם.
להלן ווריאציה אחרת של האלגוריתם:
# Heap sort.
# Takes an array (Values), and sorts it in increasing order.
Heapsort(Values)
1 bh = Make-Binary-Heap()
2 for i in [1, ..., Length(Values)]
3 Insert(bh, Values[i])
4 for i in [1, ..., Length(Values)]
5 Values[i] = Delete-Min(bh)
אנא חזור על שני הסעיפים הקודמים לגבי ווריאציה זו.
תשובה
עריכהגרסה עם Build-Heap
עריכה- קל לראות שהאלגוריתם עובד: כפי שראינו בבניית ערימה בינרית ממערך,
Build-Heap
מייצרת ערימה תקינה ממערך, והריDelete-Min
מוציאה ומחזירה את האיבר הקטן ביותר. - הסיבוכיות היא במקרה הגרוע. ראינו מימוש ל
Build-Heap
שעובד בזמן , ואנו יודעים שסיבוכיותDelete-Min
על ערימה בת איברים היא במקרה הגרוע. סיבוכיות הלולאה 3-4, לכן, היא (ראה גם טורים שימושיים בסדרי גדילה).
גרסה עם סדרת פעולות Insert
עריכה- אין שינוי בנכונות, כי הלולאה 3-4 גם כן מייצרת ערימה תקינה מהמערך, ואין הבדל מנקודה זו.
- אף הסיבוכיות אינה שונה, מפני שבמקום הביטוי (בסעיף הקודם), נקבל כעת . שני הביטויים שקולים (מבחינת סדרי הגדילה).
אופטימיזציית פעולה יחידה מממשק תור קדימויות
עריכהשאלה
עריכהבאפליקציה מסויימת, יש צורך במבנה נתונים התומך בפעולות Insert(pq, v)
, Delete-Min(pq)
, וMin(pq)
. משמעות פעולות אלו היא בדיוק זו של תור קדימויות - מדובר במבנה נתונים המשתמש באיזושהי השוואה כדי לספק את האיבר הקטן בכל עת.
- ידוע שבאפליקציה זו, מספר הפעולות הצפוי מסוג
Delete-Min(pq)
וMin(pq)
הוא זניח יחסית למספר הפעולות הצפוי מסוגInsert(pq, v)
. אנא הצע מימוש כך שInsert(pq, v)
יהיה יעיל ככל האפשר. מה סיבוכיות שתי הפעולות האחרות במימוש שהצעת? - האם ייתכן מימוש לתור קדימויות בו הן
Insert(pq, v)
והןDelete-Min(pq)
יעבדו בזמן ?
שימו לב: #בסעיף הראשון, עליך למצוא יעיל ככל האפשר לInsert(pq, v) . אין צורך למצוא מימוש יעיל במיוחד לשתי הפעולות האחרות.
|
תשובה
עריכה- סעיף זה הוא טריביאלי. נשתמש ברשימה מקושרת דו-כוונית (ייתכנו מימושים אחרים, כמובן).
Insert
תקרא לInsert-Front
של רשימה מקושרת, ותעבוד בזמן . אתMin
וDelete-Min
נממש בעזרת לולאות על חוליות הרשימה. סיבוכיות כל אחת מפעולות אלה תהיה לינארית במספר החוליות במקרה הגרוע. - נזכר שבהנתן תור קדימויות, ניתן לממש בעזרת סידרת פעולות
Insert
ולאחריה סידרת פעולותDelete-Min
(באופן כמעט זהה לHeapsort
). לו כל פעולה היתה עובדת בזמן , אז ניתן היה למיין בזמן לינארי, בניגוד לחסם התחתון על מיון מבוסס-השוואות שלמדנו.
איחוד תורי קדימויות
עריכהשאלה
עריכהרוצים לממש את הפונקציה Union(bh_1, bh_2)
, המקבלת שתי ערימות בינריות, ומחזירה ערימה בינרית שאיבריה הם איחוד איברי bh_1
וbh_2
.
אנא הסבר במילים אך באופן ברור כיצד לממש את הפונקציה, ונתח את סיבוכיות מימושך.
תשובה
עריכהמבני נתונים ואלגוריתמים - מחברת קורס/מבני נתונים/תורי קדימויות/תרגילים/איחוד תורי קדימויות/תשובה
מימושים חלופיים לבניית תור קדימויות ממערך
עריכהשאלה
עריכההסטודנט העיראקי איפ חמיס תברא מתבונן בקטע הקוד הבא:
# Makes a binary heap from an array (Values).
Build-Heap(Values)
1 bh = Make-Priority-Queue()
2 bh.size = Length(Values)
3 for i in [1, ..., Length(Values)]
4 bh.Values[i] = Values[i]
5 Arrange(bh)
6 return bh
קטע קוד זה מקבל מערך, ומחזיר ערימה בינרית . איפ חוכך בדעתו כיצד מומשה הפונקציה Arrange
. (אנו למעשה כבר ראינו את המימוש הבא:
Arrange(bh)
1 for i in [Length(bh.Values), ..., 1]
2 Bubble-Down(bh, i)
אך הוא אינו יודע זאת).
מספר אפשרויות עולות על דעתו:
Arrange(bh)
1 for i in [⌊Length(bh.Values) / 2⌋, ..., 1]
2 Bubble-Down(bh, i)
Arrange(bh)
1 Merge-Sort(bh.Values)
Arrange(bh)
1 for i in [1, ..., Length(bh.Values)]
2 Bubble-Down(bh, i)
Arrange(bh)
1 for i in [1, ..., Length(bh.Values)]
2 Bubble-Up(bh, i)
Arrange(bh)
1 for i in [Length(bh.Values), ..., 1]
2 Bubble-Up(bh, i)
עבור כל אחת מהאפשרויות, אנא הוכח או הפרך את הטענה שבהכרח תתקבל ערימה תקנית, ונתח את הסיבוכיות במקרה הגרוע (בלי קשר לשאלה האם תתקבל ערימה תקנית).
תשובה
עריכה- הטענה נכונה. נניח שבערימה יש איברים. האיבר האחרון בערימה הוא באינדקס , ואביו (אם יש לו) בהכרח יושב ב . לכן, למרות שהלולאה אינה עוברת על כל האיברים, היא מדלגת רק על ערימות בעלי איבר יחיד, שהן, עפ"י תכונת הערימה, תקינות. הלולאה תתקן כל ערימה שדורשת תיקון, בדיוק מהסיבה שראינו בהרצאה. הסיבוכיות היא . אפשר לראות שהלולאה עוברת על חצי מהאיברים, ולכן היא . מצד שני, היא עושה פחות מBuild-Heap, וזו היתה , ולכן גם לולאה זו .
- הטענה נכונה. אם ילדו השמאלי של איבר במקום נמצא באינדקס , וילדו הימני נמצא באינדקס , אז כל מסלול מהשורש לעלה עובר על סדרת אינדקסים מונוטונית עולה. לכן, אם המערך ממויין, כל מסלול מהשורש לעלה יעבור על סדרת ערכים מונוטונית לא-יורדת. הסיבוכיות הנה מיון מיזוג, כלומר .
- הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבוכיות הנה , בדיוק מהניתוח שכבר ראינו לגבי הפתרון מההרצאה.
- הטענה נכונה. היות ש
Bubble-Up(bh, i)
אינו יכול להשפיע על איברים באינדקס מעל , הקוד שקול לסדרת פעולותInsert
. מאותה סיבה, הסיבכויות היא זו של סדרת פעולות Insert, כלומר . - הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבכויות היא זו של סדרת פעולות
Insert
, כלומר .