Next Permutation
ניתן לך מערך של מספרים שלמים nums, וייתכנו ערכים שחוזרים על עצמם. דמיין את כל הסידורים השונים של הערכים האלה, ממוינים לפי סדר מילוני: השווה בין שני סידורים באינדקס הראשון שבו הם שונים, והסידור שבו הערך קטן יותר מופיע שם קודם. החזר את הסידור שמופיע מיד אחרי nums בסדר הזה.
אם nums הוא כבר הסידור הגדול ביותר, אין סידור אחריו: החזר את הסידור הקטן ביותר, שבו הערכים ממוינים בסדר עולה. שאף לזמן ריצה של O(n) ולזיכרון נוסף של O(1).
פונקציה
- numsinteger-array
- הסידור הנוכחי של הערכים
- מחזירהinteger-array
- הסידור הבא לפי סדר מילוני, או הערכים ממוינים בסדר עולה אם nums הוא הגדול ביותר
אילוצים
1 ≤ nums.length ≤ 50000 ≤ nums[i] ≤ 1000
דוגמאות
- קלט
- nums = [2, 5, 4, 1]
- פלט
- [4, 1, 2, 5]
- הסבר
- הזנב
5, 4, 1יורד, לכן המספר 2 שלפניו חייב לגדול. ערך הזנב הקטן ביותר שגדול מ־2 הוא 4; הצב אותו שם ואת השאר בסדר עולה:[4, 1, 2, 5].
- קלט
- nums = [3, 2, 2, 1]
- פלט
- [1, 2, 2, 3]
- הסבר
- כל המערך יורד, ולכן זהו הסידור הגדול ביותר של 3, 2, 2 ו-1. התשובה חוזרת לקטן ביותר,
[1, 2, 2, 3].
- קלט
- nums = [1, 5, 1]
- פלט
- [5, 1, 1]
- הסבר
- ה־1 הראשון חייב לגדול. ה־1 האחרון שווה לו, ולא גדול ממנו, לכן הערך היחיד שיכול להחליף אותו הוא 5, מה שנותן
[5, 1, 1].
שאלת המשך
איך היית מוצא במקום זאת את הסידור הקודם, זה שמופיע ממש לפני nums בסדר מילוני?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
כדי להגדיל מעט את המערך, שנה אותו ככל האפשר ימינה. מתי אי אפשר להגדיל עוד את הסיומת של המערך על ידי סידור מחדש של הערכים שלה בלבד?
התקדם מימין כל עוד הערכים ממשיכים לעלות. הערך הראשון שקטן מהערך שאחריו הוא נקודת הציר: זהו המיקום הימני ביותר שיכול לגדול. כל מה שאחריו נמצא בירידה.
החליפו את נקודת הציר בערך הימני ביותר שאחריה וגדול ממנה ממש. החלק שאחרי נקודת הציר עדיין בסדר יורד, לכן הפכו את הסדר שלו כדי שיהיה קטן ככל האפשר. אם אין נקודת ציר, הפכו את הסדר של המערך כולו.
פתרון
סיומת יורדת כבר גדולה ככל שערכיה מאפשרים, ולכן הסידור הבא משנה את הערך שלפניה, הציר. הגדל את הציר בכמות הקטנה ביותר האפשרית, ואז הקטן את הסיומת ככל האפשר. לשם כך נדרשות שתי סריקות מימין והיפוך אחד, בתוך המערך.
סידור רשימות לפי סדר
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
בנו סידורים בחיפוש לעומק מתוך הערכים הממוינים, ונסו תמיד קודם את הערך הקטן ביותר שעדיין לא נעשה בו שימוש. כך מתקבלים הסידורים בסדר מילוני, ולכן הראשון שגדול ממש מ־nums הוא התשובה; אם אין כזה, החזירו את הערכים הממוינים. ערכים חוזרים יוצרים חלק מהסידורים פעמיים, כמו ברצף 115, 151, 115, 151, 511, 511 עבור [1, 1, 5], אבל עותק רק חוזר על סידור שנבנה קודם, ולכן הראשון שגדול יותר עדיין יהיה הבא.
הכמות גדלה לפי n!: ל־12 ערכים שונים יש 479,001,600 סידורים.
אלגוריתם
- מיין עותק של
numsלתוךpool. - בנה סידורים בחיפוש לעומק, תוך בחירת איברים שלא נעשה בהם שימוש מתוך
poolמשמאל לימין. - כשסידור הושלם, השווה אותו ל-
numsמיקום אחר מיקום. - החזר את הסידור הראשון שגדול מ-
nums. - אם אין אף סידור גדול יותר, החזר את
pool.
def nextPermutation(nums):
n = len(nums)
pool = sorted(nums) # taking the smallest unused value first gives sorted order
used = [False] * n
current = []
def build():
# True once current holds the first arrangement greater than nums
if len(current) == n:
return current > nums # Python compares lists in dictionary order
for i in range(n):
if not used[i]:
used[i] = True
current.append(pool[i])
if build():
return True
current.pop()
used[i] = False
return False
if build():
return current
return pool # nothing is greater: nums was the largest arrangementמצא את הציר, החלף, הפוך את הסיומת
האינטואיציה
הציר הוא הערך הראשון מימין שקטן מהשכן שלו מימין: 2 בתוך [2, 5, 4, 1]. זהו המיקום הימני ביותר שיכול לגדול, והגדלת המיקום הימני ביותר האפשרי משאירה את העלייה הקטנה ביותר.
החליפו את הציר בערך הקטן ביותר בסיומת שגדול ממנו ממש. הסיומת יורדת, ולכן זהו הערך הימני ביותר שגדול מהציר: 4, והתוצאה היא [4, 5, 2, 1]. הסיומת עדיין יורדת, כי הציר הישן מתאים למקום שבו היה 4. כעת, כשהערך הראשון גדול יותר, הסיומת צריכה להיות קטנה ככל האפשר, בסדר עולה; היפוך רצף יורד משיג זאת: [4, 1, 2, 5].
כשאין ציר, המערך הוא הסידור הגדול ביותר, והיפוך כולו נותן את הקטן ביותר. כל צעד הוא סריקה אחת או היפוך אחד: זמן O(n) וזיכרון O(1), אף שגרסת R מחזירה עותק בגודל O(n).
אלגוריתם
- הגדר את
iל־n-2והזז אותו שמאלה כל עודnums[i] ≥ nums[i + 1]. - אם
i ≥ 0, הגדר אתjל־n-1והזז אותו שמאלה כל עודnums[j] ≤ nums[i], ואז החלף ביןnums[i]ל־nums[j]. - הפוך את החלק מ־
i + 1ועד הסוף באמצעות שני אינדקסים שמתקדמים זה לעבר זה. - החזר את
nums.
def nextPermutation(nums):
n = len(nums)
# 1. the pivot is the last index whose value is smaller than the next one
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
if i >= 0:
# 2. swap it with the rightmost value that is larger than it
j = n - 1
while nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
# 3. the suffix after the pivot is falling: reverse it to make it rising
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
return nums
מלכודות ומקרי קצה
- שימוש ב־
>במקום ב־≥בחיפוש אחר נקודת הציר. ב־[3, 2, 2, 1], הזוג2, 2אינו עלייה, ועצירה שם בוחרת נקודת ציר שאי אפשר להגדיל. - החלפה עם ערך ששווה לנקודת הציר. ב־
[1, 5, 1], החלפת שני הערכים 1 אינה משנה דבר; יש לחפש ערך גדול יותר ממש. - החלפה עם הערך הגדול ביותר בסיומת. ב־
[2, 5, 4, 1], הדבר מציב את 5 בחזית ומדלג על כל הסידורים שמתחילים ב־4. - דילוג על המקרה שבו אין נקודת ציר. יש להחזיר את הסידור הגדול ביותר כשהוא ממוין, באמצעות היפוך של המערך כולו.
- ב־Rust, אינדקס מסוג
usizeשמתחיל ב־n-2גורם ל־panic כאשרn = 1, והתקדמות שמאלה מעבר ל־0 גורמת ל־panic כשאין נקודת ציר. יש להשתמש באינדקס עם סימן או לספור מ־1.
שאלות נפוצות3
מהי סיבוכיות הזמן של התמורה הבאה?
O(n). סריקה אחת מימין מוצאת את נקודת הציר, סריקה שנייה מוצאת את הערך שיש להחליף איתו, והיפוך אחד מתקן את הסיומת. הזיכרון הנוסף הוא O(1), כי הכול מתרחש בתוך המערך.
למה להפוך את הסיומת במקום למיין אותה?
לאחר ההחלפה הסיומת עדיין יורדת, ולכן היפוך שלה כבר ממיין אותה בסדר עולה. היפוך אורך O(n); מיון נותן את אותה תוצאה בזמן O(n log n).
האם התמורה הבאה פועלת עם ערכים כפולים?
כן, כל עוד שתי ההשוואות מתייחסות לערכים שווים כאל ערכים שאינם גדולים יותר. חיפוש הציר מדלג על שכנים שווים באמצעות ≥, וחיפוש ההחלפה מדלג על ערכים השווים לציר באמצעות ≤. לאחר מכן [1, 5, 1] הופך ל־[5, 1, 1], הסידור הבא השונה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def nextPermutation(nums):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
nums = [2, 5, 4, 1]
הפלט הצפוי
[4, 1, 2, 5]