String Compression
נתונה לך מחרוזת s של אותיות באנגלית. דחוס אותה: החלף כל רצף של שתי אותיות זהות או יותר ברצף באות הזאת ואחריה אורך הרצף, והשאר אות שעומדת לבדה כפי שהיא. לכן aabccc הופכת ל־a2bc3. החזר את המחרוזת הדחוסה.
אותיות גדולות וקטנות נחשבות לאותיות שונות: aA הם שני רצפים באורך 1.
פונקציה
- sstring
- המחרוזת לדחיסה, אותיות באנגלית בלבד
- מחזירהstring
- המחרוזת הדחוסה, שבה כל רצף נכתב כאות שלו ואחריה אורכו, כאשר האורך גדול מ־1
אילוצים
1 ≤ s.length ≤ 2 × 104sמכילה רק אותיות באנגלית,aעדzו-AעדZ.- אורך רצף נכתב בשיטה העשרונית ויכול להכיל כמה ספרות.
דוגמאות
- קלט
- s = "aabccc"
- פלט
- "a2bc3"
- הסבר
- הרצפים הם
aa,bו־ccc. הם הופכים ל־a2,b(אות בודדת לא מקבלת ספירה) ול־c3.
- קלט
- s = "abbbbbbbbbbbb"
- פלט
- "ab12"
- הסבר
- אחד
a, ואז שנים-עשרb. האורך 12 נכתב כשתי התווים1ו-2, ולכן התוצאה היאab12.
- קלט
- s = "zzZZZ"
- פלט
- "z2Z3"
- הסבר
- האות הקטנה
zוהאות הגדולהZהן אותיות שונות, לכן זהו רצף של שתיים ואחריו רצף של שלוש:z2Z3.
שאלת המשך
כתבו את הפעולה ההפוכה: בהינתן מחרוזת דחוסה כמו ab12, בנו מחדש את המקור. איך קוראים מספר חזרות שמורכב מכמה ספרות?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
קרא את
sכשורה של בלוקים, כשכל בלוק מורכב מאות אחת שחוזרת על עצמה. אילו שתי עובדות על בלוק צריך לדעת כדי לכתוב אותו?כל בלוק זקוק לאות שלו ולאורכו. שמרו אינדקס אחד במקום שבו הבלוק מתחיל, והזיזו אינדקס שני קדימה כל עוד הוא עדיין רואה את אותה האות; המרחק ביניהם הוא האורך.
השאר את
iבתחילת רצף והזז אתjעד ש-s[j]שונה או שהמחרוזת מסתיימת. כתוב אתs[i], ואז אתj-iכטקסט עשרוני כשהערך גדול מ-1, והעבר אתiל-j. אורך כמו 12 מורכב משתי תווים.
פתרון
מצאו היכן כל רצף של אותיות זהות מתחיל ומסתיים, ואז כתבו את אורכו כטקסט. רצף של אות אחת אינו מקבל ספירה, וספירה יכולה להכיל כמה ספרות. שני אינדקסים, אחד בתחילת הרצף ואחד הסורק עד סופו, מבצעים זאת במעבר אחד.
אסוף את הרצפים, ואז כתוב אותם
האינטואיציה
תחילה תעדו את הרצפים כזוגות של אות ואורך: עבור aabccc, (a, 2), (b, 1), (c, 3). כל אות מאריכה את הרצף האחרון או פותחת רצף חדש. לאחר מכן כתבו את האות של כל רצף, ואחריה את אורכו אם הוא גדול מ-1.
שני המעברים הם O(n). הרשימה יכולה להכיל n רצפים כשאין שתי אותיות שכנות זהות, ולכן נדרש שטח נוסף של O(n).
אלגוריתם
- צרו רשימה ריקה של רצפים.
- עבור כל אות ב־
s: אם הרצף האחרון מכיל את אותה אות, הוסיפו 1 לאורכו; אחרת, הוסיפו רצף חדש באורך 1. - עבור כל רצף, כתבו את האות שלו, ואז את אורכו כטקסט עשרוני אם האורך גדול מ־1.
def compress(s):
# First pass: collect the runs as [letter, length] pairs
runs = []
for ch in s:
if runs and runs[-1][0] == ch:
runs[-1][1] += 1
else:
runs.append([ch, 1])
# Second pass: write each run, with its length when it is more than 1
parts = []
for letter, length in runs:
parts.append(letter)
if length > 1:
parts.append(str(length))
return "".join(parts)שני מצביעים בכל רצף
האינטואיציה
השאר את i באות הראשונה של הרצף הנוכחי והזז את j קדימה כל עוד s[j] שווה ל־s[i]. כשהתנועה של j נעצרת, באות אחרת או בסוף, אורך הרצף הוא j-i. כתוב אותו וקבע i = j. במחרוזת abbbbbbbbbbbb, התנועה של j נעצרת תחילה ב־1, מה שנותן a, ואז ב־13, מה שנותן b12.
אין צורך במקרה מיוחד עבור הרצף האחרון, כי הלולאה הפנימית נעצרת בסוף המחרוזת. j מתקדם רק קדימה, לכן זמן הריצה הוא O(n), ובנוסף לתוצאה נשמרים שני אינדקסים. גרסאות R ו־Swift מפצלות תחילה את s למערך תווים, ולכן הן משתמשות ב־O(n) של מקום נוסף.
אלגוריתם
- הגדר את
i = 0. - כל עוד
iנמצא בתוך המחרוזת, הגדר אתj = iוהזז אתjקדימה כל עוד הוא בתוך המחרוזת ו-s[j]שווה ל-s[i]. - כתוב את
s[i]; אםj-iגדול מ-1, כתוב אותו כטקסט עשרוני. - הגדר את
i = jוחזור על הפעולה.
def compress(s):
parts = []
n = len(s)
i = 0
while i < n:
# Move j to the first index after the run that starts at i
j = i
while j < n and s[j] == s[i]:
j += 1
parts.append(s[i])
if j - i > 1:
parts.append(str(j - i))
i = j
return "".join(parts)
מלכודות ומקרי קצה
- כתיבת רצף רק כשנתקלים באות שונה. אחרי ה-
cccהאחרון שלaabcccלא מופיע דבר, ולכן מחזיריםa2b. - כתיבת הספירה כתו אחד, למשל
'0' + count. זה נכשל ב-10: נדרשים שני התווים12עבור שתים-עשרה אותיותb. - כתיבת
1אחרי אות שמופיעה פעם אחת.bנשארתb, ולאb1. - התעלמות מהבדלי אותיות גדולות וקטנות.
zzZZZהיאz2Z3, ולאz5. - הגדלת מחרוזת בלתי משתנה באמצעות
+, כמו ב-Java או ב-C#. כל+מעתיק את התוצאה עד לאותו שלב, מה שהופך את הלולאה ל-O(n²); יש להשתמש בבונה מחרוזות.
שאלות נפוצות3
מהי סיבוכיות הזמן של דחיסת מחרוזת?
המעבר באמצעות שני מצביעים דורש זמן O(n), מכיוון שאינדקס הסריקה נע רק קדימה. מלבד התוצאה, הוא שומר שני אינדקסים, ולכן המקום הנוסף הוא O(1).
האם המחרוזת הדחוסה יכולה להיות ארוכה יותר מהמחרוזת המקורית?
לא בגרסה הזאת. אות בודדת נשארת תו אחד, ורצף של k ≥ 2 אותיות הופך לאות אחת בתוספת הספרות של k, ולעולם לא ליותר מ־k תווים. שיטה שכותבת ספירה אחרי כל אות, כמו a1b1, עלולה להכפיל את האורך.
איך דוחסים מחרוזת במקום?
השתמשו במערך תווים עם אינדקס קריאה ואינדקס כתיבה. סרקו כל רצף באמצעות אינדקס הקריאה, ואז כתבו את האות ואת הספרות של אורכו באינדקס הכתיבה. הפלט לעולם אינו ארוך יותר מהקלט שנקרא עד כה, ולכן אינדקס הכתיבה לעולם אינו עוקף את אינדקס הקריאה. החזירו את האורך החדש בסוף.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def compress(s):
# כתבו כאן קודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
s = "aabccc"
הפלט הצפוי
"a2bc3"