0Pricing
DSA Interview Prep · درس

المحاكاة المتبادلة للمكدس والطابور

نفّذ طابورًا باستخدام مكدسين ومكدسًا باستخدام طابورين، مع شرح التكلفة المُهلكة لكل أسلوب

المحاكاة المتبادلة للمكدس والطابور درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

لماذا نحاكي أحدهما بالآخر؟

يُعدّ تنفيذ طابور باستخدام مكدسين ومكدس باستخدام طابورين من أسئلة مقابلات التصميم الكلاسيكية. تختبر هذه المسائل فهمكم لخصائص الثبات في هياكل البيانات، وقدرتكم على الحفاظ على ضمانات أحد الهياكل أثناء استخدام العمليات الأساسية لهيكل آخر. كما يستخدمها المحاورون مدخلًا لمناقشة التعقيد التراكمي.

الفكرة الأساسية هي أن المكدسات تعمل وفق LIFO والطوابير وفق FIFO. وللتحويل بينهما، يجب عكس الترتيب؛ فعكس مكدس داخل مكدس آخر ينتج ترتيب الإدراج الأصلي، وهو ترتيب FIFO.

تنفيذ طابور باستخدام مكدسين (النهج الكسول)

النهج الكسول: استخدموا مكدس inbox لعمليات الإضافة ومكدس outbox لعمليات الإزالة. عند استدعاء dequeue، إذا كان outbox فارغًا، فانقلوا جميع العناصر من inbox إلى outbox؛ فهذا العكس يعيد ترتيب FIFO. وإذا لم يكن outbox فارغًا، فأزيلوا العنصر منه مباشرةً. تُجرى عمليات النقل عند الحاجة، مما يوزّع تكلفة النقل O(n) على عدد كبير من العمليات.

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def _transfer(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())

    def pop(self):
        self._transfer()
        return self.outbox.pop()

    def peek(self):
        self._transfer()
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.pop())   # 2
q.push(4)
print(q.pop())   # 3

تحليل O(1) التراكمي لطابور مبني من مكدسات

يُنقل كل عنصر من inbox إلى outbox مرة واحدة كحد أقصى. عندما تكون عملية pop من outbox بتعقيد O(1)، ولا تحدث عمليات النقل إلا عندما يكون outbox فارغًا، فإن إجمالي العمل لعدد n من عمليات push وn من عمليات pop لا يتجاوز 2n عملية على المكدس؛ أي O(n) إجمالًا وO(1) تراكميًا لكل عملية. وهذا يعني أن العمليات الفردية قد تكون O(n) في أسوأ الحالات، لكن متوسطها هو O(1).

# Trace transfer costs for 10 push/pop interleaved
class TrackedQueue:
    def __init__(self):
        self.inbox = []; self.outbox = []; self.transfers = 0

    def push(self, x): self.inbox.append(x)

    def pop(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())
                self.transfers += 1
        return self.outbox.pop()

q = TrackedQueue()
for i in range(5):
    q.push(i)
for _ in range(5):
    q.pop()
q.push(10); q.push(20)
q.pop()
print('Total transfer operations:', q.transfers)  # at most n

تنفيذ مكدس باستخدام طابورين (الإزالة الكسولة)

يُعدّ تنفيذ مكدس باستخدام طابورين أقل طبيعية لأن الطوابير تعمل وفق FIFO. في عملية push، أضيفوا العنصر إلى الطابور الرئيسي (O(1)). وفي عمليتي pop أو peek، أزيلوا جميع العناصر باستثناء الأخير إلى الطابور المؤقت، واحفظوا العنصر الأخير، ثم بدّلوا الطابورين. تبلغ تكلفة ذلك O(n) لكل عملية pop، لكنها تبلغ O(1) لكل عملية push.

from collections import deque

class MyStack:
    def __init__(self):
        self.main = deque()
        self.temp = deque()

    def push(self, x):
        self.main.append(x)   # O(1)

    def pop(self):
        # Move all but last element to temp
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main.popleft()   # the 'top'
        self.main, self.temp = self.temp, self.main  # swap
        return val

    def top(self):
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main[0]
        self.temp.append(self.main.popleft())
        self.main, self.temp = self.temp, self.main
        return val

    def empty(self):
        return len(self.main) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.pop())  # 2

تنفيذ مكدس باستخدام طابور واحد (التدوير عند الإضافة)

