תחי ישראל - אין לנו ארץ אחרת

תחי ישראל -אין לנו ארץ אחרת

stacks, ערימות וחידות

מחבר:
בתאריך:

ערימה stack היא מבנה נתונים שעובד לפי העיקרון של אחרון נכנס ראשון יוצא (Last-In-First-Out) LIFO. משמעות הדבר שהפריט האחרון שנוסף לערימה הוא הראשון שיוסר ממנה.

LIFO in Stack implementation. LIFO = Last-In First-Out. The push and Pop operations are to/from the top of the stack.

אנחנו יכולים להוסיף פריטים רק לראשה של-stack ערימה, ואם אנחנו רוצים להסיר פריטים אנחנו חייבים גם כן להסיר מראש הערימה. אתה יכול לחשוב על ערימת ספרים המונחת על השולחן: אתה יכול להוסיף ספר לראש הערימה, ואם אתה רוצה להסיר ספר אתה יכול להסיר מראש הערימה.

Stacks of book is an analogy to stack implementation in programming

משתמשים ב-stacks ערימות בשביל פונקציות שאפשר להחזיר אחורנית undo/redo בדפדפן או בעורכי טקסט (דוגמת וורד) "undo stack" כאשר כל שינוי למסמך נוסף לראש הערימה, וניתן לחזור אחורנית על ידי כך שמסירים בכל פעם את השינוי האחרון שנמצא בראש הערימה.

The undo stack that softwares like word or Excel use is an example of a stack use case

כשאנחנו מיישמים stack בקוד להוספת פריט לראש הערימה אנחנו קוראים push ולהסרת פריט pop.

LIFO in Stack implementation. LIFO = Last-In First-Out. The push and Pop operations are to/from the top of the stack.

נראה דוגמה לקוד בשביל יישום ערימה stack פשוטה מבוססת רשימה list של פייתון:

# create an empty list to represent the stack
stack = []  

# push some items onto the stack
stack.append('apple')
stack.append('banana')
stack.append('cherry')

# pop an item off the top of the stack
top_item = stack.pop()

print(top_item)  # output: 'cherry'
print(stack)     # output: ['apple', 'banana']
  • יצרנו רשימה ריקה שקראנו לה "stack" והיא מתפקדת בתור הערימה התכנותית stack. בהמשך, הוספנו 3 פריטים לערימה באמצעות המתודה append() (לא באמצעות push? לא. כי אנחנו עובדים עם רשימה ולא עם קוד ייעודי שעושה stack. אבל רגע, תיכף נגיע לזה). לבסוף הסרנו את הפריט אשר בראש הערימה באמצעות המתודה pop() שגם החזירה את הפריט אותו הסרנו. הדפסנו את הפריט שהוסר מראש הערימה כמו גם את חלקה של הערימה שנותר אחרי הסרת הפריט מראש הערימה.

 

יישום של Stack באמצעות קלאס collections.deque

יישום של stack באמצעות list של פייתון אינו אופטימלי בגלל שרשימות של פייתון הם דינמיות, המקום שהם תופסות בזיכרון המחשב משתנה על פי הצורך, ושינויים אילה צורכים משאבי מחשוב.

כדי לפתור את הבעיה, אפשר להשתמש במבנה נתונים ייעודי של פייתון דוגמת הקלאס collections.deque. קלאס זה מיועד מראש לעבוד בתור stack ערימה מה שמאפשר לו להיות יעיל יותר עבור הוספת והסרת פריטים, בלי צורך להקצות זיכרון בתהליך יקר כמו שפייתון מפעיל בשביל רשימות מתארכות.

להלן דוגמה ליישום הקלאס collections.deque לצורך יישום stack:

from collections import deque

stack = deque()

stack.append('apple')
stack.append('banana')
stack.append('cherry')

# pop an item off the top of the stack
top_item = stack.pop()

print(top_item)  # output: 'cherry'
print(stack)     # output: deque(['apple', 'banana'])

בשביל ליישם Stack אנחנו צריכים מספר מתודות לכן נשתמש בקלאס עוטף wrapper class שנקרא לו Stack בתוכו ניצור את המתודות הדרושות לנו שיהיו מבוססות על הקלאס collections.deque

from collections import deque

