📚 هياكل البيانات في بايثون: فهم وتطبيق الـ Stack (المكدس) خطوة بخطوة

مرحباً بك في عالم هياكل البيانات! 🎯 اليوم سنتعرف على واحدة من أهم وأبسط الهياكل، وهي الستاك (Stack) أو المكدس بالعربية. تخيل أنك تضع مجموعة من الأطباق فوق بعضها البعض، هل يمكنك أخذ الطبق الذي في المنتصف أولاً؟ بالطبع لا! ستأخذ الطبق العلوي أولاً. هذا هو المبدأ الأساسي للستاك.


🔍 ما هو الـ Stack (المكدس)؟

الستاك هو هيكل بيانات خطي (Linear Data Structure) يعمل بمبدأ LIFO، وهي اختصار لـ "Last In, First Out"، أي "آخر من يدخل هو أول من يخرج".

💡 تخيله هكذا:

  • مكدس الكتب: تضيف كتاباً جديداً في الأعلى (يدخل أخيراً)، وعندما تريد كتاباً، تأخذه من الأعلى (يخرج أولاً).
  • مكدس الأطباق: كما ذكرنا سابقاً.
  • زر الرجوع (Undo) في برامج الكتابة: آخر إجراء قمت به هو أول إجراء يمكنك التراجع عنه.

⚙️ العمليات الأساسية على الـ Stack

لعمل الستاك بشكل صحيح، نحتاج إلى ثلاث عمليات رئيسية:

  1. push(item) ➡️ إضافة عنصر: تضيف عنصراً جديداً إلى قمة (أعلى) المكدس.
  2. pop() ⬅️ إزالة عنصر: تزيل العنصر الموجود في قمة المكدس وتعيده.
  3. peek() 👁️ نظرة خاطفة: تُرجع العنصر الموجود في قمة المكدس دون إزالته، فقط لمعرفة ما هو.

💻 طريقتان لتنفيذ الـ Stack في بايثون

هناك طريقتان رئيسيتان لاستخدام الستاك في بايثون: الطريقة البسيطة باستخدام القوائم الجاهزة، والطريقة الاحترافية بالتنفيذ من الصفر.

الطريقة 1: استخدام القوائم المدمجة (الطريقة البسيطة)

بايثون لا تحتوي على هيكل Stack مدمج، ولكن يمكننا بسهولة محاكاته باستخدام القوائم (Lists)! هيا نطبق.

المثال 1: إنشاء ستاك وإضافة عناصر (push)
# إنشاء ستاك فارغ باستخدام قائمة
my_stack = []  # هذا هو ستاكنا

# عملية push: إضافة عناصر إلى قمة الستاك
my_stack.append("كتاب الرياضيات")  # العنصر الأول يصبح في القاع
my_stack.append("كتاب العلوم")
my_stack.append("كتاب التاريخ")   # هذا العنصر الأخير سيكون في القمة

print("الستاك بعد عمليات push:", my_stack)
# المخرجات: ['كتاب الرياضيات', 'كتاب العلوم', 'كتاب التاريخ']

الشرح: استخدمنا الدالة append() للإضافة إلى نهاية القائمة، والتي تمثل قمة الستاك لدينا.

المثال 2: إزالة عنصر من الستاك (pop)
# المتابعة من المثال السابق
print("الستاك الحالي:", my_stack)  # ['كتاب الرياضيات', 'كتاب العلوم', 'كتاب التاريخ']

# عملية pop: إزالة العنصر من القمة
top_item = my_stack.pop()
print("العنصر الذي تمت إزالته (pop):", top_item)  # كتاب التاريخ
print("الستاك بعد عملية pop واحدة:", my_stack)   # ['كتاب الرياضيات', 'كتاب العلوم']

# لنقم بعملية pop أخرى
removed_item = my_stack.pop()
print("تمت إزالة:", removed_item)  # كتاب العلوم
print("الستاك الآن:", my_stack)    # ['كتاب الرياضيات']

الشرح: الدالة pop() تزيل آخر عنصر في القائمة (قمة الستاك) وتعيد قيمته. لاحظ كيف يخرج "كتاب التاريخ" أولاً لأنه دخل أخيراً (مبدأ LIFO).

المثال 3: النظر إلى العنصر العلوي دون إزالته (peek)
# المتابعة من المثال السابق
print("الستاك الحالي:", my_stack)  # ['كتاب الرياضيات']

# عملية peek: رؤية العنصر في القمة دون حذفه
if my_stack:  # نتحقق أولاً أن الستاك ليس فارغاً
    top_element = my_stack[-1]  # الفهرس -1 يشير إلى آخر عنصر (القمة)
    print("العنصر في قمة الستاك (peek) هو:", top_element)  # كتاب الرياضيات
    print("الستاك بعد peek (لم يتغير):", my_stack)  # ['كتاب الرياضيات']
else:
    print("الستاك فارغ!")

الشرح: للقيام بعملية peek، نصل إلى العنصر باستخدام الفهرس -1، والذي يشير دائماً إلى آخر عنصر في القائمة (أعلى الستاك)، دون استخدام pop() حتى لا نحذفه.


الطريقة 2: تنفيذ الـ Stack من الصفـر (الطريقة الاحترافية)

للفهم الأعمق لكيفية عمل الستاك داخلياً، دعنا ننشئ فئة Stack خاصة بنا من البداية.

