المحاكاة المتبادلة للمكدس والطابور
نفّذ طابورًا باستخدام مكدسين ومكدسًا باستخدام طابورين، مع شرح التكلفة المُهلكة لكل أسلوب
المحاكاة المتبادلة للمكدس والطابور درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding 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()) # FalseLeetCode 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) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «المحاكاة المتبادلة للمكدس والطابور»؟
نفّذ طابورًا باستخدام مكدسين ومكدسًا باستخدام طابورين، مع شرح التكلفة المُهلكة لكل أسلوب تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «المحاكاة المتبادلة للمكدس والطابور»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- تنفيذ المكدس وتطبيقاته
- تنفيذ الطابور وDeque
- نمط المكدس الرتيب
- المحاكاة المتبادلة للمكدس والطابور