אנליזה של אלגוריתמים

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

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

המונח "ניתוח אלגוריתמים" נטבע על ידי דונלד קונת'.[1] ניתוח אלגוריתמים הוא חלק חשוב של תאוריה רחבה יותר הנקראת תורת הסיבוכיות, אשר מספקת הערכות תאורטיות לכמות המשאבים הדרושים על ידי כל אלגוריתם שפותר בעיה חישובית נתונה. הערכות אלה מספקים תובנה לכיוונים הגיוניות לחיפוש אחר אלגוריתמים יעילים.

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

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

דגמי עלויות[עריכת קוד מקור | עריכה]

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

יש שני מודלי חישוב עליות המשומשים בצורה נרחבת:[2][3][4][5][6]

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

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

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

ניתוח זמן ריצה[עריכת קוד מקור | עריכה]

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

החסרונות של אמות מידה אמפריות[עריכת קוד מקור | עריכה]

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

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

n (גודל רשימה) זמן ריצה של מחשב א'

( ננו שניות)

זמן ריצה של מחשב ב'

( ננו שניות)

16 8 100,000
63 32 150,000
250 125 200,000
1,000 500 250,000

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

n (גודל רשימה) זמן ריצה של מחשב א'


( ננו שניות)

זמן ריצה של מחשב ב'

( ננו שניות)

16 8 100,000
63 32 150,000
250 125 200,000
1,000 500 250,000
... ... ...
1,000,000 500,000 500,000
4,000,000 2,000,000 550,000
16,000,000 8,000,000 600,000
... ... ...
63,072 × 1012 31,536 × 1012 ns,

או 1 שנה

1,375,000 ns,

או 1.375 מילישניות.

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

רמות של קצב גידול[עריכת קוד מקור | עריכה]

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

סימון O גדולה הוא דרך נוחה כדי לבטא את התרחיש הגרוע ביותר עבור אלגוריתם נתון, אף על פי שזה יכול לשמש גם כדי להביע את המקרה הממוצע — לדוגמה, התרחיש הגרוע ביותר עבור quicksort הוא O(n2), אבל זמן הריצה עבור המקרה הממוצע הוא O(n log n).

רמות אימפריות של קצב גידול[עריכת קוד מקור | עריכה]

תחת ההנחה שזמן הביצוע עוקב אחר כלל הכוח,, ניתן למצוא את המקדם [8] על ידי לקיחת מדידות אמפיריות של זמן ריצה על נקודות בעלות גודל בעייתי וחישוב כך . במילים אחרות, זה מודד את השיפוע של קו אמפירי  על log-log plot של זמן הביצוע לעומת גודל הבעיה, בנקודת גודל כלשהי. אם רמת הצמיחה אכן מלווה את כלל הכוח (אזי הקו על log-log-plot אכן קו ישר), הערך האמפירי של a יישאר קבוע בטווחים שונים, ואם לא, הוא ישתנה (ואז הקו הוא קו מעוגל) - אבל זה עדיין יכול לשמש להשוואה של כל שני אלגוריתמים נתונים בהתחשבות בהתנהגות של רמות קצב גידול מקומיות אמפריות. ובהכלה על הטבלה מלעיל:

n (גודל רשימה) זמן הריצה של מחשב A

( ננו שניות)

רמת קצב גידול מקומית

(n^_)

זמן הריצה של מחשב B

( ננו שניות)

רמת קצב גידול מקומית

(n^_)

15 7 100,000
65 32 1.04 150,000 0.28
250 125 1.01 200,000 0.21
1,000 500 1.00 250,000 0.16
... ... ...
1,000,000 500,000 1.00 500,000 0.10
4,000,000 2,000,000 1.00 550,000 0.07
16,000,000 8,000,000 1.00 600,000 0.06
... ... ...

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

הערכת סיבוכיות זמן ריצה[עריכת קוד מקור | עריכה]

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

1 (* get a positive integer from input *)
2 if n > 10
3 write 'This might take a while...'
4 for i = 1 to n
5 for j = 1 to i
6 write i * j
7 write 'Done!'

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

באלגוריתם לעיל, שלבים 1, 2 ו-7 ירוצו פעם אחת בלבד. עבור הערכה של המקרה הגרוע ביותר, יש להניח כי שלב 3 יורץ גם הוא. לפיכך, הסכום הכולל של זמן כדי לריץ את צעדים 1–3 וצעד 7 הוא:

את הלולאות בשלבים 4, 5 ו-6 יותר מסובך להעריך. החיצונית בשלב 4 תרוץ ( n + 1 ) פעמים (שימו לב כי צעד נוסף נדרש כדי לסיים את ללולאה, ולכן 1 + n ולא n ריצות), אשר יצרכו T4(n + 1) זמן. הלולאה הפנימית, לעומת זאת, נשלטת על ידי הערך של i, אשר נע מ-1 עד i. במעבר הראשון דרך הלולאה החיצונית. j. נע מ-1 ל-1: הלולאה הפנימית רצה פעם אחת, אז הרצת הלולאה הפנימית (שלב 6) צורכת T6, והלולאה הפנימית (שלב 5) צורכת 2T5. זמן. במעבר הבא דרך הלולאה החיצונית. j.  נע מ-1 ל-2: לולאה הפנימית רצה פעמיים, אז הרצת הלולאה הפנימית (שלב 6) צורכת 2T6, והלולאה הפנימית (שלב 5) צורכת 3T5 זמן.

בסך הכל, כל הזמן הנדרש כדי להפעיל את הלולאה הפנימית יכול לבוא לידי ביטוי כהתקדמות אריתמתית:

אשר יכול להתווסף כפאקטור כך את הסך כל הזמן הנדרש כדי להפעיל את הלולאה החיצונית ניתן להעריך באופן דומה:

אשר יכול להתווסף  כפאקטור כך

לכן, סה"כ זמן ריצה עבור אלגוריתם זה הוא:

אשר יורד עד כדי

ככלל אצבע, ניתם להניח שהסדר ההגבוהה ביותר בכל פונקציה שולט בקצב הצמיחה, ובכך מגדיר את סדר זמן הריצה. בדוגמה זו, n^2 הוא הסדר הגבוה ביותר, אז ניתן להסיק כי f(n) = O(n2). באופן רשמי, זה יכול להיות מוכח, כדלקמן:

Prove that

Let k be a constant greater than or equal to [T1..T7]

Therefore

גישה יותר אלגנטית לניתוח אלגוריתם זה תהיה להכריז כי [T1..T7] כל אחד שווים יחידת זמן אחת, במערכת של יחידות שנבחרו, כך שיחידה אחת הוא גדול או שווה לזמן בפועל פעמים של שלבים אלה. כלומר שזמן הריצה של האלגוריתם האלגוריתם מחושב כדלקמן:

ניתוח קצב גידול של משאבים אחרים[עריכת קוד מקור | עריכה]

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

while (file still open)
let n = size of file
for every 100,000 kilobytes of increase in file size
double the amount of memory reserved

במקרה הזה, ככל שגודל הקובץ n גדל, הזיכרון נצרך בקצב גידול מעריכי, אשר מסדר O(2n). זה מאוד מהיר ורוב הסיכויים קצת גידול לא ניתן לניהול של שימוש משאבי זיכרון.

רלוונטיות[עריכת קוד מקור | עריכה]

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

גורמים קבועים[עריכת קוד מקור | עריכה]

אנליזה של אלגוריתמים בדרך כלל מתמקדת בביצועים אסימפטוטיים, במיוחד ברמה הבסיסית, אבל ביישומים מעשיים גורמים קבועים הם חשובים, ונתונים בעולם האמיתי תמיד מוגבלים בגודל בפועל. הגבול הוא בדרך כלל בגודל של זיכרון השמיש, אז על מערכות של 32 סיביות 232 = 4  GiB (או יותר אם מזיכרון מקוטע), ובמערכות של-64 סיביות 264 = 16 EiB. לפיכך תחת גודל מוגבל, סדר הגודל (זמן או מקום) יכול להיות מוחלף על ידי גורם קבוע, ובמובן זה כל האלגוריתמים המעשיים הם O(1) עבור קבוע מספיק גדול, או גודל נתונים קטן מספיק.

פרשנות זו היא יעילה בעיקר עבור פונקציות הגדלות לאט מאוד: (בינארית) הלוגריתם החוזר (log*) הוא פחות מ-5 לכל כמות נתונים מעשית (265536 ביטים); (בינארי) log-log (log log n) הוא פחות מ-6 לכמעט כל כמות נתונים מעשית (264 סיביות); ובינארית (log (log n) הוא פחות מ-64 לכמעט כל כמות נתונים מעשית (264 סיביות). אלגוריתם עם מרוכבות שאינה קבועה מורכבות עשוי בכל זאת להיות יותר יעיל מאשר אלגוריתם קבוע על נתונים פרקטיים אם החסם העליות של הזמן הקבוע יוצר תוצאה קבועה גדולה יותר לדוגמר  יכול להיות כל עוד ו .

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

ראו גם[עריכת קוד מקור | עריכה]

לקריאה נוספת[עריכת קוד מקור | עריכה]

  • Sedgewick, Robert (1998). Algorithms in C, Parts 1-4: Fundamentals, Data Structures, Sorting, Searching (מהדורה 3rd). Reading, MA: Addison-Wesley Professional. ISBN 978-0-201-31452-6. 
  • Knuth, Donald. The Art of Computer Programming. Addison-Wesley. 
  • Greene, Daniel A.; Knuth, Donald E. (1982). Mathematics for the Analysis of Algorithms (מהדורה Second). Birkhäuser. ISBN 3-7643-3102-X. 

הערות שוליים[עריכת קוד מקור | עריכה]

  1. ^ Donald Knuth, Recent News
  2. ^ Alfred V. Aho; John E. Hopcroft; Jeffrey D. Ullman (1974). The design and analysis of computer algorithms. Addison-Wesley Pub. Co.. 
  3. ^ Juraj Hromkovič (2004). Theoretical computer science: introduction to Automata, computability, complexity, algorithmics, randomization, communication, and cryptography. Springer. עמ' 177–178. ISBN 978-3-540-14015-3. 
  4. ^ Giorgio Ausiello (1999). Complexity and approximation: combinatorial optimization problems and their approximability properties. Springer. עמ' 3–8. ISBN 978-3-540-65431-5. 
  5. ^ Wegener, Ingo (2005), Complexity theory: exploring the limits of efficient algorithms, Berlin, New York: Springer-Verlag, עמ' 20, ISBN 978-3-540-21045-0 
  6. ^ Robert Endre Tarjan (1983). Data structures and network algorithms. SIAM. עמ' 3–7. ISBN 978-0-89871-187-5. 
  7. ^ Examples of the price of abstraction?, cstheory.stackexchange.com
  8. ^ How To Avoid O-Abuse and Bribes, at the blog "Gödel’s Lost Letter and P=NP" by R. J. Lipton, professor of Computer Science at Georgia Tech, recounting idea by Robert Sedgewick