class Stack:
    def __init__(self):
        self.items = deque()

    def push(self, item):
        self.items.append(item)

    def pop(self):
        if len(self.items) > 0:
            return self.items.pop()
        else:
            return None

    def peek(self):
        if len(self.items) > 0:
            return self.items[-1]
        else:
            return None

    def is_empty(self):
        return len(self.items) == 0

    def size(self):
        return len(self.items)

    def __str__(self):
        return f"{self.items}"

הקלאס Stack שכתבנו כולל את המתודות:

  • push() - מוסיף פריט לראש הערימה
  • pop() - מסיר פריט מראש הערימה ומחזיר אותו. אם הערימה ריקה מחזיר None
  • peek() - מאפשר להציץ ולראות מה הפריט בראש הערימה בלי להסיר אותו
  • size() - מחזיר את מספר הפריטים בערימה
  • is_empty() - מחזיר True אם הערימה ריקה. אחרת מחזיר False.
  • __str__ - מחזיר ייצוג טקסטואלי של האובייקט.

הקוד הבא בוחן את הקלאס Stack:

my_stack = Stack()
my_stack.push(1)
my_stack.push(42)


print(my_stack) # deque([1, 42])
print(my_stack.peek()) # 42
print(my_stack) # deque([1, 42])
print(my_stack.pop()) # 42
print(my_stack) # deque([1])
print(my_stack.size()) # 1
print(my_stack.is_empty()) # False
print(my_stack.pop()) # 1
print(my_stack.pop()) # None
print(my_stack.is_empty()) # True

 

כיצד להשתמש ב-Stack להפיכת סדר התווים במחרוזת?

ניתן להפוך את כיוונם של התווים במחרוזת באמצעות פייתון בדרכים פשוטות. השיטה הפשוטה ביותר משתמשת ב-slicing:

s = "Stacks are fun"
print(s[::-1])
  • דרך זו היא יעילה ביותר מבחינה תכנותית. אפשר להשתמש גם בפונקציה reversed.

אבל במדריך זה אנחנו לומדים על שימוש ב-stack. אז נשתמש במה שלמדנו כדי להפוך מחרוזת ולקבל את התווים שלה בסדר הפוך. אתה מוזמן לנסות בעצמך. זה לא מסובך כל מה שצריך הוא להבין ש-stack מאפשר להפוך את כיוונם של רצפים כי סדר השליפה הוא הפוך מסדר ההוספה לערימה.

עכשיו תנסה בעצמך לפתח קוד פייתון אשר משתמש בקלאס Stack כדי להפוך מחרוזות. תשתדל לעשות את זה מבלי להציץ בפתרון.

 

אם אתה רוצה להיעזר בפתרון הרי הוא לפניך. איך הופכים את כיוונה של מחרוזת על ידי שימוש ב-stack?

def reverse_string(s):
  """
  Reverses a string using a stack.

  Args:
    s: The string to reverse.

  Returns:
    The reversed string.
  """

  # Create a stack to store the characters of the string.
  stack = Stack()

  # Iterate over the characters of the string.
  for c in s:
    # Push the character onto the stack.
    stack.push(c)

  # Iterate over the stack, popping off characters and appending them to a new string.
  reversed_string = ""
  while stack.size():
    reversed_string += stack.pop()

  # Return the reversed string.
  return reversed_string

נבחן את הפונקציה:

print(reverse_string("Python")) # prints "nohtyP"
print(reverse_string("P")) # prints "P"
print(reverse_string("")) # prints
print(reverse_string("100")) # prints "001"

נסביר: הפונקציה "reverse_string" קולטת מחרוזת אותה היא צריכה להחזיר בסדר הפוך. היא משתמשת ב-stacks לאחסון תווי המחרוזת. בשלב ראשון, הפונקציה עוברת על כל תווי המחרוזת על פי הסדר המקורי שלהם, ודוחפת אותם אל תוך הערימה stack. בשלב שני, הערימה מתרוקנת לפי סדר, מראש הערימה ועד לבסיסה. כל פריט שהיא מחזירה הוא תו שמתווסף למחרוזת reversed_string אותה הפונקציה מחזירה.

 

כיצד להשתמש ב-Stack לזיהוי פלינדרומים?

פלינדרום הוא כל רצף - מילה, ביטוי, מספר - שניתן לקרוא אותו מההתחלה לסוף ומהסוף להתחלה ובכל זאת לקבל את אותו דבר. דוגמאות לפלינדרומים בעברית הם: "שמש", "לבלב", "מוּם תַּחַת מוּם", "ילד נתן דלי". באנגלית: "madam", "kayak", "racecar", "civic" או המשפט: "A man, a plan, a canal: Panama". אפשר גם בספרות: 7777 או תאריכים 11/11/11, 2/2/22

