Rotate Array
ניתן לך מערך של מספרים שלמים nums ומספר k. סובבו את המערך ימינה ב־k צעדים: בכל צעד כל ערך זז מקום אחד ימינה, והערך האחרון עובר לתחילת המערך. החזירו את המערך המסובב.
k יכול להיות גדול בהרבה מאורך המערך.
פונקציה
- numsinteger-array
- מערך המספרים השלמים שיש לסובב
- kinteger
- כמה צעדים לסיבוב ימינה
- מחזירהinteger-array
- המערך nums מסובב ימינה ב־k צעדים
אילוצים
1 ≤ nums.length ≤ 5000-104 ≤ nums[i] ≤ 1040 ≤ k ≤ 109
דוגמאות
- קלט
- nums = [4, 9, 1, 6, 3]k = 2
- פלט
- [6, 3, 4, 9, 1]
- הסבר
- שני צעדים ימינה מעבירים את שני הערכים האחרונים, 6 ו-3, אל תחילת המערך. שלושת הערכים האחרים זזים שני מקומות ימינה ושומרים על הסדר שלהם:
[6, 3, 4, 9, 1].
- קלט
- nums = [10, 20, 30]k = 7
- פלט
- [30, 10, 20]
- הסבר
- כל 3 צעדים מחזירים מערך באורך 3 למקום שבו התחיל, לכן 7 צעדים שקולים ל־7 mod 3 = צעד אחד. צעד אחד מעביר את 30 לחזית.
- קלט
- nums = [-5, 8]k = 4
- פלט
- [-5, 8]
- הסבר
- 4 mod 2 = 0, לכן אחרי 4 צעדים כל ערך חוזר למקומו והמערך חוזר ללא שינוי.
שאלת המשך
האם אפשר לבצע סיבוב במקום על ידי העברת כל ערך ישירות אל (i + k) mod n והעברת הערך שהוא דוחק הלאה? כמה מחזורים זה יוצר?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
מערך באורך
nחוזר לסדר ההתחלתי שלו לאחרnצעדים. כמה צעדים באמת חשובים כאשרkהוא 10^9?לאחר הסיבוב, הערכים האחרונים
k mod nיוצרים את תחילת המערך, ושאר הערכים באים אחריהם, כשכל מקטע נשאר בסדר המקורי שלו. לאן מגיע הערך שבאינדקסi?היפוך של כל המערך מציב את הבלוק האחרון בהתחלה, אבל שני הבלוקים יוצאים בסדר הפוך. הפוך כל בלוק פעם נוספת כדי להחזיר את הסדר שלהם. בכל היפוך, שני אינדקסים מתקדמים זה לעבר זה.
פתרון
כל n צעדים מחזירים את המערך לנקודת ההתחלה, לכן תחילה צמצם את k, שעשוי להיות 10^9, ל-k mod n. לאחר מכן כתוב כל ערך ישירות באינדקס הסופי שלו במערך שני, או השג את אותה התוצאה בתוך המערך באמצעות שלוש פעולות היפוך ו-O(1) זיכרון נוסף.
העתק כל ערך לאינדקס החדש שלו
האינטואיציה
אחרי k צעדים, הערך באינדקס i נמצא ב־(i + k) mod n, לכן כתוב אותו שם ישירות בלי לבצע את הצעדים. תחילה צמצם את k ל־k mod n: סיבוב של n צעדים מחזיר כל ערך למקום שבו היה, ולכן כש־k = 7 ו־n = 3, נותר רק צעד אחד.
מעבר אחד מציב את כל n הערכים: זמן O(n), ובנוסף זיכרון O(n) עבור התוצאה. אי אפשר לכתוב לתוך nums עצמו בשיטה הזאת, כי כל כתיבה תדרוס ערך שעדיין לא הזזת.
אלגוריתם
- הגדר את
nכאורך שלnumsוהחלף אתkב־k mod n. - צור מערך תוצאה באורך
n. - עבור כל אינדקס
i, אחסן אתnums[i]ב־result[(i + k) mod n]. - החזר את התוצאה.
def rotate(nums, k):
n = len(nums)
k %= n # a full turn of n steps changes nothing
result = [0] * n
for i in range(n):
result[(i + k) % n] = nums[i]
return resultשלוש היפוכים במקום
האינטואיציה
פצלו את המערך ל־A, שמכיל את n-k הערכים הראשונים, ול־B, שמכיל את k הערכים האחרונים. המערך המסובב הוא B ואחריו A, כשכל מקטע נשאר בסדר המקורי שלו. היפוך של המערך כולו הופך גם את סדר המקטעים וגם את הסדר בתוך כל אחד מהם, כך ש־B מופיע ראשון אך נקרא מהסוף להתחלה, וכך גם A. היפוך חוזר של כל מקטע משחזר את הסדר הפנימי שלו.
עבור [4, 9, 1, 6, 3] ו־k = 2: היפוך של הכול נותן [3, 6, 1, 9, 4], היפוך של שני הערכים הראשונים נותן [6, 3, 1, 9, 4], והיפוך של שלושת הערכים האחרונים נותן [6, 3, 4, 9, 1].
כל ערך מוחלף לכל היותר פעמיים, לכן זמן הריצה הוא O(n), והזיכרון הנוסף הוא כמה משתני אינדקס (O(n) ב־R, שמעתיקה את הווקטור כשפונקציית העזר להיפוך משנה אותו).
אלגוריתם
- הגדר את
nכאורך שלnumsוהחלף אתkב-k mod n. - הפוך את המערך כולו, מהאינדקס 0 עד
n-1. - הפוך את הבלוק הראשון, מהאינדקס 0 עד
k-1. - הפוך את הבלוק השני, מהאינדקס
kעדn-1. - החזר את
nums.
def rotate(nums, k):
n = len(nums)
k %= n # a full turn of n steps changes nothing
def reverse(left, right):
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
reverse(0, n - 1) # the last k values come to the front, backwards
reverse(0, k - 1) # put the front block back in order
reverse(k, n - 1) # and the back block
return nums
מלכודות ומקרי קצה
- שכחת לחשב
k mod n. כאשרk = 10^9, סיבוב צעד אחר צעד לעולם לא יסתיים, והיפוך שלkהערכים הראשונים יחרוג בהרבה מסוף המערך. - חיתוך בלי מודולו. ב-Python, הביטוי
nums[-k:] + nums[:-k]עםk = 7ו-n = 3מחזיר את המערך ללא שינוי, כי שני החיתוכים נעצרים בקצה המערך. - היפוך בסדר שגוי. היפוך של שני המקטעים תחילה ושל המערך כולו בסוף הוא סיבוב שמאלה.
- טעות של אחד במיקום גבול המקטע: המקטע הראשון מסתיים באינדקס
k-1והשני מתחיל ב-k. ב-Lua וב-R, שבהן הספירה מתחילה מ-1, המקטעים הם1..kו-k+1..n.
שאלות נפוצות3
מהי סיבוכיות הזמן של סיבוב מערך?
O(n) גם בגישת ההעתקה וגם בשלוש ההיפוכים, כי כל ערך מועבר מספר קבוע של פעמים. צמצום k באמצעות k mod n תחילה מונע מ־k גדול מאוד להשפיע. סיבוב צעד אחד בכל פעם עולה O(n × k) במקום זאת.
מה קורה כש־k גדול מאורך המערך?
כל n צעדים מחזירים את המערך לסדר ההתחלתי שלו, לכן רק k mod n צעדים חשובים. סיבוב של [10, 20, 30] ב־7 צעדים זהה לסיבוב שלו בצעד אחד, שמניב את [30, 10, 20]. כאשר k הוא כפולה של n, המערך אינו משתנה.
איך מסובבים מערך שמאלה?
סיבוב שמאלה ב־k הוא סיבוב ימינה ב־n - (k mod n). בעזרת היפוכים, הפוך תחילה את k mod n הערכים הראשונים, לאחר מכן את השאר, ואז את כל המערך. בשתי השיטות זמן הריצה נשאר O(n).
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def rotate(nums, k):
# כתבו כאן קודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
nums = [4, 9, 1, 6, 3] k = 2
הפלט הצפוי
[6, 3, 4, 9, 1]