מבני נתונים ואלגוריתמים - מחברת קורס/שיעורי בית 4/תשובות
1
עריכהנניח שמספר האיברים בקלט הוא .
א'
עריכהכל קריאה לInsert-Back
של רשימה מקושרת היא , ולכן סיבוכיות הלולאה היא
.
פתרון זה יעיל הן בזמן הריצה והן בזכרון מבחינת סדרי גדילה, מפני שהן זמן הריצה והן דרישת הזכרון לינאריים במספר האיברים. עם זאת, קבועי זמן הריצה ודרישת הזכרון גבוהים למדי. לדוגמה, בגלל המצביעים בחוליות הרשימה המקושרת, כמות הזכרון שתדרש עבור עבור שמירת כל תו תהיה לפחות פי שלושה מכמות הזכרון המינימלית לשמירת תו.
ב'
עריכהנתבונן באיטרציה ה של הלולאה 3-12. בהכרח תופעל הלולאה הפנימית 9-10, וסיבוכיותה לינארית ב . זמן הריצה הוא לכן .
הפתרון אינו יעיל מבחינת זמן ריצה, אך הוא חסכוני מאד (בצורה קיצונית, אפילו) בדרישת הזכרון שלו.
ג'
עריכהנתבונן באיטרציה ה של הלולאה 3-12. הלולאה הפנימית 9-10 תופעל אמ"ם חזקה של 2, וסיבוכיותה לינארית ב . אם הוא מספר הפעמים שהופעלה הלולאה הפנימית, אז קל לראות שמתקיים .
זמן הריצה הוא לכן .
הפתרון יעיל מבחינת זמן הריצה הכולל, וסביר מבחינת דרישת הזכרון שלו (הנמצאת בין אלו של שני הפתרונות הקודמים). נשים לב שלמרות שזמן הריצה הכולל הוא לינארי (כבפתרון 1), זמן הריצה של איטרציה בודדת אינו (בנגוד לפתרון 1). המשתמש עלול להמתין זמן רב לפני מספר הקשות אם פתרון זה בשימוש.
2
עריכהא'
עריכה# Takes a list (l) and a value (v).
# Modifies all the links in the list so that their value is v.
List-Set-All(l, v)
1 lnk = l.front
2 while lnk != Nil
3 lnk.value = v
4 lnk = lnk.next
ב'
עריכה# Takes two lists (l1, l2).
# Modifies them so that all the links are in the first list (l1),
# and the second list (l2) is empty.
List-Union(l1, l2)
1 if l1.back != Nil
2 l1.back.next = l2.front
3 if l2.front != Nil
4 l2.front.prev = l1.back
5 l1.back = l2.back
6 l1.size = l1.size + l2.size
7 l2.front = l2.back = Nil
8 l2.size = 0
3
עריכהפסוודו-קוד והסבר
עריכהלהלן הפסוודו-קוד לפתרון:
#Takes a singly-linked list (l) and reverses it.
Reverse(l)
1 new-front = Nil
2 while l.front != Nil
3 nxt = l.front.next
4 l.front.next = new-front
5 new-front = l.front
6 l.front = nxt
7 l.front = new-front
נתבונן ברשימה המקושרת בתרשים הבא.
- שורה 1 מייצרת מצביע
new-front
המצביע לNil
(A). מצביע זה יצביע לרשימות החוליות שכבר הפכו את סדרן. - כעת נעבור בלולאה 2-6, כל עוד נותרו איברים אליהם מצביע
l.front
. בכל איטרציה:- שורה 3 מייצרת מצביע
nxt
המצביע לl.front.next
(B). - שורה 4 גורמת ל
l.front.next
להצביע לרשימת החוליות שאליה מצביעnew-front
(C) (בתחילה זו רשימת חוליות ריקה, כמובן). - שורה 5 גורמת ל
new-front
להצביע לl.front
(D) - שורה 6 מקדמת את
l.front
לחוליה הבאה (E). - כלומר, כל איטרציה מעבירה חוליה מתחילת הרשימה לראש רשימת חוליות אליה מצביע
new-front
. (F) מראה זאת בצורה ברורה יותר עבור האיטרציה הראשונה.
- שורה 3 מייצרת מצביע
- לאחר סיום הלולאה, שורה 7 עושה את כל מה שנותר: לגרום ל
l.front
להצביע לחוליה הראשונה בnew-front
.
ניתוח סיבוכיות וזיכרון
עריכהקל לראות שהסיבוכיות היא :
- 1 ו7 הן .
- 3-6 הן ולכן 2-6 (כי ברור שהלולאה מתבצעת פעמים).
כמו כן השתמשנו בכמות מוגבלת של זכרון (השתמשנו בשני משתני עזר בלבד).
4
עריכההמבנה הרקורסיבי של הבעיה
עריכהראשית מספר סימונים:
- כפי שצוין בשאלה, נגדיר את אורך כ , ואת אורך כ .
- נסמן את הרישה ה של מחרוזת כ . במילים אחרות, .
- נגדיר כ את אורך הLCS של ו . במילים אחרות, היא אורך תת-הסדרה הארוכה ביותר המשותפת ל ו .
עכשיו תורכם: וודא שהמך מבין מדוע לפי הגדרה זו, הוא אורך הLCS של ו . |
משפט: מקיים את הנוסחה הבאה:
|
הוכחה: אם או , אז (לפחות) אחת המחרוזות היא המחרוזת הריקה, ורק המחרוזת הריקה יכולה להיות תת-מחרוזת של המחרוזת הריקה. למחרוזת הריקה יש אורך 0, ומכאן החלק הראשון של המשפט.
מכאן נניח ששתי המחרוזות אינן ריקות.
נניח ש . ראשית, קל לראות שבהכרח תווה האחרון של הוא - אם זה לא היה המצב, אז גם היא מחרוזת משותפת ל ו , דבר שנוגד עפ"י ההגדרה את היותה של תת-המחרוזת המשותפת הארוכה ביותר. נחשוב כעת על כל תתי המחרוזות המשותפות מהצורה : עבור כל אחת מהן, קל לראות ש היא תת-מחרוזת משותפת ל ו , ולכן בהכרח הארוכה שבהן היא זו שהרישה שלה היא הLCS של ו .
לחלופין, נניח ש ; יש שלוש אפשרויות:
- מסתיימת ב - אם כן, הLCS בהכרח מהצורה , כאשר היא הLCS של ו .
- מסתיימת ב - אם כן, הLCS בהכרח מהצורה , כאשר היא הLCS של ו .
- אינה מסתיימת ב , וכן אינה מסתיימת ב - אם זה המצב, אז הLCS של ו הוא הLCS של ו (וכן גם הLCS של ו ), כך ששתי הבדיקות הקודמות מכסות כבר (פעמיים אפילו) מקרה זה.
מציאת אורך הLCS
עריכהלהלן פסוודו-קוד למציאת אורך הLCS.
1 L = Make-Matrix(m, n)
2 for i in [1, ..., m]
3 for j in [1, ..., n]
4 L[i][j] = Nil
LCS-Length(i, j)
1 if L[i][j] == Nil
2 if i == 0 or j == 0
3 return 0
4 else if X[i] == Y[j]
5 L[i][j] = 1 + LCS-Length(i - 1, j - 1)
6 else
7 guess-x = LCS-Length(i - 1, j)
8 guess-y = LCS-Length(i, j - 1)
9 L[i][j] = Max(guess-x, guess-y)
10 return L[i][j]
ארבע השורות הראשונות מאתחלות מטריצה המשמשת לmemoization, והפונקציה LCS-Length
מוצאת את את האורך המבוקש. 1-4 מייצרות מטריצה (גלובלית) שכל אחד מאיבריה מאותחל לNil
. 2-9 של LCS-Length
, פועלות בדיוק לפי נוסחת הנסיגה הקודמת (שאת נכונותה כבר הראינו). המטריצה L
, בפרט ב1 של LCS-Length
, מטפלות במצב בו הפונקציה נקראת עם פרמטרים שאתם היא כבר נקראה בעבר.
ניתוח סיבוכיות
עריכהנגדיר את כזמן שאורכת LCS-Length
בהנחה שכל אחת מקריאותיה הרקורסיביות היא . קל לראות ש .
זמן הריצה של LCS-Length(m, n)
חסום מלמעלה על ידי .
חוץ מכך ישנו האתחול 1-4 שאורך גם כן . הזמן הכולל, לכן, הוא .
הדפסת איברי הLCS
עריכהראשית נשנה מעט את הפסוודו-קוד הקודם.
1 L = Make-Matrix(m, n)
2 D = Make-Matrix(m, n)
3 for i in [1, ..., m]
4 for j in [1, ..., n]
5 L[i][j] = Nil
LCS-Length(i, j)
1 if L[i][j] == Nil
2 if i == 0 or j == 0
3 return 0
4 else if X[i] == Y[j]
5 L[i][j] = 1 + LCS-Length(i - 1, j - 1)
6 D[i][j] = 'b'
7 else
8 guess-x = LCS-Length(i - 1, j)
9 guess-y = LCS-Length(i, j - 1)
10 L[i][j] = Max(guess-x, guess-y)
11 if L[i][j] == guess-x
12 D[i][j] = 'x'
13 else
14 D[i][j] = 'y'
15 return L[i][j]
המטריצה הגלובלית D
מתארת בכניסה ה את ההחלטה לגבי הLCS של ו :
'x'
מסמלת את ההחלטה לוותר על התו האחרון ב .'y'
מסמלת את ההחלטה לוותר על התו האחרון ב .'b'
מסמלת את ההחלטה לוותר על התו האחרון בשניהם. זה בדיוק המצב בו נמצא כחלק מהLCS.הפונקציה הבאה מדפיסה את הLCS כאשר קוראים לPrint-LCS(m, n)
:
Print-LCS(i, j)
1 if i > 1 and j > 1
2 if D[i][j] == 'x':
3 Print-LCS(i - 1, j)
4 else if D[i][j] == 'y':
5 Print-LCS(i, j - 1)
6 else
7 Print-LCS(i - 1, j - 1)
1 if D[i][j] == 'b':
2 Print(X[i])
קל לראות שהסיבוכיות עדיין : השינויים בLCS-Length
אינם משנים את סיבוכיותה; כל קריאה לPrint-LCS(i, j)
מורידה (לפחות) את אחד הארגומנטים שלה ב1, ולכן היא יכולה להיקרא לכל היותר פעמים. הסיבוכיות, לכן, היא + = .
5
עריכההמבנה הרקורסיבי
עריכהנגדיר כ את מקסימום הרווח מפיסת בד במידות .
משפט: נתונה על ידי נוסחת הנסיגה הבאה: |
כדאי לדעת: שים לב שהנוסחה הנ"ל כוללת בתוכה תנאי עצירה, מפני שעבור , מה שכתוב כאן הינו פשוט .אפשר גם לכתוב את נוסחת הנסיגה בדרכים אחרות, ובחלקן גם יופיעו תנאי עצירה בצורה מפורשת יותר. |
הוכחה: יש שלוש אפשרויות:
- כדאי לחתוך את פיסת הבד אפקית באיזשהו (כמתואר בתרשים הבא). נשים לב שאפשרות זו רלוונטית רק עבור עבור .
- כדאי לחתוך את פיסת הבד אנכית באיזשהו (כמתואר בתרשים הבא אחריו). נשים לב שאפשרות זו רלוונטית רק עבור עבור .
- כלל לא משתלם לחתוך את פיסת הבד.
בתרשים הבא, לדוגמה, חיתוך אופקי של הצורה בA בשורה ייצור שתי צורות מלבניות בB; לראשונה שבהן יהיו שורות:
בתרשים הבא, לדוגמה, חיתוך אנכי של הצורה בA בעמודה ייצור שתי צורות מלבניות בB; לראשונה שבהן יהיו עמודות:
כעת נדון בכל אחת משלוש האפשרויות:
- כדאי לחתוך את פיסת הבד אפקית באיזשהו . במקרה זה נקבל שתי חתיכות בד, האחת במידות , והשניה במידות . עפ"י ההגדרה, הטוב ביותר שאפשר להשיג עבור הראשונה הוא , ועבור השניה .
- כדאי לחתוך את פיסת הבד אנכית באיזשהו . במקרה זה נקבל שתי חתיכות בד, האחת במידות , והשניה במידות . עפ"י ההגדרה, הטוב ביותר שאפשר להשיג עבור הראשונה הוא , ועבור השניה .
- כלל לא משתלם לחתוך את פיסת הבד: הטוב ביותר האפשרי הוא
Value(i, j)
.
היות שאיננו יודעים איזו משלוש האפשרויות היא הטובה ביותר, אנו לוקחים את המקסימום מבין שלושתן.
מימוש רקורסיבי נאיבי
עריכההפסוודו-קוד הבא מראה מימוש פשוט לנוסחה הנ"ל:
Max-Value(i, j)
1 max-value = 0
# Check horizontal cuts.
# This loop means the C-language loop for(i' = 1; i' < i; ++i')
2 for i' in [1, ..., i - 1]
3 guess = Max-Value(i', j) + Max-Value(i - i', j)
4 if guess > max-value
5 max-value = guess
# Check vertical cuts.
# This loop means the C-language loop for(j' = 1; j' < j; ++j')
6 for j' in [1, ..., j - 1]
7 guess = Max-Value(i, j') + Max-Value(i, j - j')
8 if guess > max-value
9 max-value = guess
# Check the worth of this shape.
19 guess = Value(i, j)
11 if guess > max-value
12 max-value = guess
13 return max-value
שימוש בmemoization
עריכהכרגיל, נוסיף מבנה נתונים פשוט (במקרה זה המטריצה M
) כדי לשמור את הערכים שכבר חישבנו:
1 M = Make-Matrix(m, n)
2 for i in [1, ..., m]
3 for j in [1, ..., n]
4 M[i][j] = Nil
Max-Value(i, j)
1 if M[i, j] != Nil
2 return M[i, j]
4 max-value = 0
# Check horizontal cuts.
5 for i' in [1, ..., i - 1]
6 guess = Max-Value(i', j) + Max-Value(i - i', j)
7 if guess > max-value
8 max-value = guess
# Check vertical cuts.
9 for j' in [1, ..., j - 1]
10 guess = Max-Value(i, j') + Max-Value(i, j - j')
11 if guess > max-value
12 max-value = guess
# Check the worth of this shape.
13 guess = Value(i, j)
14 if guess > max-value
15 max-value = guess
16 M[i, j] = max-value
17 return max-value
הדפסת החלטות החיתוך
עריכהשוב כרגיל, כדי לשמור גם את סדרת הפעולות (ולא רק תוצאתן), נוסיף עוד מבנה נתונים פשוט (במקרה זה המטריצה C
):
1 M = Make-Matrix(m, n)
2 C = Make-Matrix(n, n)
3 for i in [1, ..., m]
4 for j in [1, ..., n]
5 M[i][j] = Nil
6 C[i][j] = Nil
Max-Value(i, j)
1 if M[i, j] != Nil
2 return M[i, j]
3 max-value = 0
# Check horizontal cuts.
4 for i' in [1, ..., i - 1]
5 guess = Max-Value(i', j) + Max-Value(i - i', j)
6 if guess > max-value
7 max-value = guess
8 C[i][j] = ('Horizontal', i')
# Check vertical cuts.
9 for j' in [1, ..., j - 1]
10 guess = Max-Value(i, j') + Max-Value(i, j - j')
11 if guess > max-value
12 max-value = guess
13 C[i][j] = ('Vertical', j')
# Check the worth of this shape.
14 guess = Value(i, j)
15 if guess > max-value
16 max-value = guess
17 C[i][j] = ('Shape', 0)
18 M[i, j] = max-value
19 return max-value
המטריצה C
היא מטריצה של זוגות (בשפת C היינו ממשים זאת בעזרת מטריצה של מבנים):
- האיבר הראשון בכל זוג הוא מחרוזת, המתארת איזו משלוש האפשרויות היא בחרנו.
- האיבר השני בכל זוג הוא מספר המתאר היכן חתכנו.
הנה הקוד המדפיס את סדרת הפעולות:
Print-Cuts(i, j)
1 Print('For the best value of ' + i + ' and ' + j)
2 (type, index) = C[i][j]
3 if type == 'Horizontal'
4 Print 'Cut horizontally at ' + index
5 Print-Cuts(index, j)
6 Print-Cuts(i - index, j)
7 else if type == 'Vertical'
8 Print 'Cut vertically at ' + index
9 Print-Cuts(i, index)
10 Print-Cuts(i, j - index)
11 else
12 Print 'Use the shape itself'
ניתוח סיבוכיות
עריכהנגדיר כ את סיבוכיות Max-Cuts(i, j)
בהנחה שכל קריאה רקורסיבית היא . קל מאד לראות מהקוד ש . נשים לב גם ש , ולכן . סיבוכיות חסומה מלמעלה על ידי
.
גם כאשר לוקחים בחשבון את האתחול והדפסת הפרטים, הפתרון עדיין .
6
עריכההרעיון הכללי
עריכהנממש את התור כך שיכיל שתי מחסניות:
in-stack
תשמש אותנו להכנסת איברים.out-stack
תשמש אותנו להוצאת איברים.
כאשר דוחפים איבר v
(לתור), פשוט דוחפים אותו למחסנית in-stack
שבתור.
דוגמה: כך ייראה התור בתחילה: נניח שלאחר יצירת התור אנו דוחפים את 3 לתוכו. כך ייראה התור: אם נדחוף לתור כעת 5, כך ייראה התור: |
בפעולת Dequeue
, ננסה לבדוק האם אפשר לשלוף מout-stack
. אם כן - סיימנו, אם לא, ראשית נעביר את כל האיבר מin-stack
לout-stack
, ואז נשלוף מתוכו.
דוגמה: נניח שאנו שולפים כעת מהתור. היות ש כעת נשלוף מראש |
עכשיו תורכם: כיצד ייראה התור אם נדחוף אליו את 11? |
נדחוף זאת כרגיל לin-stack
:
עכשיו תורכם: כעת, כיצד ייראה התור אם נשלוף איבר? |
היות שout-stack
אינו ריק, פשוט נשלוף את האיבר הראשון מתוכו (כלומר 5):
פסוודו-קוד
עריכהלהלן מבנה התור (במימוש זה). הוא פשוט מכיל שתי מחסניות.
# A queue (implemented usin-stackg two stacks)
Queue
1 in-stack # A stack to which to Push elements.
2 out-stack # A stack from which to Dequeue elements.
כאשר דוחפים איבר v
(לתור), פשוט דוחפים אותו למחסנית in-stack
שבתור:
Enqueue(q, v)
1 Push(q.in-stack, v)
בפעולת Dequeue
, ננסה לבדוק האם אפשר לשלוף מout-stack
. אם כן - סיימנו, אם לא, ראשית נעביר את כל האיבר מin-stack
לout-stack
, ואז נשלוף מתוכו:
Dequeue(q)
1 if Size(q.out-stack) == 0
2 while Size(q.in-stack) > 0
3 v = Pop(q.in-stack)
4 Push(q.out-stack, v)
5 return Pop(q.out-stack)
קל לממש פעולות אחרות. לדוגמה, אם נניח שהמחסניות תומכות בפעולה Size
(המחזירה את מספר האיברים), ורוצים לממש את פעולת Size
לתור, פשוט נחזיר את סכום הגדלים:
Size(q)
1 return Size(q.in-stack) + Size(q.out-stack)
לחלופין, אם נניח שהמחסניות תומכות בפעולה Empty
(המחזירה האם המחסנית ריקה), ורוצים לממש את פעולת Empty
לתור, פשוט נחזיר האם כל אחת מהמחסניות ריקה:
Empty(q)
1 return Empty(q.in-stack) and Empty(q.out-stack)
עכשיו תורכם: כיצד תממש את פעולת Front, המחזירה את האיבר הראשון המועמד להוצאה (אך אינה משנה את תוכן התור)? |
נממש זאת באופן דומה לזה שבDequeue
של התור, אך נשתמש בTop
של מחסנית, כך:
Front(q)
1 if Size(q.out-stack) == 0
2 while Size(q.in-stack) > 0
3 v = Dequeue(q.in-stack)
4 Push(q.out-stack, v)
5 return Top(q.out-stack)
הוכחת נכונות
עריכה
משפט: נניח שב אז (לפי סדר זה, משמאל לימין), הם בדיוק האיברים האחרונים שהוכנסו לתור וטרם הוצאו (כלומר הוא האיבר המוקדם ביותר שהוכנס וטרם נשלף, ו הוא האיבר המאוחר ביותר שהוכנס וטרם נשלף). |
להלן טענת המשפט באופן גרפי:
הוכחה: נוכיח את הטענה באינדוקציה על מספר הפעולות.
(בסיס האינדוקציה) לפני הפעולה הראשונה, 0 איברים הוכנסו או נשלפו, ושתי המחסניות ריקות. הטענה נכונה באופן טריביאלי.
(מעבר האינדוקציה) נניח שהטענה נכונה לאחר מספר פעולות: כעת בin-stack
יושבים , בout-stack
יושבים , והטענה מתקיימת עד כה. נראה שהטענה מתקיימת לאחר הפעולה הבאה:
- אם הפעולה הבאה היא
Size
(אוEmpty
) אוTop
, שום דבר אינו משתנה, והטענה בוודאי נכונה. - אם הפעולה הבאה היא
Push
של איברu
, אזout-stack
לא ישתנה, ותוכןin-stack
יהפוך להיות . ואכן, לפי הנחת האינדוקציה, (לפי סדר זה, משמאל לימין), הם בדיוק האיברים האחרונים שהוכנסו לתור וטרם הוצאו. תוכל לראות זאת בתרשים בהמשך. - אם הפעולה הבאה היא
Top
, יש שתי אפשרויות:- אם
out-stack
אינה ריקה, אז שום דבר אינו משתנה, והטענה בוודאי נכונה. - אם
in-stack
ריקה, אז לאחר 2-4 בFront
, in-stack
תהיה ריקה, וout-stack
תכיל את האיברים (כלומר האיבר הבא שיישלף). ואכן, לפי הנחת האינדוקציה בדיוק האיברים האחרונים שהוכנסו וטרם הוצאו. תוכל לראות זאת בתרשים בהמשך.
- אם
- אם הפעולה הבאה היא
Dequeue
, הטענה דומה מאד לטענה בDequeue
.
להלן תיאור גרפי של המצב בPush
:
להלן תיאור גרפי של המצב בTop
בו out-stack
ריק בתחילה:
ניתוח סיבוכיות
עריכה
משפט: הסיבוכיות של פעולות הנה . |
הוכחה: מהתבוננות בפונקציות, קל לראות את הדברים הבאים:
- כל אחת מפעולות התור, למעט
Dequeue
(וTop
), הנה . Dequeue
(או לחלופיןTop
) הנה , כאשר הוא מספר הפעולות במחסניתin-stack
בעת הקריאה.
אפשר לראות זאת מכך שהלולאה היחידה היא 1-3 בDequeue
(או לחלופין Top
), וכל אחת מפעולות הרשימה המקושרת היא .
אם מבצעים פעולות, הרי שבכל קריאה לDequeue
יהיו לכל היותר איברים במחסנית. מכאן נקבל שזמן הריצה חסום מלמעלה על ידי
.
אם כי המשפט הקודם נכון, נוכל לטעון טענה חזקה יותר.
משפט: הסיבוכיות של פעולות הנה . |
עכשיו תורכם: וודא שהנך מבין מדוע שני המשפטים אינם סותרים זה את זה. |
ראשית, קל לראות כי הסיבוכיות הנה , משום שאנו מבצעים קריאות לפונקציות, וכל אחת מהן הרי . כל שנותר, לכן, הוא להוכיח חסם . להלן שתי הוכחות לכך.
כדאי לדעת: כל אחת מההוכחות הבאות מסתמכת על צורת ניתוח הידועה בשם amortized analysis. בספר הקורס, הפרק "Amortized Analysis" עוסק בכך. |
להלן ההוכחה הראשונה.
הוכחה: נוכיח את הטענה באינדוקציה על אורך סדרת הפעולות, .
(בסיס האינדוקציה) עבור עלינו להוכיח שסדרת הפעולות (שהיא פעולה יחידה במקרה זה) הנה . ואכן, כשעוברים על הקוד של כל אחת מהפונקציות, ברור שכשהיא הפעולה הראשונה (והיחידה) בסדרת פעולות המתחילה בתור ריק - זמנה הוא .
(מעבר האינדוקציה) נניח שהטענה נכונה לכל קטן (ממש) מ , ונראה שהיא נכונה ל . כפי שהזכרנו, ברור מהתבוננות בקוד שרק לפעולות Dequeue
וFront
תתכן סיבוכיות שאיננה (וגם זאת בהתאם למצב המחסניות) - לכל אחת מהפעולות האחרות בוודאות סיבוכיות .
אם בסדרת הפעולות לא נקראה אף אחת משתי פעולות אלו, ברור שהסיבוכיות היא . אחרת, נגדיר כ את מספר הפעולה האחרונה בסדרת הפעולות שהיתה מסוג Dequeue
או Front
(כל אחת מהפעולות אחריהן יכולה להיות, נניח, Enqueue
, אך לא אחת משתי אלו). סיבוכיות סדרת הפעולות מ1 עד (כולל) היא , וזאת מהנחת האינדוקציה. סיבוכיות שאר הפעולות היא . סך הכל, לכן, נקבל .
ולהלן הוכחה חלופית.
הוכחה: כדי לראות שסך הפעולות הן , נשים לב לעובדה הבאה. כל ערך v
יכול לעבור לכל היותר פעם אחת את המסלול הבא:
Push(q.in-stack, v)
(בקריאה לPush
של התור).Dequeue(q.in-stack)
(בקריאה לDequeue
של התור).Push(q.out-stack, v)
(בקריאה לDequeue
של התור).
לכן כל איבר שנדחף לתור יכול לתרום לכל היותר לסיבוכיות רצף הפעולות. ברור שברצף של פעולות, יידחפו לתור לכל היותר איברים.
7
עריכהשאלה זו ותשובתה דומים מאד לשאלה על מציאת איברים שלמים בתחום. מטרת השאלה היא להבין כיצד לעקוף הבדלים טפלים בין בעיות שונות.
ראשית, נשים לב שבאלגוריתם המקורי, לפונקציות הראשונות "לא אכפת" האם מדובר במספרים שלמים או לא - הן רק צריכות להשוות בין שני איברים, ולא משנה להן האם משווים איברים שלמים או מרחקי איברים מרוכבים מהראשית. נכתוב את הפונקציות מחדש כך שיותאמו לבעיה החדשה:
Less-Than-Dist(Values, v)
1 return Less-Than-Dist'(Values, v, 1, Length(Values))
Less-Than-Dist'(Values, v, left, right)
1 if left > right
2 return right
3 mid = ⌊(left + right) / 2⌋
4 if Dist(v) <= Dist(Values[mid])
5 return Less-Than-Pos'(Values, v, left, mid - 1)
6 if Dist(v) > Dist(Values[mid])
7 return Less-Than-Pos'(Values, v, mid + 1, right)
כעת נותר לטפל בפונקציה האחרונה. פונקציה זו ביצעה שתי קריאות:
Less-Than-Dist(A, b)
- כאן אין הבדל.Less-Than-Dist(A, a + 1)
- מדוע הוספנו דווקא את המספר 1? מהתבוננות בתשובה עולה שכל שהיינו צריכים לעשות הוא להוסיף ל מספר שגודלו הוא לכל היותר ההפרש בין שני איברים שונים במערך. במקרה שמדובר במספרים שלמים, ההפרש בין שני מספרים שונים במערך הוא לפחות 1.
במקרה החדש, נעבור על המערך, ונמצא את - ההפרש הקטן ביותר בין שני מספרים שונים. היות שהמערך ממויין, ברור שנוכל לעשות זאת בזמן לינארי. נסיים את הפתרון בהצגת הקריאה האחרונה:
Find-Num-In-Range(A, a, b)
1 d = Min-Dist-Between-Two-Elements(A) # Finds the smallest distance as was just described.
2 return Less-Than-Dist(A, b) - Less-Than-Dist(A, a + d)