אנחנו יכולים להשתמש ב-Stack כדי לזהות האם מחרוזת היא פלינדרום.

זה השלב שבו אני ממליץ לעצור ולנסות לכתוב פונקציה שמזהה פלינדרומים. לחשוב למה אפשר לזהות פלינדרומים על ידי שימוש ב- stack, ולהשתמש בקלאס Stack כדי לפתור את הבעיה. אחרי שאתם מסיים לכתוב את הפתרון תנסה אותו על מחרוזות שונות כאילו שהם פלינדרומים וכאלו שאינם ותראה שאתה מקבל את התוצאה שאתה מצפה לה.

 

אחרי שניסית לפתור לבד אתה מוזמן להציץ בפתרון:

def is_palindrome(s):
  """
  Determines if a string is a palindrome.

  Args:
    s: The string to check.

  Returns:
    True if the string is a palindrome, False otherwise.
  """

  # Create a stack to store the characters of the string.
  stack = Stack()

  # Iterate over the characters of the string.
  for c in s:
    # Push the character onto the stack.
    stack.push(c)

  # Iterate over the stack, popping off characters and appending them to a new string.
  reversed_string = ""
  while not stack.is_empty():
    reversed_string += stack.pop()

  # Return True if the reversed string is equal to the original string, False otherwise.
  return reversed_string == s

נבחן את הפונקציה:

strings = ['madam', 'kayak' , '22/2/22', 'no', 'sir']
for s in strings:
   print(f"'{s}' is {'' if is_palindrome(s) else 'not'} a palindrome")

התוצאה:

'madam' is  a palindrome
'kayak' is a palindrome
'22/2/22' is  a palindrome
'no' is not a palindrome
'sir' is not a palindrome

הפונקציה "is_palindrome" מקבלת מחרוזת כקלט ומחזירה ערך בוליאני. True אם המחרוזת היא פלינדרום, ו-False אם לא. הפונקציה עובדת על ידי יצירת ערימה stack אשר מאחסנת את התווים של המחרוזת. התווים מאוחסנים לפי סדר אחד מעל לשני בערימה, כאשר מגיעים לתו האחרון במחרוזת, הפונקציה מסירה את הפריטים בסדר ההפוך, ומוסיפה כל תו שהיא מסירה למחרוזת חדשה "reversed_string". את המחרוזת ההפוכה היא משווה למחרוזת המקורית. תוצא השוואה זו היא הפלט שהפונקציה מפיקה.

חשוב לציין שדרכים יעילות הרבה יותר לזיהוי פלינדרומים בפייתון משתמשות בפונקציות הפנימיות של השפה שהם תמיד הרבה יותר מהירות:

# Use the ::-1 slice syntax to reverse the string
print(input == input[::-1]) # True

אפשר גם:

# 1. Use the reversed() function
# makes a list out of the string then reverses it
# 2. then join the reversed list back together to a string
print(input == "".join(reversed(input))) # True

 

כיצד להשתמש ב-Stack לזיהוי האם הסוגריים בביטוי הם מאוזנים?

אחת הדרכים בהם עורכי טקסט ו-IDE למיניהם עוזרים למתכנתים היא בכך שהם מתריאים במידה והסוגריים אינם מאוזנים בקוד שאנחנו כותבים. סוגריים מאוזנים משמעותם שכל סוגר פותח יש לו גם סוגר מאותו סוג שסוגר אותו. לדוגמה:

[(a + b) * c]

הוא ביטוי שבו הסוגריים מאוזנים.

לעומת זאת הביטוי:

(a + b) * c)

הוא דוגמה למקרה בו הסוגריים אינם מאוזנים.

המשימה היא לכתוב קוד המבוסס על הקלאס Stack אותו כתבנו לעיל שיצליח לזהות עבור ביטוי שניתן לו כקלט האם הסוגריים בו הם מאוזנים אם לאו.

אתה מוזמן לנסות לבד.

זה הפתרון שאני מציע, אתה מוזמן להציץ בו אחרי שתנסה לפתור לבד:

