Valid Parentheses
ניתנת לך מחרוזת s המורכבת רק מתווי הסוגריים (, ), [, ], { ו-}. החזר true כאשר הסוגריים מאוזנים: כל סוגר סוגר את הסוגר הפתוח האחרון, השניים מאותו סוג, ולא נשארים סוגריים פתוחים בסוף. אחרת, החזר false.
פונקציה
- sstring
- מחרוזת הסוגריים לבדיקה
- מחזירהboolean
- האם הסוגריים מאוזנים
אילוצים
1 ≤ s.length ≤ 104sמכילה רק את התווים(,),[,],{ו-}.
דוגמאות
- קלט
- s = "[]{}()"
- פלט
- true
- הסבר
- שלושה זוגות עומדים זה לצד זה, וכל אחד מהם נסגר מיד לאחר שנפתח.
- קלט
- s = "{[()()]}"
- פלט
- true
- הסבר
- שני זוגות הסוגריים העגולים נסגרים בתוך זוג הסוגריים המרובעים, שנסגר בתוך זוג הסוגריים המסולסלים. כל סוגר סוגר תואם לסוגר הפותח האחרון שעדיין פתוח.
- קלט
- s = "{(})"
- פלט
- false
- הסבר
- הסוגר
}מופיע בזמן שהסוגר(שנפתח אחריו עדיין פתוח, ולכן הוא סוגר סוג לא נכון של סוגריים.
שאלת המשך
אם המחרוזת הכילה רק סוגריים עגולים, האם היית יכול לבדוק אותה באמצעות מונה יחיד במקום מחסנית, ובזיכרון נוסף של O(1)?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
קרא את המחרוזת משמאל לימין. כשמגיע סוגר סוגר, איזה סוגר פותח מותר לו לסגור?
היא יכולה לסגור רק את הסוגר הפותח שנפתח לאחרונה ועדיין פתוח. האחרון שנפתח נסגר ראשון: זה בדיוק הסדר שמחסנית שומרת.
דחפו כל סוגר פותח למחסנית. כשמופיע סוגר סוגר, המחסנית חייבת להיות לא ריקה והאיבר שבראשה חייב להיות מאותו סוג; הוציאו אותו והמשיכו. כשהמחרוזת מסתיימת, היא מאוזנת רק אם המחסנית ריקה.
פתרון
ספירת הסוגריים אינה מספיקה: {(}) יש סוגר פותח וסוגר סוגר אחד מכל סוג, ובכל זאת היא שגויה, כי הסדר חשוב. מחסנית שומרת את הסוגריים שעדיין פתוחים לפי סדר פתיחתם, ולכן מעבר אחד בעזרת מחסנית מספיק כדי לקבוע אם המחרוזת כולה תקינה.
הסירי שוב ושוב זוגות תואמים
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
לכל מחרוזת מאוזנת יש זוג פנימי ביותר שאין דבר בין הסוגריים שלו, כמו () בתוך {[()]}. מחיקה של זוג סמוך שתואם זה לזה לעולם אינה משנה אם מחרוזת מאוזנת. לכן חוזרים על המחיקות עד שסבב אחד אינו מסיר דבר: מחרוזת מאוזנת מצטמצמת למחרוזת ריקה, ואילו מחרוזת לא מאוזנת נתקעת עם תווים שנותרו, כמו {(}) כבר מלכתחילה.
בכל סבב סורקים את כל המחרוזת ובונים עותק חדש, ומחרוזת מקוננת כמו (((...))) מאבדת זוג אחד בכל סבב. נדרשים עד n/2 סבבים של עבודה O(n), ובסך הכול O(n²).
אלגוריתם
- מחקו את כל המופעים של
(),[]ו־{}מהמחרוזת. - אם בסבב הזה לא נמחק דבר, עצרו.
- אחרת, חזרו על הפעולה עם המחרוזת הקצרה יותר.
- החזירו
trueאם המחרוזת התרוקנה, ו־falseאם נותרו בה סוגריים כלשהם.
def isValid(s):
while True:
# Delete every adjacent matched pair in one round.
shorter = s.replace("()", "").replace("[]", "").replace("{}", "")
if shorter == s:
break # nothing left to delete
s = shorter
return s == ""מעבר אחד עם מחסנית
האינטואיציה
סוגר חייב לסגור את הסוגר שנפתח אחרון ועדיין פתוח. הסוגר שנפתח אחרון נסגר ראשון, וסדר זה של אחרון נכנס, ראשון יוצא הוא בדיוק מה שמחסנית מקיימת.
דחוף למחסנית את הסוגר הסוגר שכל סוגר פותח מצפה לו. לאחר מכן, הסוגר הסוגר חייב להיות זהה לראש המחסנית. מחסנית ריקה פירושה שאין לה מה לסגור; ראש שונה פירושו שהוא סוגר סוג לא נכון, כפי שקורה עם } בתוך {(}) כאשר ) נמצא בראש. בסוף, כל מה שנותר במחסנית, כמו ב-((, נפתח ולא נסגר מעולם. כל תו נדחף או נשלף פעם אחת: זמן O(n), ולכל היותר n איברים במחסנית.
אלגוריתם
- צור מחסנית ריקה.
- עבור סוגר פותח, דחוף אליה את הסוגר הסוגר התואם.
- עבור סוגר סוגר, החזר
falseאם המחסנית ריקה או אם התו שבראשה שונה; אחרת, בצע שליפה. - אחרי התו האחרון, החזר
trueרק אם המחסנית ריקה.
def isValid(s):
closers = {'(': ')', '[': ']', '{': '}'}
stack = []
for ch in s:
if ch in closers:
stack.append(closers[ch])
elif not stack or stack.pop() != ch:
return False
return not stack
מלכודות ומקרי קצה
- ספירת כל סוג של סוגריים. ל־
{(})יש מספרים תואמים, והוא עדיין לא תקין, ו־)(נכשל רק בגלל הסדר. - הוצאה ממחסנית ריקה. מחרוזת שמתחילה בסוגר סוגר, כגון
}או)]}, חייבת להחזירfalseבמקום לגרום לקריסה. - דילוג על הבדיקה הסופית.
(([{אף פעם לא נתקל בסוגר סוגר שגוי, ולכן רק האיברים שנותרו במחסנית מראים שהוא לא תקין. - הסתמכות על האורך. אורך אי־זוגי תמיד מעיד על קלט לא תקין, אבל אורך זוגי לא מוכיח דבר: גם
(]וגם())(אינם תקינים. - בגישת ההסרה, העברת סוגריים לביטוי רגולרי או לתבנית Lua בלי לבצע להם escaping. ל־
(ול־[יש שם משמעות; השתמשו בהחלפה של טקסט רגיל.
שאלות נפוצות3
מהי סיבוכיות הזמן של Valid Parentheses?
פתרון המחסנית רץ בזמן O(n), מכיוון שכל תו נדחף או נשלף לכל היותר פעם אחת. במקרה הגרוע הוא משתמש במקום O(n), עבור מחרוזת שמכילה רק תווים פותחים. הסרה חוזרת ונשנית של זוגות תואמים אורכת זמן O(n²), מכיוון שמחרוזת מקוננת עמוק מאבדת רק זוג אחד בכל סבב.
למה משתמשים במחסנית עבור סוגריים תקינים?
סוגריים מקוננים זה בתוך זה, ולכן הסוגר שנפתח אחרון חייב להיסגר ראשון. מחסנית שומרת את הסוגר הזה בראש, כך שאפשר לקרוא אותו ולהסיר אותו בזמן קבוע, וכך כל הבדיקה נשארת ב־O(n).
האם מחרוזת ריקה נחשבת לסוגריים תקינים?
כן. במחרוזת ריקה אין סוגר לא תואם, ולכן היא מאוזנת: המחסנית נשארת ריקה, וללולאת ההסרה אין מה להסיר. בבעיה הזו, ב־s יש תמיד לפחות תו אחד.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def isValid(s):
# כתבו כאן קודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
s = "[]{}()"הפלט הצפוי
true