تنفيذ أنيق باستخدام طابور واحد: عند push، أضيفوا العنصر الجديد إلى الطابور، ثم دوّروا الطابور بحيث يصبح العنصر الجديد في المقدمة. يعني التدوير تنفيذ dequeue ثم إعادة enqueue لجميع العناصر التي كانت موجودة قبل عملية الإضافة. تصبح عمليتا pop وpeek بعد ذلك بتعقيد O(1) (أي تنفيذ dequeue أو peek للمقدمة فقط). أما push فبتعقيد O(n)، وهو عكس المفاضلة في إصدار الطابورين.

from collections import deque

class MyStackOneQueue:
    def __init__(self):
        self.q = deque()

    def push(self, x):
        self.q.append(x)
        # Rotate: move all preceding elements behind x
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):
        return self.q.popleft()

    def top(self):
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStackOneQueue()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2

خلاصة المفاضلات: أي نسخة تختارون؟

بالنسبة إلى طابور مبني من مكدسين: تبلغ تكلفة push مقدار O(1)، وتبلغ تكلفة pop وpeek مقدار O(1) تراكميًا؛ فاختاروه عندما تكون عمليات pop متكررة. وبالنسبة إلى مكدس مبني من طابورين: تبلغ تكلفة push مقدار O(1)، وتبلغ تكلفة pop مقدار O(n)؛ فاختاروه عندما تكون عمليات push أكثر بكثير من عمليات pop. وبالنسبة إلى مكدس مبني من طابور واحد: تبلغ تكلفة push مقدار O(n)، وتبلغ تكلفة pop مقدار O(1)؛ فاختاروه عندما تكون عمليات pop هي الغالبة. اذكروا هذه المفاضلات صراحةً في المقابلة لتثبتوا أنكم تفكرون فيما يتجاوز مجرد «أنه يعمل».

print('Queue from 2 stacks: push O(1), pop O(1) amortised')
print('Stack from 2 queues: push O(1), pop O(n)')
print('Stack from 1 queue:  push O(n), pop O(1)')

لماذا يعيد العكس ترتيب FIFO؟

عند دفع العناصر 1 و2 و3 إلى مكدس (inbox)، تستقر بالترتيب من الأسفل إلى الأعلى: 1 و2 و3. يؤدي إخراجها كلها إلى مكدس ثانٍ (outbox) إلى عكس الترتيب؛ فيكون العنصر 3 في قاع outbox والعنصر 1 في قمته. يؤدي الإخراج من outbox إلى الحصول على 1 ثم 2 ثم 3، وهو ترتيب الإدراج وفق FIFO تمامًا. لذلك تؤدي عمليتا عكس بالضبط (أي مكدسان) إلى استعادة FIFO، بينما ينتج عن استخدام مكدس واحد ترتيب LIFO.

# Demonstrate double-reversal = FIFO
inbox  = [1, 2, 3]   # pushed in this order
outbox = []
while inbox:
    outbox.append(inbox.pop())
print('outbox (one reversal):', outbox)  # [3, 2, 1] top-to-bottom

# Pop from outbox gives FIFO
result = []
while outbox:
    result.append(outbox.pop())
print('dequeued:', result)  # [1, 2, 3] — FIFO!

LeetCode 232: تنفيذ طابور باستخدام مكدسات

تمثل LeetCode 232 مسألة «طابور مبني من مكدسين» مباشرةً. والحل المتوقع هو النقل الكسول إلى outbox. في المقابلة، اذكروا أن كل عنصر ينتقل من inbox إلى outbox مرة واحدة كحد أقصى، مما يجعل جميع العمليات O(1) تراكميًا. واذكروا أن عمليات pop الفردية قد تكون O(n) في أسوأ الحالات، عندما يكون outbox فارغًا، لكن متوسطها عبر n من العمليات هو O(1).

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def pop(self):
        self.peek()             # ensure outbox is populated
        return self.outbox.pop()

    def peek(self):
        if not self.outbox:
            while self.inbox:   # transfer lazily
                self.outbox.append(self.inbox.pop())
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

# Simulation
q = MyQueue()
q.push(1); q.push(2)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.empty()) # False

LeetCode 225: تنفيذ مكدس باستخدام طوابير

تمثل LeetCode 225 مسألة «مكدس مبني من طوابير». ويُعدّ حل التدوير عند push باستخدام طابور واحد أوضح الحلول. بعد دفع العنصر x، دوّروا الطابور بنقل جميع العناصر الموجودة فيه مسبقًا إلى الموضع الذي يلي x. تبلغ تكلفة ذلك O(n) لكل عملية push، لكنه يجعل top وpop بتعقيد O(1). اذكروا هذه المفاضلة وتحققوا من توافقها مع القيود، مثل حمل العمل الذي تكون فيه عمليات push قليلة أو عمليات pop كثيرة.