class Stack:
    def __init__(self):
        """منشئ الفئة - ينشئ ستاك فارغ"""
        self.items = []  # نستخدم قائمة لتخزين العناصر
    
    def push(self, item):
        """إضافة عنصر إلى قمة الستاك"""
        self.items.append(item)  # نضيف العنصر إلى نهاية القائمة
    
    def pop(self):
        """إزالة وإرجاع العنصر من قمة الستاك"""
        if self.is_empty():
            raise Exception("لا يمكن إجراء pop، الستاك فارغ!")  # خطأ إذا كان الستاك فارغاً
        return self.items.pop()  # نزيل آخر عنصر من القائمة
    
    def peek(self):
        """إرجاع العنصر في قمة الستاك دون إزالته"""
        if self.is_empty():
            raise Exception("لا يمكن إجراء peek، الستاك فارغ!")  # خطأ إذا كان الستاك فارغاً
        return self.items[-1]  # نعود بقيمة آخر عنصر دون حذفه
    
    def is_empty(self):
        """التحقق مما إذا كان الستاك فارغاً"""
        return len(self.items) == 0  # نعود بـ True إذا كان الطول يساوي صفر
    
    def size(self):
        """إرجاع عدد العناصر في الستاك"""
        return len(self.items)  # نعود بطول القائمة

# مثال على استخدام الفئة المخصصة
print("=== استخدام الـ Stack المخصص ===")
book_stack = Stack()

# إضافة عناصر إلى الستاك
book_stack.push("رواية 1984")
book_stack.push("قصة الحيوانات")
book_stack.push("كتاب البرمجة")

print("حجم الستاك:", book_stack.size())  # 3
print("العنصر في القمة:", book_stack.peek())  # كتاب البرمجة

# إزالة عنصر
removed_book = book_stack.pop()
print("تمت إزالة:", removed_book)  # كتاب البرمجة
print("العنصر في القمة الآن:", book_stack.peek())  # قصة الحيوانات
print("حجم الستاك بعد pop:", book_stack.size())  # 2

الشرح: هنا قمنا ببناء هيكل الستاك بشكل كامل من الصفر. الفائدة من هذه الطريقة هي الفهم العمق لكيفية عمل الستاك وإمكانية تخصيصه حسب احتياجاتك.


🧪 تطبيقات عملية إضافية للـ Stack

لنرى كيف يمكن استخدام الستاك في مواقف حقيقية:

# مثال 1: محاكاة زر الرجوع (Undo) في برنامج نصي
class TextEditor:
    def __init__(self):
        self.text = ""
        self.undo_stack = Stack()  # ستاك لحفظ الحالات السابقة
    
    def write(self, new_text):
        """إضافة نص جديد وحفظ الحالة السابقة"""
        self.undo_stack.push(self.text)  # حفظ الحالة الحالية قبل التعديل
        self.text += new_text
    
    def undo(self):
        """التراجع عن آخر عملية"""
        if not self.undo_stack.is_empty():
            self.text = self.undo_stack.pop()  # استعادة الحالة السابقة

# اختبار محرر النص
editor = TextEditor()
editor.write("مرحبا")
editor.write(" بالعالم")
print("النص الحالي:", editor.text)  # مرحبا بالعالم

editor.undo()
print("بعد التراجع:", editor.text)  # مرحبا

# مثال 2: التحقق من توازن الأقواس
def check_parentheses(expression):
    """التحقق مما إذا كانت الأقواس متوازنة في التعبير"""
    stack = Stack()
    opening_brackets = "({["
    closing_brackets = ")}]"
    matching = {')': '(', '}': '{', ']': '['}
    
    for char in expression:
        if char in opening_brackets:
            stack.push(char)  # نضيف الأقواس الافتتاحية إلى الستاك
        elif char in closing_brackets:
            if stack.is_empty() or stack.pop() != matching[char]:
                return False  # الأقواس غير متوازنة
    
    return stack.is_empty()  # يجب أن يكون الستاك فارغاً إذا كانت الأقواس متوازنة

# اختبار توازن الأقواس
print("التوازن الصحيح:", check_parentheses("(a + b) * (c - d)"))  # True
print("التوازن الخاطئ:", check_parentheses("((a + b) * (c - d)"))  # False

⚠️ التحقق من الستاك الفارغ

من المهم جداً التحقق من أن الستاك ليس فارغاً قبل إجراء عمليتي pop() أو peek.

# المثال مع القوائم المدمجة
stack_example = []

# محاولة pop من ستاك فارغ ستسبب خطأ
# stack_example.pop()  # سيثير خطأ: IndexError: pop from empty list

# الطريقة الصحيحة: التحقق أولاً
if not stack_example:  # إذا كان الستاك فارغاً
    print("⚠️  تحذير: لا يمكن إجراء pop، الستاك فارغ!")
else:
    item = stack_example.pop()
    print("تمت إزالة:", item)

🧠 ملخص الدرس

  • الستاك (Stack) هو هيكل بيانات يعمل بمبدأ LIFO (آخر من يدخل هو أول من يخرج).
  • العمليات الأساسية هي: push() للإضافة، pop() للإزالة، و peek() للمعاينة.
  • طريقتا التنفيذ: استخدام القوائم الجاهزة أو بناء فئة مخصصة من الصفر.
  • التطبيقات العملية: أزرار الرجوع (Undo)، التحقق من توازن الأقواس، وغيرها.
  • تأكد دائماً من أن الستاك ليس فارغاً قبل استدعاء pop() أو peek().

🚀 ماذا سنتعلم في الدرس القادم؟

الآن وقد أتقنت عمل الستاك الذي يتبع قاعدة "آخر من يدخل أول من يخرج" (LIFO)، حان الوقت للانتقال إلى هيكل بيانات مشهور يقابله تماماً! في الدرس القادم، سنتعرف على الطابور (Queue)، الذي يعمل بمبدأ "أول من يدخل هو أول من يخرج" (FIFO). تخيل طابور الانتظار في السوبرماركت أو طابور الطباعة. استعد لتعلم كيفية إضافة العناصر في نهاية الطابور وإزالتها من مقدمته! 🌟