לדלג לתוכן

מבני נתונים ואלגוריתמים - מחברת קורס/מבני נתונים/תורי קדימויות/תרגילים

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי


מבנה נתונים לחציון דינאמי

[עריכה]

שאלה

[עריכה]

הגדרה:

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




דוגמה:

  1. החציון של הוא 2 (2 הוא האיבר האמצעי של איבריה הממויינים).
  2. החציון של הוא 2 (2 הוא האיבר האמצעי של איבריה הממויינים).
  3. החציון של הוא 2.5 (2 ו3 הם איבריה האמצעיים של איבריה הממויינים).
  4. החציון של הוא 2 (2 הוא האיבר האמצעי של איבריה הממויינים).


רוצים לממש ביעילות מבנה נתונים 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 (הממזגת שני מערכים). אפשר להוכיח (פורמאלית באינדוקציה), שהאיבר הקטן ביותר שטרם הודפס הוא אחד מ(לכל היותר) האיברים הראשונים בכל רשימה מקושרת. בנוסף, הוכחנו שהתור בוחר את האיבר הקטן ביותר, מה שאומר שבחרנו את האיבר הקטן ביותר בקבוצת האיברים המכילה בהכרח את האיבר הקטן ביותר.

ניתוח סיבוכיות
[עריכה]

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


עכשיו תורכם:

הראה שקיימים איברים כך שהסיבוכיות היא , והסק מכאן שסיבוכיות המקרה הגרוע הנה

.

הגרסה לבעיה המקורית

[עריכה]

בעזרת מספר שינויים, אפשר לפתור את הבעיה המקורית.

  1. כעת תור הקדימויות יכיל שלישיות. האיבר הראשון מכל שלישיה הוא ערך מהמערך, האיבר השני הוא מספר המערך, והשלישי הוא האינדקס הבא ממנו יש לקחת ערך מהמערך.
  2. במקום להדפיס את הערכים הממוזגים, נשמור אותם למערך ונחזיר אותו.
# 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)
  1. אנא הוכח שהאלגוריתם עובד
  2. אנא נתח את סיבוכיות האלגוריתם.


להלן ווריאציה אחרת של האלגוריתם:

# 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

[עריכה]
  1. קל לראות שהאלגוריתם עובד: כפי שראינו בבניית ערימה בינרית ממערך, Build-Heap מייצרת ערימה תקינה ממערך, והרי Delete-Min מוציאה ומחזירה את האיבר הקטן ביותר.
  2. הסיבוכיות היא במקרה הגרוע. ראינו מימוש לBuild-Heap שעובד בזמן ,‏ ואנו יודעים שסיבוכיות Delete-Min על ערימה בת איברים היא במקרה הגרוע. סיבוכיות הלולאה 3-4, לכן, היא (ראה גם טורים שימושיים בסדרי גדילה).

גרסה עם סדרת פעולות Insert

[עריכה]
  1. אין שינוי בנכונות, כי הלולאה 3-4 גם כן מייצרת ערימה תקינה מהמערך, ואין הבדל מנקודה זו.
  2. אף הסיבוכיות אינה שונה, מפני שבמקום הביטוי (בסעיף הקודם), נקבל כעת . שני הביטויים שקולים (מבחינת סדרי הגדילה).

אופטימיזציית פעולה יחידה מממשק תור קדימויות

[עריכה]

שאלה

[עריכה]

באפליקציה מסויימת, יש צורך במבנה נתונים התומך בפעולות Insert(pq, v),‏ Delete-Min(pq),‏ וMin(pq).‏ משמעות פעולות אלו היא בדיוק זו של תור קדימויות - מדובר במבנה נתונים המשתמש באיזושהי השוואה כדי לספק את האיבר הקטן בכל עת.


  1. ידוע שבאפליקציה זו, מספר הפעולות הצפוי מסוג Delete-Min(pq) וMin(pq) הוא זניח יחסית למספר הפעולות הצפוי מסוג Insert(pq, v).‏ אנא הצע מימוש כך שInsert(pq, v) יהיה יעיל ככל האפשר. מה סיבוכיות שתי הפעולות האחרות במימוש שהצעת?
  2. האם ייתכן מימוש לתור קדימויות בו הן Insert(pq, v) והן Delete-Min(pq) יעבדו בזמן ?


שימו לב:

#בסעיף הראשון, עליך למצוא יעיל ככל האפשר לInsert(pq, v). אין צורך למצוא מימוש יעיל במיוחד לשתי הפעולות האחרות.
  1. אפשר לפתור את הסעיף השני גם מבלי לפתור את הסעיף הראשון.

תשובה

[עריכה]
  1. סעיף זה הוא טריביאלי. נשתמש ברשימה מקושרת דו-כוונית (ייתכנו מימושים אחרים, כמובן). Insert תקרא לInsert-Front של רשימה מקושרת, ותעבוד בזמן . את Min וDelete-Min נממש בעזרת לולאות על חוליות הרשימה. סיבוכיות כל אחת מפעולות אלה תהיה לינארית במספר החוליות במקרה הגרוע.
  2. נזכר שבהנתן תור קדימויות, ניתן לממש בעזרת סידרת פעולות 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)

עבור כל אחת מהאפשרויות, אנא הוכח או הפרך את הטענה שבהכרח תתקבל ערימה תקנית, ונתח את הסיבוכיות במקרה הגרוע (בלי קשר לשאלה האם תתקבל ערימה תקנית).

תשובה

[עריכה]
  1. הטענה נכונה. נניח שבערימה יש איברים. האיבר האחרון בערימה הוא באינדקס , ואביו (אם יש לו) בהכרח יושב ב. לכן, למרות שהלולאה אינה עוברת על כל האיברים, היא מדלגת רק על ערימות בעלי איבר יחיד, שהן, עפ"י תכונת הערימה, תקינות. הלולאה תתקן כל ערימה שדורשת תיקון, בדיוק מהסיבה שראינו בהרצאה. הסיבוכיות היא . אפשר לראות שהלולאה עוברת על חצי מהאיברים, ולכן היא . מצד שני, היא עושה פחות מBuild-Heap, וזו היתה , ולכן גם לולאה זו .
  2. הטענה נכונה. אם ילדו השמאלי של איבר במקום נמצא באינדקס , וילדו הימני נמצא באינדקס , אז כל מסלול מהשורש לעלה עובר על סדרת אינדקסים מונוטונית עולה. לכן, אם המערך ממויין, כל מסלול מהשורש לעלה יעבור על סדרת ערכים מונוטונית לא-יורדת. הסיבוכיות הנה מיון מיזוג, כלומר .
  3. הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבוכיות הנה , בדיוק מהניתוח שכבר ראינו לגבי הפתרון מההרצאה.
  4. הטענה נכונה. היות ש Bubble-Up(bh, i) אינו יכול להשפיע על איברים באינדקס מעל , הקוד שקול לסדרת פעולות Insert. מאותה סיבה, הסיבכויות היא זו של סדרת פעולות Insert, כלומר .
  5. הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבכויות היא זו של סדרת פעולות Insert, כלומר .