לדלג לתוכן

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

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


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


שימו לב:

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

פרימיטיבים

[עריכה]

פקודות מסויימות פשוטות כ"כ שברור שכ"א אורכת . להלן מספר דוגמות:

Print( "Hello" )
a = 3
if i == j

פעולות חשבוניות

[עריכה]

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

++b
mid = (left + right) / 2

קריאות לפונקציות

[עריכה]

נניח שאנו רואים בקוד את הפקודה Foo(a, B, 3). הפונקציה Foo אורכת זמן כלשהו, אך כמה עולה הקריאה עצמה? חשוב להבין שהקריאה עצמה אורכת זמן.

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

רצפים

[עריכה]

כל רצף של פקודות עולה סך הזמן של כל אחת מהפקודות. לדוגמה, אם Foo(a, B, 3) עולה (כאשר הוא (Length(B)), אז קטע הקוד הבא אורך .

1	a = 3

2	Foo(a, B, 3)

3	if j == 1
4		print "Hello"

1, 3, ו4 אורכות כל אחת , ו2 אורכת . סה"כ נקבל (עפ"י אדיטיביות).

לולאות

[עריכה]

נתבונן בלולאה בקטע הקוד הבא.

1	for i in [1, , n]
2		Foo(a, B, 3)

ברור לחלוטין ש2 מתבצעת פעמים. לכן, אם Foo היא , אז 2 תורמת לסיבוכיות, ואם Foo היא , אז 2 תורמת לסיבוכיות.

מה סיבוכיות שתי השורות, 1-2? במקרה הראשון גם כן , ובמקרה השני גם כן .‏ 1 עולה : ככלות הכל, בשפת C, היינו כותבים אותה כך:

for(i = 1; i <= n; ++i)

לכן במקרה הראשון הסיבוכיות היא , ובמקרה השני .

באופן דומה, נתבונן בלולאה הכפולה בקטע הקוד הבא.

1	for i in [1, , n]
2		for j in [1, , m]
3			Foo(a, B, 3)

ברור ש2 מתבצעת פעמים, ו3 מתבצעת פעמים. לכן אם Foo היא , אז הסיבוכיות היא ואם Foo היא , אז הסיבוכיות היא

לסיום, נתבונן בלולאה הכפולה בקטע הקוד הבא.

1	for i in [1, , n]
2		for j in [1, , f(i)]
3			Print( "Hello" )

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

פונקציות רקורסיביות רגילות

[עריכה]

להלן דוגמה לפונקציה רקורסיבית.

Foo(n)
1	if n == 1
2		return 1
	
3	return Foo(n - 1)

כיצד אפשר לנתח זאת? לרוב כדאי לפעול לפי הרעיון הבא. נגדיר את זמן הריצה של Foo בהנתן קלט בגודל כ. בגוף הפונקציה, נחייב כל קריאה לפונקציה לפי הגדרה זאת.

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



דוגמה:

להלן הפסוודו-קוד של מיון מיזוג:


# Merge sort. 
# Takes an array (Values).
# Returns an array of the same elements, sorted in increasing order.
Merge-Sort(Values)
1	if Length(Values) <= 1
2		return Values

3	mid = (1 + Length(Values)) / 2
		
4	L = Merge-Sort( Values[1, ... , mid] )
5	R = Merge-Sort( Values[mid + 1, ... , Length(Values)] )
	
6	return Merge(L, R)

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

עץ הפרישה של מיון מיזוג.
עץ הפרישה של מיון מיזוג.

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



פונקציות רקורסיביות עם Memoization

[עריכה]

שקלו לדלג על נושא זה

נושא זה מניח שאתה מכיר תכנון דינאמי, ובפרט memoization. אם זה אינו המצב, חזור לכאן לאחר שתלמד נושא זה.



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

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

(את ההסבר לכך נלמד בבעיית דג הסלמון.)


כדאי לדעת:

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


הפרק הקודם:
נוסחאות נסיגה
מציאת סיבוכיות פסוודו-קוד
תרגילים
הפרק הבא:
מיון הכנסה ומיזוג