المكدس الرتيب: تصاعدي مقابل تنازلي
حافظ على مكدس تصاعدي أو تنازلي للإجابة بكفاءة عن استعلامات العنصر الأكبر التالي والعنصر الأصغر السابق في O(n).
المكدس الرتيب: تصاعدي مقابل تنازلي درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ما المكدس الرتيب؟
المكدس الرتيب هو مكدس يحافظ على ترتيب أحادي الاتجاه لعناصره، إما متزايدًا دائمًا من الأسفل إلى الأعلى أو متناقصًا دائمًا. وقبل دفع عنصر جديد إلى المكدس، نزيل جميع العناصر التي تنتهك هذا الثبات الرتيب. وتتيح هذه البنية المقيّدة حلولًا بزمن O(n) لمسائل كانت ستتطلب حلقات متداخلة بزمن O(n²).
وتتمثل الفكرة الأساسية في أن كل عنصر يُدفع إلى المكدس ويُزال منه مرة واحدة على الأكثر، ولذلك يكون العدد الإجمالي للعمليات على امتداد المرور على المصفوفة هو O(n)، وليس O(n²). فعندما نزيل عنصرًا، نكون قد وجدنا الإجابة التي كان ينتظرها.
# Monotonic increasing stack (bottom to top: smallest to largest)
stack = []
for val in [3, 1, 4, 1, 5, 9, 2, 6]:
while stack and stack[-1] > val:
stack.pop() # maintain increasing invariant
stack.append(val)
print('Increasing stack (left-to-right):', stack) # [1, 1, 2, 6]
# Monotonic decreasing stack (bottom to top: largest to smallest)
stack = []
for val in [3, 1, 4, 1, 5, 9, 2, 6]:
while stack and stack[-1] < val:
stack.pop() # maintain decreasing invariant
stack.append(val)
print('Decreasing stack (left-to-right):', stack) # [9, 6]العنصر الأكبر التالي I
تطلب مسألة العنصر الأكبر التالي العثور، لكل عنصر، على أول عنصر أكبر منه إلى يمينه. وتكون الحلقة المزدوجة ذات الزمن O(n²) بطيئة جدًا. أما باستخدام مكدس رتيب متناقص، فنحلها بزمن O(n).
عالجوا العناصر من اليسار إلى اليمين. وقبل دفع العنصر i إلى المكدس، أزيلوا جميع العناصر الأصغر من nums[i] من المكدس، إذ إن nums[i] هو العنصر الأكبر التالي لكل منها. وبعد معالجة جميع العناصر، لا يكون للعناصر المتبقية في المكدس أي عنصر أكبر إلى يمينها، وتكون الإجابة = -1.
def next_greater_element(nums):
n = len(nums)
result = [-1] * n
stack = [] # stores indices; stack values are decreasing
for i in range(n):
# Pop elements smaller than nums[i]
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i] # nums[i] is next greater for idx
stack.append(i)
# Remaining elements in stack have no next greater => keep -1
return result
nums = [2, 1, 2, 4, 3]
print(next_greater_element(nums)) # [4, 2, 4, -1, -1]
nums2 = [1, 3, 2, 4]
print(next_greater_element(nums2)) # [3, 4, 4, -1]العنصر الأكبر التالي: تتبّع الخوارزمية
لنتتبّع [2, 1, 2, 4, 3] خطوةً خطوة. نحافظ على مكدس تنازلي من الفهارس التي لم يُعثر بعد على العنصر الأكبر التالي لها.
- i=0, val=2: المكدس فارغ، أضف 0 إلى المكدس. المكدس: [0]
- i=1, val=1: 1 < nums[0]=2، أضف 1 إلى المكدس. المكدس: [0,1]
- i=2, val=2: أزل 1 (nums[1]=1 < 2)، result[1]=2؛ الآن nums[0]=2 ليس < 2، فأضف 2 إلى المكدس. المكدس: [0,2]
- i=3, val=4: أزل 2 (result[2]=4)، ثم أزل 0 (result[0]=4)، وأضف 3 إلى المكدس. المكدس: [3]
- i=4, val=3: 3 < nums[3]=4، أضف 4 إلى المكدس. المكدس: [3,4]
- النهاية: العنصران في المكدس [3,4] لهما result=-1
def next_greater_trace(nums):
n = len(nums)
result = [-1] * n
stack = []
for i in range(n):
print(f'i={i} val={nums[i]}: stack={[nums[s] for s in stack]}', end=' => ')
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i]
print(f'pop {nums[idx]}, NGE={nums[i]};', end=' ')
stack.append(i)
print(f'push {nums[i]}, stack={[nums[s] for s in stack]}')
print('Result:', result)
return result
next_greater_trace([2, 1, 2, 4, 3])العنصر الأصغر السابق
تجيب المكدسات الرتيبة أيضًا عن استعلامات العنصر الأصغر السابق (PSE): أي أقرب عنصر إلى يسار كل عنصر يكون أصغر منه. بدلًا من إزالة العناصر عند مواجهة عنصر أكبر، نزيل العناصر عند مواجهة عنصر أكبر من أو يساوي العنصر الحالي، ثم نسجل قمة المكدس بوصفها PSE قبل الإضافة.
يتغير أسلوب الإجابة: ما زلنا نعالج العناصر من اليسار إلى اليمين، لكننا نجيب عن الأسئلة قبل الإضافة مباشرةً بدلًا من الإجابة عند إزالة العناصر. تكون قمة المكدس في تلك اللحظة هي أقرب عنصر أصغر إلى اليسار. وإذا كان المكدس فارغًا، فلا يوجد عنصر أصغر إلى اليسار (وتكون الإجابة = -1 أو قيمة مميزة).
def previous_smaller_element(nums):
n = len(nums)
result = [-1] * n
stack = [] # monotonic increasing (values increase bottom to top)
for i in range(n):
# Pop elements >= current (maintain strictly increasing invariant)
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
# Top of stack is previous smaller element (if exists)
if stack:
result[i] = nums[stack[-1]]
stack.append(i)
return result
nums = [4, 5, 2, 10, 8]
print('PSE:', previous_smaller_element(nums)) # [-1, 4, -1, 2, 2]
nums2 = [1, 3, 2, 5, 4]
print('PSE:', previous_smaller_element(nums2)) # [-1, 1, 1, 2, 2]درجات الحرارة اليومية: انتظار الأيام الأدفأ
تطلب مسألة درجات الحرارة اليومية (LeetCode 739)، عند إعطائكم درجات الحرارة اليومية، إعادة مصفوفة يكون كل عنصر فيها هو عدد الأيام حتى ظهور درجة حرارة أعلى. هذا هو نمط العنصر الأكبر التالي نفسه، لكننا نريد عدد الأيام (فرق الفهارس) بدلًا من القيمة الأكبر.
استخدموا مكدسًا تنازليًا رتيبًا من الفهارس. عند العثور على درجة حرارة أعلى عند الفهرس i، أزيلوا جميع الفهارس j من المكدس التي تحقق temps[j] < temps[i]، واضبطوا result[j] = i - j. أما الفهارس المتبقية فلا يوم أدفأ لها في المستقبل (result = 0).
def daily_temperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = [] # indices of unresolved days
for i in range(n):
while stack and temperatures[stack[-1]] < temperatures[i]:
j = stack.pop()
result[j] = i - j # days until warmer
stack.append(i)
return result
temps = [73, 74, 75, 71, 69, 72, 76, 73]
print(daily_temperatures(temps)) # [1, 1, 4, 2, 1, 1, 0, 0]
temps2 = [30, 40, 50, 60]
print(daily_temperatures(temps2)) # [1, 1, 1, 0] (always warmer next day)
temps3 = [30, 60, 90]
print(daily_temperatures(temps3)) # [1, 1, 0]المكدس التصاعدي مقابل التنازلي: متى تستخدم كلًّا منهما
يُعد اختيار اتجاه المكدس الصحيح أمرًا بالغ الأهمية:
- المكدس التنازلي الرتيب (يُزال العنصر عندما يكون current > top): يجيب عن استعلامات العنصر الأكبر التالي والعنصر الأكبر السابق. ويُستخدم في daily-temperatures وlargest-rectangle وtrap-rain-water.
- المكدس التصاعدي الرتيب (يُزال العنصر عندما يكون current < top): يجيب عن استعلامات العنصر الأصغر التالي والعنصر الأصغر السابق. ويُستخدم في إيجاد نطاق أسعار الأسهم وعدد الأشخاص المرئيين في الطابور.
تذكروا: العنصر الذي يسبب الإزالة هو إجابة الاستعلام الخاص بالعنصر المُزال، سواء كان العنصر الأكبر التالي أو العنصر الأصغر التالي، وفقًا للثابت الذي تحافظون عليه.
# Summary: which stack type for which query?
queries = {
'Next Greater Element': 'Decreasing stack (pop when new > top)',
'Next Smaller Element': 'Increasing stack (pop when new < top)',
'Previous Greater Element': 'Decreasing stack (answer = top before push)',
'Previous Smaller Element': 'Increasing stack (answer = top before push)',
}
for query, approach in queries.items():
print(f'{query}:\n => {approach}\n')
# Mnemonic:
# NGE/PGE => decreasing stack (we pop smaller elements, finding their next/prev larger)
# NSE/PSE => increasing stack (we pop larger elements, finding their next/prev smaller)العنصر الأكبر التالي في مصفوفة دائرية
تطلب مسألة العنصر الأكبر التالي II (LeetCode 503)، عند إعطائكم مصفوفة دائرية (مع الالتفاف إلى البداية)، إيجاد العنصر الأكبر التالي. تكمن الحيلة في معالجة المصفوفة مرتين عبر مضاعفة الفهارس: تكرار الفهارس من 0 إلى 2n-1، واستخدام index % n للالتفاف إلى البداية. نضيف إلى المكدس الفهارس من 0 إلى n-1 فقط (في المرور الأول) حتى لا نعدّ العناصر مرتين.
بديلًا عن ذلك، عالجوا المصفوفة في المرور الثاني من دون إضافة فهارس جديدة، واكتفوا بالإزالة. ي处理 هذا الأمر النظر إلى العناصر التالية في المصفوفة الدائرية على نحو صحيح من دون نسخ المصفوفة فعليًا، مع الحفاظ على مساحة O(n).
def next_greater_element_circular(nums):
n = len(nums)
result = [-1] * n
stack = []
for i in range(2 * n):
while stack and nums[stack[-1]] < nums[i % n]:
idx = stack.pop()
result[idx] = nums[i % n]
if i < n:
stack.append(i) # only push real indices (0..n-1)
return result
print(next_greater_element_circular([1, 2, 1])) # [2, -1, 2]
print(next_greater_element_circular([1, 2, 3, 4, 3])) # [2, 3, 4, -1, 4]
print(next_greater_element_circular([5, 4, 3, 2, 1])) # [-1, 5, 5, 5, 5]مشكلة نطاق أسعار الأسهم
تطلب مشكلة نطاق أسعار الأسهم، عند إعطائكم أسعار الأسهم اليومية، حساب نطاق كل يوم — أي عدد الأيام المتتالية السابقة التي كان سعرها أقل من أو يساوي سعر اليوم. هذه في جوهرها مسألة العنصر الأكبر السابق: النطاق هو المسافة من اليوم الحالي إلى أقرب يوم ذي سعر أعلى منه بشكل صارم.
استخدموا مكدسًا تنازليًا رتيبًا. عند معالجة اليوم i، أزيلوا جميع الأيام التي يكون سعرها ≤ current. يكون النطاق i - stack[-1] إذا كان المكدس غير فارغ، أو i + 1 إذا كان فارغًا (أي إن السعر هو الأعلى حتى الآن). ثم أضيفوا i.
def stock_span(prices):
spans = []
stack = [] # indices of prices forming decreasing sequence
for i, price in enumerate(prices):
while stack and prices[stack[-1]] <= price:
stack.pop()
span = i - stack[-1] if stack else i + 1
spans.append(span)
stack.append(i)
return spans
prices = [100, 80, 60, 70, 60, 75, 85]
print('Prices:', prices)
print('Spans: ', stock_span(prices)) # [1, 1, 1, 2, 1, 4, 6]
# Verification for day 5 (price=75): prev higher is day 1 (80), span = 5-1 = 4
# Day 6 (price=85): prev higher is day 0 (100), span = 6-0 = 6المكدس الرتيب للأشخاص المرئيين في الطابور
تطلب مسألة عدد الأشخاص المرئيين في طابور أن يقف الأشخاص في طابور، ولكل منهم طول. يستطيع الشخص i رؤية الشخص j (حيث j > i) إذا كان جميع الأشخاص بينهما أقصر من كليهما. تُستخدم هنا مكدسات تنازلية رتيبة.
عالجوا الأشخاص من اليمين إلى اليسار. حافظوا على مكدس تنازلي من الأطوال. ولكل شخص، احسبوا عدد الأشخاص الذين يستطيع رؤيتهم: أزيلوا جميع الأشخاص الأقصر (يمكن رؤيتهم، لكنهم يُحجبون بعد ذلك)، ثم أضيفوا 1 إذا لم يكن المكدس فارغًا بعد الإزالة (فالشخص الأطول الأول مرئي أيضًا). يعطي هذا تعقيدًا إجماليًا قدره O(n)، لأن كل شخص يُضاف ويُزال مرة واحدة على الأكثر.
def visible_people(heights):
n = len(heights)
result = [0] * n
stack = [] # decreasing monotonic stack (heights)
for i in range(n - 1, -1, -1): # right to left
count = 0
while stack and stack[-1] < heights[i]:
stack.pop()
count += 1 # can see this shorter person
if stack:
count += 1 # can see the first person >= heights[i]
result[i] = count
stack.append(heights[i])
return result
heights = [10, 6, 8, 5, 11, 9]
print('Heights:', heights)
print('Visible:', visible_people(heights)) # [3, 1, 2, 1, 1, 0]ضمان O(n): لماذا يُضاف كل عنصر ويُزال مرة واحدة على الأكثر
يأتي ضمان الزمن O(n) في خوارزميات المكدس الرتيب من حجة بسيطة في التحليل الاستهلاكي: يُضاف كل عنصر إلى المكدس مرة واحدة بالضبط، ويُزال مرة واحدة على الأكثر. ولا يمكن إضافة أي عنصر أو إزالته أكثر من مرة. لذلك، لا يتجاوز العدد الإجمالي لعمليات push + pop خلال الحلقة بأكملها 2n، مما يعطي عملًا إجماليًا قدره O(n)، رغم أن حلقة while المتداخلة قد توحي بأن التعقيد O(n²).
من المهم توضيح هذا التحليل الاستهلاكي في المقابلات. لا تعمل حلقة while عدد n من المرات في كل تكرار، بل تعمل فقط بالقدر اللازم لإزالة العناصر التي كانت تنتظر، وتلك العناصر تختفي نهائيًا بعد إزالتها.
def next_greater_instrumented(nums):
result = [-1] * len(nums)
stack = []
pushes = pops = 0
for i in range(len(nums)):
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i]
pops += 1
stack.append(i)
pushes += 1
print(f'n={len(nums)}, pushes={pushes}, pops={pops}')
print(f'Total operations = {pushes + pops} <= 2n = {2*len(nums)}')
return result
import random
nums = random.sample(range(1000), 100)
next_greater_instrumented(nums)
# Confirm: total operations always <= 2nالتعرّف على مسائل المكدس الرتيب
من المرجح أن تحتاج المسألة إلى مكدس رتيب إذا طلبت العنصر الأكبر أو الأصغر الأقرب، أو نطاق الأسعار، أو العناصر المرئية في صف، أو المساحات المعتمدة على المدرجات البيانية. ابحثوا عن هذه الكلمات والأنماط: يحتاج كل عنصر إلى الإجابة من أقرب عنصر مناسب في اتجاه واحد (إلى اليسار أو اليمين).
إذا كان الحل بالقوة الغاشمة يمسح إلى اليسار أو اليمين انطلاقًا من كل عنصر (O(n²))، فاستبدلوا هذا المسح بمكدس رتيب. يحتفظ المكدس بمرشحي الإجابة، ويتخلص من المرشحين غير المهمين، ويزيل الإجابة الصحيحة في اللحظة التي نحتاج إليها فيها تمامًا.
# Monotonic stack problem recognition guide
patterns = [
('Next/previous greater element', 'Decreasing stack; answer found on pop'),
('Next/previous smaller element', 'Increasing stack; answer found on pop'),
('Days until warmer/colder', 'Stack of indices; answer = i - j'),
('Stock span', 'Decreasing stack; span = i - prev larger idx'),
('Largest rectangle in histogram', 'Increasing stack; area computed on pop'),
('Trapping rain water', 'Decreasing stack or two-pointer'),
('Sliding window maximum', 'Decreasing deque of indices'),
]
print('Monotonic Stack / Deque Pattern Guide:')
print('='*60)
for problem, approach in patterns:
print(f'Problem: {problem}')
print(f' Approach: {approach}')
print()تحقق سريع
اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep في هذا الدرس.
مراجعة الدرس
تعلمتم في هذا الدرس أن المكدس الرتيب يحافظ على ترتيب تصاعدي أو تنازلي عبر إزالة العناصر التي تنتهك الثابت قبل الإضافة، وأن المكدس التنازلي يجيب عن العنصر الأكبر التالي أو السابق، بينما يجيب المكدس التصاعدي عن العنصر الأصغر التالي أو السابق، وأن كل عنصر يُضاف ويُزال مرة واحدة على الأكثر، مما يعطي زمنًا إجماليًا قدره O(n)، وليس O(n²). في الخطوة التالية، سنطبق المكدس الرتيب لإيجاد أكبر مستطيل في مدرج بياني.
الأسئلة الشائعة
هل درس «المكدس الرتيب: تصاعدي مقابل تنازلي» مجاني؟
نعم — نص درس «المكدس الرتيب: تصاعدي مقابل تنازلي» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «المكدس الرتيب: تصاعدي مقابل تنازلي»؟
حافظ على مكدس تصاعدي أو تنازلي للإجابة بكفاءة عن استعلامات العنصر الأكبر التالي والعنصر الأصغر السابق في O(n). تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «المكدس الرتيب: تصاعدي مقابل تنازلي»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- المكدس الرتيب: تصاعدي مقابل تنازلي
- أكبر مستطيل في المدرج التكراري
- أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب
- احتجاز مياه الأمطار: المكدس والمؤشران