from collections import deque

class MyStack:
    def __init__(self):
        self.q = deque()

    def push(self, x):       # O(n)
        self.q.append(x)
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):           # O(1)
        return self.q.popleft()

    def top(self):           # O(1)
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2
print(s.empty()) # False

التوسّع إلى ثلاثة مكدسات في مصفوفة واحدة

يتمثل أحد تحديات التصميم ذات الصلة في تنفيذ ثلاثة مكدسات باستخدام مصفوفة واحدة. تتمثل إحدى الطرق في تقسيم المصفوفة إلى ثلاثة أقسام ثابتة ومتساوية. أما الطريقة الأكثر مرونة فتستخدم تخزينًا متشابكًا مع مؤشرات، بحيث ينمو كل مكدس ضمن منطقته، وتُنسخ العناصر عند تصادم الحدود. يختبر هذا الأسلوب إدارة المصفوفات الديناميكية، ويُطرح في مقابلات المطورين ذوي الخبرة. طريقة الأقسام الثابتة أبسط، لكنها تهدر المساحة إذا نمت المكدسات بمعدلات متفاوتة.

class ThreeStacks:
    def __init__(self, size):
        self.data = [0] * (3 * size)
        self.tops = [-1, -1, -1]  # relative top of each stack
        self.size = size

    def push(self, stack_num, val):
        self.tops[stack_num] += 1
        if self.tops[stack_num] >= self.size:
            raise OverflowError('stack full')
        self.data[stack_num * self.size + self.tops[stack_num]] = val

    def pop(self, stack_num):
        if self.tops[stack_num] < 0:
            raise IndexError('stack empty')
        val = self.data[stack_num * self.size + self.tops[stack_num]]
        self.tops[stack_num] -= 1
        return val

ts = ThreeStacks(5)
ts.push(0, 10); ts.push(1, 20); ts.push(2, 30)
print(ts.pop(0), ts.pop(1), ts.pop(2))  # 10 20 30

أهم الاستنتاجات: أنماط المحاكاة

تعلّمكم مسائل المحاكاة المتبادلة مبدأً أوسع: يمكن بناء أي بنية بيانات باستخدام بنية أخرى، متى توفر قدر كافٍ من التخزين المؤقت الوسيط وعكس الترتيب. تعتمد تكلفة المحاكاة على العمليات التي تحسّنونها — يمكنكم دائمًا جعل push بتكلفة O(1) أو جعل pop بتكلفة O(1)، لكن جعل العمليتين كلتيهما بتكلفة O(1) يتطلب التحليل التراكمي أو استخدام عدة بنيات مساعدة.

في المقابلة، اسألوا دائمًا: «ما العمليات الأكثر تكرارًا؟» فهذا يوجّه اختيار صيغة التنفيذ، ويُظهر تفكيرًا متقدمًا في المتطلبات التشغيلية.

اختبار سريع

اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.

مراجعة الدرس

تعلّمتم في هذا الدرس أن الطابور المبني من مكدسين يحقق عملية pop بتكلفة مُهَدرَجة مقدارها O(1)، من خلال نقل العناصر بتكاسل من inbox إلى outbox، وأن المكدس المبني من طابور واحد يحقق عملية pop بتكلفة O(1)، عبر تدوير الطابور مع كل عملية push، بينما تبلغ تكلفة push مقدار O(n)، وأن اختيار العملية التي ستبلغ تكلفتها O(1) يعتمد على نمط الاستخدام. ننتقل بعد ذلك إلى البنية الداخلية لخرائط التجزئة ومعالجة التصادمات.

الأسئلة الشائعة

هل درس «المحاكاة المتبادلة للمكدس والطابور» مجاني؟

نعم — نص درس «المحاكاة المتبادلة للمكدس والطابور» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «المحاكاة المتبادلة للمكدس والطابور»؟

نفّذ طابورًا باستخدام مكدسين ومكدسًا باستخدام طابورين، مع شرح التكلفة المُهلكة لكل أسلوب تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «المحاكاة المتبادلة للمكدس والطابور»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تنفيذ المكدس وتطبيقاته
  2. تنفيذ الطابور وDeque
  3. نمط المكدس الرتيب
  4. المحاكاة المتبادلة للمكدس والطابور
← العودة إلى DSA Interview Prep