Two Sum
ניתן לך מערך של מספרים שלמים nums ומספר שלם target. בדיוק שני איברים ב־nums, בשני מיקומים שונים, מסתכמים ל־target. החזר את האינדקסים של שני האיברים האלה, כשהאינדקס הקטן יותר ראשון.
פונקציה
- numsinteger-array
- מערך המספרים השלמים שבו יש לחפש
- targetinteger
- הסכום של שני הערכים חייב להגיע ל־
- מחזירהinteger-array
- שני האינדקסים, כשהקטן יותר מופיע ראשון
אילוצים
2 ≤ nums.length ≤ 104-109 ≤ nums[i] ≤ 109-109 ≤ target ≤ 109- זוג אחד בדיוק של מיקומים שונים מסתכם ב־
target.
דוגמאות
- קלט
- nums = [3, 8, 12, 5]target = 17
- פלט
- [2, 3]
- הסבר
- 12 נמצא באינדקס 2 ו־5 באינדקס 3, ו־12 + 5 = 17. אף זוג אחר לא מגיע ל־17.
- קלט
- nums = [6, 1, 4, 10]target = 7
- פלט
- [0, 1]
- הסבר
- 6 באינדקס 0 ו־1 באינדקס 1 מסתכמים ב־7: הזוג יכול להיות שני האיברים הראשונים.
- קלט
- nums = [2, -6, 9, 4, 13]target = 3
- פלט
- [1, 2]
- הסבר
- -6 באינדקס 1 ו-9 באינדקס 2 מסתכמים ב-3. מספרים שליליים מתחברים באותו אופן כמו מספרים חיוביים.
שאלת המשך
אם nums כבר הגיע ממוין, האם תוכל למצוא את הזוג בזמן O(n) ועם זיכרון נוסף של O(1) בלבד?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
בדיקת כל זוג באמצעות שתי לולאות מקוננות היא נכונה, אבל עבור 10,000 מספרים מדובר בכ-50 מיליון בדיקות. האם תוכל למצוא את בן הזוג של כל מספר בלי לסרוק שוב את הרשימה?
כשאתה עומד על ערך
x, אתה כבר יודע איזה ערך ישלים את הזוג: היעד פחותx. השאלה היחידה היא אם כבר עברת על הערך הזה, ובאיזה אינדקס.עבור על הרשימה פעם אחת ושמור מפת גיבוב של כל ערך שכבר עברת עליו, יחד עם האינדקס שלו. בכל מיקום, חפש קודם את הערך המשלים החסר; אם הוא נמצא במפה, יש לך את שני האינדקסים. אחרת, שמור את הערך הנוכחי והמשך הלאה. חיפוש לפני השמירה מונע ממספר להתאים לעצמו.
פתרון
בדיקה של כל זוג עובדת, אבל כמות העבודה גדלה כריבוע האורך. נהפוך את השאלה: כשאתה נמצא על מספר x, אתה כבר יודע שהמספר המשלים שלו חייב להיות target - x. טבלת גיבוב אומרת לך בזמן קבוע אם כבר עברת על הערך הזה, ולכן מעבר אחד מספיק.
בדוק כל זוג
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
קובעים אינדקס ראשון i ומנסים כל אינדקס j שמימינו עד ש-nums[i] + nums[j] שווה ל-target. התחלת j מ-i + 1 מונעת ממיקום להשתתף בזוג עם עצמו, בודקת כל זוג פעם אחת ומציבה את האינדקס הקטן יותר ראשון. הזוג הרצוי הוא אחד מהזוגות שבודקים, ולכן מוצאים אותו.
n מספרים יוצרים בערך n²/2 זוגות: n = 10^4 פירושו קרוב ל-5 × 10^7 חיבורים.
אלגוריתם
- עבור בלולאה על
iבכל האינדקסים שלnums. - עבור כל
i, עבור בלולאה עלjמ-i + 1ועד לאינדקס האחרון. - אם
nums[i] + nums[j]שווה ל-target, החזר את[i, j]. - אם שתי הלולאות מסתיימות, החזר מערך ריק. מובטח בבעיה שזה לעולם לא יקרה.
def twoSum(nums, target):
n = len(nums)
for i in range(n):
# Only look at partners to the right, so each pair is checked once.
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []מעבר אחד עם מפת גיבוב
האינטואיציה
עבור nums[i] קבוע, הלולאה הפנימית של שיטת הכוח הגס מחפשת ערך אחד שידוע לך מראש: target - nums[i]. הסריקה הזאת היא בעצם חיפוש, ומפת גיבוב שממפה ערכים לאינדקסים מאפשרת לבצע חיפושים בזמן קבוע בממוצע.
חפשו קודם, אחסנו אחר כך. כך מספר יכול להתאים רק למספר שקדם לו, לעולם לא לעצמו, ומכיוון שהמספר התואם תמיד מופיע קודם, האינדקס שלו קודם. כל מספר נבדק פעם אחת, עם פעולות במפה שזמן הריצה שלהן קבוע, ולכן זמן החיפוש הוא O(n); המחיר הוא מפה עם עד n רשומות.
אלגוריתם
- צור מפה ריקה מערך לאינדקס.
- עבור כל אינדקס
i, חשב אתneed, הערך שמשלים את הזוג:targetפחותnums[i]. - אם
needנמצא במפה, החזר את האינדקס השמור שלו ולאחריו אתi. - אחרת, שמור את
nums[i]עם האינדקסiוהמשך.
def twoSum(nums, target):
seen = {}
for i, num in enumerate(nums):
need = target - num
if need in seen:
return [seen[need], i]
seen[num] = i
return []
מלכודות ומקרי קצה
- שמירת מספר לפני חיפוש המספר המשלים שלו. עם
[4, 2, 6]ו-target = 8, המספר 4 מוצא את עצמו ומחזירים[0, 0]. - החזרת האינדקסים בסדר שגוי. כשמחפשים לפני ששומרים, האינדקס מהמפה הוא הקטן יותר והוא מופיע ראשון.
- מיון המערך כדי להשתמש בשני מצביעים, ואז החזרת המיקומים בעותק הממוין. השאירו את האינדקס המקורי של כל ערך לצדו.
- הנחה שהערכים חיוביים או שונים זה מזה. עבור
[0, 7, -3, 0]עםtarget = 0, שני האפסים משלימים זה את זה:[0, 3]. - ב-Lua וב-R, החזרת האינדקסים שמתחילים ב-1. חסרו 1 מכל אחד מהם.
שאלות נפוצות3
מהי סיבוכיות הזמן של Two Sum?
פתרון מפת הגיבוב רץ בזמן O(n), כי הוא מבקר בכל מספר פעם אחת, וכל חיפוש או הוספה אורכים זמן קבוע בממוצע. הוא משתמש ב-O(n) מקום נוסף עבור המפה. בדיקת כל זוג אורכת זמן O(n²) ומשתמשת ב-O(1) מקום.
האם אפשר לפתור את בעיית סכום של שניים בלי להשתמש במקום נוסף?
לא בזמן ליניארי במערך לא ממוין. עם O(1) מקום נוסף, חוזרים לבדוק כל זוג, וזה O(n²). מפת הגיבוב מחליפה זיכרון בגודל O(n) בזמן ליניארי.
איך Two Sum מתמודד עם מספרים כפולים?
כפילויות עובדות כשמחפשים את המספר המשלים לפני ששומרים את המספר הנוכחי. ב-[7, 3, 7] עם target = 14, ה-7 הראשון נשמר באינדקס 0 וה-7 השני מוצא אותו, וכך מתקבל [0, 2]. 7 יחיד לעולם לא יוצר זוג עם עצמו: הוא עדיין לא נמצא במפה כשמחפשים אותו.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def twoSum(nums, target):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
nums = [3, 8, 12, 5] target = 17
הפלט הצפוי
[2, 3]