ניתוח סיבוכיות - פונקציות רקורסיביות פיתוח טלסקופי
ננסה להשתמש בכך שהפונקציה היא רקורסיבית על מנת לרשום גם עבור הסיבוכיות ביטוי רקורסיבי.
factorial() 3 מתחילים מכתיבת ביטוי לא מפורש ל-( T( ביטוי רקורסיבי בעצמו usiged log factorial(usiged it ) { if ( == 0) retur 1; retur * factorial(-1); : במקרה שלנו, נקבל מקוד הפונקציה את הביטוי הבא האם הקבוע הזה בכלל חשוב? T ( ) = T ( 1) + C
factorial() 4 כעת, ננסה לפתוח את הנוסחה, כי מטרתנו לקבל ביטוי לא רקורסיבי! (-1) בביטוי שרשמנו. נקבל כי זמן הריצה עבור הוא: נציב 1- במקום T ( 1) = T ( 2) + C נקבל: זה ניתן להציב בחזרה בביטוי ל-( T(. ערך T ( ) = T ( 1) + C = ( T ( 2) ) = + C + C= = T ( 2) + 2C באופן דומה נוכל להמשיך ולהציב עבור 2-, 3- וכן הלאה.
factorial() 5 :T() נמשיך כך, ונקבל את הפיתוח הטלסקופי של T ( ) = T ( 1) + C = ( T ( 2) ) = + C + C= = T ( 2) + 2 C = = T ( 3) + 3C = = T ( k) + k C T ( 2) = T ( 3) + C כאשר השורה התחתונה היא השורה ה- k בפיתוח הטלסקופי.
factorial() מתי עוצרים? 6 שרשרת הפיתוח נעצרת כאשר מגיעים למקרה הבסיס. נשים לב שעבור מקרה זה, הביטוי שרשמנו בתחילה ל- T() איננו תקף. במקרה של,factorial() מקרה הבסיס הוא 0=, ולכן כאשר נגיע ל-( T(0 בפיתוח נעצור. ובכן, מתי נגיע ל-( T(0 בפיתוח הטלסקופי? לשם כך נתבונן בביטוי שחישבנו עבור השורה ה- kבפיתוח: T ( ) = T ( k) + k C אנו רואים כי עבור ההצבה k= (כלומר בשורה ה- של הפיתוח) נקבל בדיוק (0)T. נציב אם כך k=בביטוי זה ונקבל: T ( ) = T (0) + C
factorial() 7 סיימנו! זהו, (0)T הוא מספר קבוע, כיוון שהוא איננו תלוי ב-. לכן נוכל להחליפו בקבוע C 1 (זהו למעשה מספר הפעולות הדרושות עבור הקלט 0=), ואנו מקבלים את הביטוי המפורש ל-( T( : T ( ) = T (0) + C = = C + C = = Θ( ) 1 2
פיתוח טלסקופי של Biary Search 8 בכל איטרציה של הפונקציה הרקורסיבית, מתבצע מספר קבוע C של פעולות, וכן מתבצעת קריאה רקורסיבית עם קלט בגודל 2/. לפיכך, הביטוי הלא מפורש ל-( T( הוא: ( ) T ( ) = T + C 2 הצבת 2/ בביטוי זה נותנת: ( ) ( ) T = T + C 2 4
פיתוח טלסקופי של Biary Search 9 נמשיך כך ונקבל את הפיתוח הטלסקופי של.T() ביטוי לשורה ה- kבפיתוח, ולכן נרשום: אנו רוצים לקבל ( ) T ( ) = T + C= 2 ( T( ) ) = + C + C = 4 ( ) = T + 2C= 4 ( ) = T + 3C = 8 ( ) k = T + k C 2
פיתוח טלסקופי של Biary Search 10 במקרה שלנו מקרה הבסיס הוא עבור מערך בגודל 1. מתי נגיע ל-( T(1? ובכן, ניתן לראות שזה מתרחש כאשר 2, k = שזה אומר.k=log() שרצינו): (כפי ל-( T(, ונקבל זאת בביטוי נציב T ( ) = T (1) + log( ) C = = C + log( ) C = 1 2 = Θ(log( ))
סיבוכיות של Bubble Sort 11 ננתח כעת את הסיבוכיות של אלגוריתם המיון bubble sort שפגשנו בתחילת הפרק: void bubble_sort(it A[], it N) { it i=0; if (N <= 1) retur; bubble_sort(a+1, N-1); while(a[i] > A[i+1]) { swap(a+i, A+i+1); if ((++i) == N-1) break;
סיבוכיות של Bubble Sort 12 כל קריאה רקורסיבית מקטינה את במקרה זה הוא.Θ() ב- 1, ולכן עומק הרקורסיה סיבוכיות זיכרון: כל קריאה רקורסיבית דורשת זיכרון בגודל קבוע, ולכן סיבוכיות הזיכרון היא.Θ() זמן ריצה: לשם כך נבצע פיתוח טלסקופי של זמן הריצה. נשים לב שבכל איטרציה של הפונקציה הרקורסיבית מתבצעת קריאה רקורסיבית עם מערך בגודל 1-, וכן מתבצעות (במקרה הגרוע) עוד פעולות החלפה. לכן זמן הריצה מקיים: T ( ) = T ( 1) +
פיתוח טלסקופי של Bubble Sort 13 מהפיתוח הטלסקופי נקבל במפורש את זמן הריצה: T ( ) = T ( 1) + = = T ( 2) + ( 1) + = = T ( 3) + ( 2) + ( 1) + = = T ( k) + ( ( k 1)) + + ( 1) + = = T (0) + 1+ 2 + + ( 1) + = 1 ( + 1) = C + = Θ 2 2 ( )
ניתוח אלגוריתם Merge Sort 14 ננתח כעת את הסיבוכיות של מיון,merge sort בגרסתו הרקורסיבית. נזכיר ראשית סקיצה של אלגוריתם merge sort הרקורסיבי: merge_sort(a[n]) { if (N <= 1) retur; allocate tmp[n]; merge_sort( A[0..N/2] ); merge_sort( A[N/2+1..N-1] ); tmp = merge( A[0..N/2], A[N/2+1..N-1] ); memcpy(a tmp);
סיבוכיות הזמן של Merge Sort 15 נשים לב שבכל פעם שנכנסים לתוך קריאה רקורסיבית, אורך המערך קטן פי 2. לכן, העומק המקסימאלי של הרקורסיה הוא.Θ(log()) זמן ריצה: נבצע כרגיל פיתוח טלסקופי עבור זמן הריצה. מתבצעות איטרציה של merge_sort() בכל שניתן להבחין, כפי שתי קריאות רקורסיבית, כל אחת עם מערך בגודל 2/. כמו כן מתבצעת גם פעולת,merge שדורשת עוד פעולות. אנו מקבלים את הביטוי הבא: ( ) T ( ) = 2 T + 2 13 מבוא למדעי המחשב - תרגולים - פרק רן רובינשטיין
פיתוח טלסקופי של Merge Sort 16 הפיתוח הטלסקופי של ביטוי זה הינו: ( ) T ( ) = 2 T + = 2 ( ( ) T ) = 2 2 + + = ( ) 4 ( ) 8 ( ) 2 4 2 = 4 T + 2= = 8 T + 3 = = + k = k 2 T k
פיתוח טלסקופי של Merge Sort 17.k=log() על מנת להגיע למקרה הבסיס, 2, k = נקבל את התוצאה: עלינו להציב כלומר ( ) k T ( ) = 2 T k + k = 2 ( ) = T 1 + log( ) = = C + log( ) = 1 = Θ( log( ))
סיבוכיות הזיכרון של Merge Sort 18 מה לגבי סיבוכיות הזיכרון של?merge sort ובכן, גם במקרה זה ניתן לבצע פיתוח טלסקופי, אולם הגישה מעט שונה. נזכיר שבמקרה של סיבוכיות זיכרון, המקסימאלית הנדרשת לשם ביצוע הפונקציה. אנו מעוניינים בכמות הזיכרון במהלך ריצת הפונקציה הרקורסיבית, כמות הזיכרון התפוס גדלה וקטנה כל העת כאשר אנו נכנסים ויוצאים מקריאות רקורסיביות. מה שעלינו לעשות הוא לזהות את השלב ברקורסיה שבו כמות הזיכרון התפוס היא הגדולה ביותר. כפי שמייד נראה, הפיתוח הטלסקופי עבור סיבוכיות זיכרון משקף את פעולת המקסימיזציה הזו, על ידי החלפת פעולות החיבור שראינו בחישובי סיבוכיות הזמן עם פעולות מקסימום.
סיבוכיות הזיכרון של Merge Sort 19 כל קריאה רקורסיבית דורשת זיכרון להקצאת מערך tmpשגודלו, וכן זיכרון נוסף לצורך ביצוע הקריאות הרקורסיביות. נשים לב שלמרות שישנן שתי קריאות רקורסיביות בפונקצית המיון, ומפנה קודם הראשונה מסתיימת זמנית, אלא אינן מתבצעות בו הן את הזיכרון שהיא תפסה, ורק לאחר מכן השנייה מתחילה. לכן, הזיכרון המקסימאלי שיהיה תפוס במהלך ריצת הפונקציה הוא זה הדרוש לאחסון,tmp ועוד זה שנדרש על ידי הקריאה הרקורסיבית שתופסת יותר זיכרון מבין השתיים שמבוצעות.
סיבוכיות הזיכרון של Merge Sort 20 נסמן את סיבוכיות הזיכרון עבור מערך באורך ב-( S(. לפיכך, אחת מן הקריאות הרקורסיביות דורשת זיכרון בגודל.S(/2) כל S ( ) = + max S, S :S() ( ( ) ( ) ) 2 2 נקבל את הביטוי הבא עבור במקרה שלנו שני הערכים בתוך ה- max שווים, ואנו מקבלים ביטוי פשוט ל-( S(. פיתוח טלסקופי של ביטוי זה נותן (בדקו!): ( ) S( ) = + S =Θ( ) 2
it max2(it a, it b) { if (a>b) retur a; else retur b; ביטוי רקורסיבי: ) it max(it a[], it { T( ) = T( 1) + 1 if ( == 1) retur a[0]; T( 1) =Θ( 1) retur max2( a[-1], max(a, -1) ); פיתוח: ( ) = ( 1) + 1= ( 2) + 1+ 1= = ( 1) + 1+ + 1= ( 1) + =Θ( ) T T T T T
it max(it a[], it ) { if ( == 1) retur a[0]; retur max2(max(a,/2), max(a+(/2), -/2) ); ביטוי רקורסיבי: ( ) ( ) ( 1) = 1 T = 2 T / 2 + 1 T
( ) ( ) ( 1) = 1 T = 2 T / 2 + 1 T ( ) ( ) T( ) ( ) T = 2 T / 2 + 1= 2 2 / 4 + 1 + 1= 2 2 = 2T 2 1 2 2 T 1 2 1 2 + + = 3 2 + + + = 2 3 2 = 2T + 2 + 2+ 1= 3 2 T + + + + + = 2 k k 1 k 2 2 2 2 2 1 k log 1 log i 2 T 1 2 ( ) = + = i= 0 ( log ) 1 2 1 = + = O 2 1 ( )
לסיום: היפוך מחרוזת 24 נכתוב לסיום פונקציה () strflipשמקבלת מחרוזת ומחזירה אותה הפוכה (מהסוף להתחלה), אך מבצעת זאת רקורסיבית. נראה כאן שתי אלטרנטיבות, ונשווה את זמן הריצה שלהן: void strflip1(char *str) { if (*str == 0) retur; strflip1(str+1); while (str[1]!= 0) { swap(str,str+1); str++; T T 2 ( ) = ( 1) + = = Θ( )
היפוך מחרוזת: אופציה שנייה 25 הפתרון הקודם איננו יעיל במיוחד: הוא דורש זמן ריבועי ב-, כאשר אנו יודעים שניתן לעשות אותה הפעולה בזמן ליניארי ב-. הפתרון הבא יעיל יותר, ואולם הוא דורש שהפונקציה תקבל שני מצביעים: אחד לתו הראשון במחרוזת, והשני לתו האחרון בה (הכוונה לתו שלפני ה- ull ): void strflip2(char *begi, char *ed) { if (begi >= ed) retur; swap(begi, ed); strflip2(begi+1, ed-1);
פונקציות מעטפת 26 בעיה קטנה: הפתרון השני אמנם יעיל יותר, אך חתימת הפונקציה שכתבנו שונה מזו שאנו רוצים! ישנה טכניקה סטנדרטית לפתרון סוגיה זו: נכתוב פונקצית מעטפת ש"תעטוף" את הפונקציה הרקורסיבית שכתבנו. הרעיון הוא שהפונקציה שהמשתמש קורא לה בפועל תהיה פונקצית המעטפת, והיא תהייה בדיוק עם החתימה אותה אנו רוצים. במעשה, כל תפקידה של פונקציה זו הוא לקרוא לפונקציה הרקורסיבית, כאשר היא מספקת לה את כל הפרמטרים הנוספים אותם אנו רוצים "להסתיר" מהמשתמש. על מנת שהמשתמש לא יהיה מודע לכל זאת, ניתן לפונקצית המעטפת את השם,strflip2() ואת שם הפונקציה הרקורסיבית שכתבנו נשנה ל-() strflip2_aux.
27 היפוך מחרוזת: האופציה השנייה נקבל את צמד הפונקציות הבאות. שימו לב שלמעשה המשתמש יודע רק על קיומה של הפונקציה הראשונה (פונקצית המעטפת): void strflip2(char *str) { strflip2_aux(str, str + strle(str)-1); void strflip2_aux(char *begi, char *ed) { if (begi >= ed) retur; swap(begi, ed); strflip2_aux(begi+1, ed-1); T ( ) = T ( 2) + C = = Θ( )
סיבוכיות מקום ( 1) מה משפיע על סיבוכיות מקום - רקורסיה מכיוון ובכל זמן נתון ייתכן וכמה עותקים של הפונקציה פתוחים בו זמנית, הרי כמות הזכרון שצריכים היא סך הזכרון שכל עותק צריך מכיוון ואנו מחפשים את המקסימלי נסתכל תמיד על המסלול הכי ארוך בריצה ז"א העומק של הרקורסיה - לא רקורסיה הקצאות זכרון ב- malloc ה גודל שאנו מקצים הוא תלוי קלט, ולכן משתנה מריצה לריצה. מה לא משפיע כל המשתנים שאנו מקצים הם בטוח בגודל קבוע (לא יכול להיות שמספר המשתנים תלוי בגודל הקלט...) ולכן הם תמיד Θ
סיבוכיות זמן ללא רקורסיה הדברים המשפיעים על הסיבוכיות הם לולאות מה שמעניין אותנו עבור כל לולאה הוא כמה זמן לוקח לה לרוץ וצורת החישוב היא כזאת לכל איטרציה יש מחיר ואנו נסכום את המחירים של כל האיטרציות. לכן יש שני דברים שנרצה לבחון כמה איטרציות יש כמה עולה כל איטרציה (שימו לב לא בטוח שכל האיטרציות עולות אותו מחיר...) איטרציות לא מקוננות הבאות אחת אחרי השניה משפיעות כסכום!
דוגמה (בינתיים פשוטה...) usiged it fuca(usiged it ) { usiged it i, j, res=1; for(i = 1; i < /4; i++) { for (j = 1; j < i ; j*=2) { it x=8; res*=x; retur res; חורף תשס"ח מועד ב
חישוב סיבוכיות מקום usiged it fuca(usiged it ) { usiged it i, j, res=1; for(i = 1; i < /4; i++) { retur res; for (j = 1; j < i ; j*=2) { it x=8; אינו x כל res*=x; בינתיים יש לנו מספר קבוע של משתנים ולכן מספר קבוע של זכרון שהפונקציה צריכה... שימו לב מספר העותקים של תלוי בכלל במספר האיטרציות! פעם מוקצה משתנה x חדש, והוא מת מיד כאן
חישוב סיבוכיות זמן usiged it fuca(usiged it ) { usiged it i, j, res=1; for(i = 1; i < /4; i++) { for (j = 1; j < i ; j*=2) { it x=8; res*=x; log(i) retur res; 4 i= 1 איך מתחילים? תמיד מהלולאה הפנימית ביותר! לב כי כל איטרציה בלולאת נשים ה- j עולה לנו (1)Θ מספר האיטרציות הוא log(i). למה? עכשיו אנחנו יודעים שכל איטרציה בלולאת ה- i עולה log(i) - נסכום את זה על פני ה- i האפשריים: log log 1 2 log! log log 4 4 ( i) = = ( ) = ( )
דברים חשובים שכדאי לדעת לפני שמתחילים לפתור שאלה ב... סיבוכיות i= 1 i= 1 i= 1 1 =Θ i i i ( log) חוקי לוגריתם: טורים נוספים: ( + 1)( 2+ 1) = =Θ 6 ( ) 2 3 k =Θ k+ 1 ( ) log ai = log( ai) i i b log a = blog a ( ) ( ) סכום סדרה חשבונית: S = ( + ) a a סכום סדרה הנדסית: S = 1 2 ( ) a q 1 1 q 1