def is_valid_bracketing(s):
    """Checks if the input string has balanced brackets.

    Args:
        s: The input string to be checked.

    Returns:
        True if the brackets are balanced, False otherwise.
    """
    
    open_brackets = "([{"
    close_brackets = ")]}"

    # Stack to store unclosed opening brackets
    stack = []
    
    # Iterate over each character in the string
    for c in s:
        # If the character is a closing bracket
        if c in close_brackets:
            # Check if there's an opening bracket to match
            if len(stack) == 0:  
                # Unmatched closing bracket
                return False
            # Check if the brackets match
            elif open_brackets.index(stack[-1]) != close_brackets.index(c):  
                # Mismatched bracket forund
                return False
            else:
                # Matching bracket found, remove from stack
                stack.pop()
        # If the character is an opening bracket, push it onto the stack
        elif c in open_brackets:  
            # Only append open brackets
            stack.append(c)
            
    # If the stack is empty, all brackets are matched
    return len(stack) == 0

נבדוק:

# Test cases

str1 = 'a{b}=[c]'
print(str1, is_valid_bracketing(str1))  # Output: True

str2 = 'a*b{(c(}'
print(str2, is_valid_bracketing(str2))  # Output: False

str3 = '[1{(2)}]'
print(str3, is_valid_bracketing(str3))  # Output: True

str4 = 'x[1{)}]'
print(str4, is_valid_bracketing(str4))  # Output: False

str5 = '3=[[4]}'
print(str5, is_valid_bracketing(str5))  # Output: False

str6 = '3={[4-1]}'
print(str6, is_valid_bracketing(str6))  # Output: True

str7 = '{'
print(str7, is_valid_bracketing(str7))  # Output: False

str8 = '}'
print(str8, is_valid_bracketing(str8))  # Output: False

str9 = ''
print(str9, is_valid_bracketing(str9))  # Output: True

נסביר:

ה-Stack, שבמקרה זה הוא רשימה פייתונית, מאחסן את הסוגריים הפותחים. בכל פעם שהפונקציה מזהה סוגר פותח ")]}" היא מוסיפה אותו לראש הערימה. בכל פעם שהפונקציה מזהה סוגר מסיים "([{" הסוגר הפותח המקביל מוסר מראש הערימה. אם הערימה ריקה כאשר הפונקציה מסיימת לעבור על כל הביטוי, המסקנה היא שהסוגריים מאוזנים. אם הערימה stack אינה ריקה אז הסוגריים אינם מאוזנים.

יעילות משך הביצוע O(N) נובעת מזה שהפונקציה עוברת על כל תו במחרוזת שאורכה N פעם אחת, ועל כל תו היא מבצעת פעולות שמשך הביצוע שלהם קבוע O(N) כך שסך כל קומפלקסיות הזמן היא O(N) * O(1) = O(N).

יעילות האחסון היא גם כן O(N) בגלל שבמקרה הכי גרוע, בו המחרוזת מורכבת כולה מסוגריים פותחים, אורך ה-Stack יהיה N.

נוכל להשתמש בפונקציה לפתרון מגוון בעיות, דוגמת:

  • וידוא ביטוי מתמטי
  • פרסינג של מסמכי XML ו-JSON
  • בדיקה של קוד מחשב

 

שימוש ב-stack בשביל להמיר מספר לייצוגו הבינארי

אחד האלגוריתמים המשמש להמרת מספר עשרוני לייצוג הבינארי שלו היא לחלק ב-2, לזכור את השארית ואת מנת החלוקה לחלק שוב ב-2. חוזרים על התהליך עד שמגיעים למנת חלוקה 0. אחרי שמסיימים לחלק, מחברים יחדיו את השאריות למחרוזת המבטאת את הייצוג הבינארי.

לדוגמה, נהפוך את המספר 25 לייצוגו הבינארי.

צעד 1: נחלק את המספר 25 ב-2 ונרשום לעצמנו את מנת החלוקה ואת השארית:

25 ÷ 2 = 12 remainder 1

צעד 2: נחלק את מנת החלוקה מהצעד הקודם ב-2 ונרשום לעצמנו את מנת החלוקה ואת השארית:

12 ÷ 2 = 6 remainder 0

נחזור על 2:

6 ÷ 2 = 3 remainder 0

נחזור על 2:

3 ÷ 2 = 1 remainder 1

נחזור על 2:

1 ÷ 2 = 0 remainder 1

צעד 3: אין לנו אפשרות יותר לחלק כי מנת החלוקה היא 0 אז נחבר את השאריות מהצעדים הקודמים מהסוף להתחלה:

11001

נסכם את האלגוריתם להפיכת מספר לייצוגו הבינארי:

  1. נחלק מספר ב-2 ונרשום את מנת החלוקה quotient ואת השארית remainder (שהיא בהכרח 0 או 1)
  2. נחזור על חלוקה ב-2 עד שנקבל מנת חלוקה שהיא 0.
  3. נקרא את השאריות מהסוף להתחלה ונחבר מהם את הייצוג הבינארי של המספר.

אפשר לעשות ייצוג בינארי באמצעות stack בגלל התכונה שלו שהוא הופך את הפלט שמזינים אליו.

נסה לכתוב פונקציה המשתמשת ב-stack כדי להמיר מספר לייצוג בינארי.

פתרון אחד לתרגיל מופיע להלן (עדיף להשתמש בו רק אחרי שניסית לפתור בעצמך):

from collections import deque

def int_to_bin(num):
    # Validate the input and if smaller than 1 return 0
    if num < 1: 
        return "0"
    
    # Use stack to save the remainders
    stack = deque()

     # Collect the results to the stack
    while num > 0:
        remain = num%2
        num = num//2
        stack.append(remain)

    # Pop the results one-by-one in reverse order
    output = ''
    while stack:
        current_digit = stack.pop()
        output += str(current_digit)

    # Return a string
    return output

נבחן את הפונקציה:

# test cases
test_cases = [
    [0, 0],
    [13, 1101],
    [414568369, 11000101101011100111110110001]
]

print(f"{'Result'.ljust(10)}{'Expected'.ljust(10)}{'Conclusion'}")
print(f"{'-'*10}{'-'*10}{'-'*10}")
for num, expected in test_cases:
    res = int_to_bin(num)
    conclusion = "😄" if str(res) == str(expected) else "😭"
    print(f"{str(res).ljust(10)}{str(expected).ljust(10)}{conclusion}")
print("Finito Programa")

סיבוכיות הזמן והמקום של הפונקציה הם O(N).

 

מדריכים נוספים בסדרה ללימוד פייתון

פתרון בעיית הקיטבג knapsack באמצעות תכנות דינמי

אלגוריתם Selection Sort מיון בחירה

אלגוריתם חיפוש בינארי - מהבנה ליישום

 

לכל המדריכים בסדרה ללימוד פייתון

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

המדריכים באתר עוסקים בנושאי תכנות ופיתוח אישי. הקוד שמוצג משמש להדגמה ולצרכי לימוד. התוכן והקוד המוצגים באתר נבדקו בקפידה ונמצאו תקינים. אבל ייתכן ששימוש במערכות שונות, דוגמת דפדפן או מערכת הפעלה שונה ולאור השינויים הטכנולוגיים התכופים בעולם שבו אנו חיים יגרום לתוצאות שונות מהמצופה. בכל מקרה, אין בעל האתר נושא באחריות לכל שיבוש או שימוש לא אחראי בתכנים הלימודיים באתר.

למרות האמור לעיל, ומתוך רצון טוב, אם נתקלת בקשיים ביישום הקוד באתר מפאת מה שנראה לך כשגיאה או כחוסר עקביות נא להשאיר תגובה עם פירוט הבעיה באזור התגובות בתחתית המדריכים. זה יכול לעזור למשתמשים אחרים שנתקלו באותה בעיה ואם אני רואה שהבעיה עקרונית אני עשוי לערוך התאמה במדריך או להסיר אותו כדי להימנע מהטעיית הציבור.

שימו לב! הסקריפטים במדריכים מיועדים למטרות לימוד בלבד. כשאתם עובדים על הפרויקטים שלכם אתם צריכים להשתמש בספריות וסביבות פיתוח מוכחות, מהירות ובטוחות.

המשתמש באתר צריך להיות מודע לכך שאם וכאשר הוא מפתח קוד בשביל פרויקט הוא חייב לשים לב ולהשתמש בסביבת הפיתוח המתאימה ביותר, הבטוחה ביותר, היעילה ביותר וכמובן שהוא צריך לבדוק את הקוד בהיבטים של יעילות ואבטחה. מי אמר שלהיות מפתח זו עבודה קלה ?

השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.

הוסף תגובה חדשה

 

 

ענה על השאלה הפשוטה הבאה כתנאי להוספת תגובה:

איך אומרים בעברית אינטרנט?