Faragiri

همهٔ زبان‌ها

آموزش پشته و صفساختمان داده

انبار پالت و صف نانوایی، دو ظرف با یک سطر تفاوت. همان یک سطر تعیین می‌کند برداشتن از سر، چهار میلی‌ثانیه باشد یا هشتصد.

۴بخش
۳دقیقه خواندن
۴عمق

دو فهرست، یک تفاوت

قلم بعدی از کدام سر بیرون می‌آید

همه سطرهای این جدول برای دو ساختار یکسان‌اند جز یکی، و همان یک سطر هم رفتار را تعیین می‌کند و هم هزینه را.

پشتهآخرین ورودی، اولین خروجی
صفاولین ورودی، اولین خروجی
اضافه می‌کند بهتهته
برمی‌دارد ازتهسر
جواب می‌دهد بهپیش از این داشتم چه می‌کردمچه کسی بیشتر منتظر مانده
ساخته می‌شود روییک فهرست، و چیز دیگری لازم نیستdeque، چون فهرست سرش کند است
صد هزار برداشتنهر جور باشد ارزان۰٫۸۲ ثانیه روی فهرست، ۰٫۰۰۴۳ روی deque

هیچ طرفی علامت نخورده، چون هیچ‌کدام بهتر نیست. جواب سوم هرم است و در هیچ سطری از این جدول نیست: کوچک‌ترین قلم را می‌دهد، نه قدیمی‌ترین و نه تازه‌ترین.

یک انبار که پالت‌ها را روی هم می‌چیند و یک نانوایی که نوبت می‌دهد، هر دو یک کار می‌کنند: قلم تازه را ته صف اضافه می‌کنند. تفاوتشان در یک جمله است، اینکه بعدی از کدام سر بیرون می‌آید، و همین یک جمله دو ساختار می‌سازد.

  • پالت‌ها: palet.append("P-19") و بعد palet.pop() که P-19 را می‌دهد، یعنی همان آخری که آمد
  • نوبت‌ها: nobat.append("N-07") و بعد nobat.popleft() که قدیمی‌ترین نوبت را می‌دهد

پشته به این پرسش جواب می‌دهد که پیش از این داشتم چه کار می‌کردم، و صف به این پرسش که چه کسی بیشتر از همه منتظر مانده. هر جا کاری را نیمه‌کاره رها می‌کنید تا کار تازه‌ای را شروع کنید، یک پشته لازم دارید؛ خود مفسر هم برای همین یکی دارد و برنامه چطور یادش می‌ماند باید کجا برگردد همان است.

هزینه، که تنها دلیل وجود دو کلاس است

یک فهرست معمولی برای پشته کافی است و چیزی بیشتر لازم نیست. برای صف نیست، چون برداشتن از سرِ فهرست یعنی جابه‌جا کردن همه خانه‌های بعدی. اندازه‌گیری‌شده روی یک مک‌بوک با تراشه M1 Max و پایتون 3.14 در تاریخ ۲۰۲۶-۰۹-۰۳، صد هزار برداشتن از سر: با list.pop(0) حدود ۰٫۸۲ ثانیه و با deque.popleft() حدود ۰٫۰۰۴۳ ثانیه. همان کار، دویست برابر ارزان‌تر، فقط با عوض کردن سطر ایمپورت.

چیزی که این را پنهان می‌کند این است که فهرست و deque روی صد نوبت دقیقا یک شکل رفتار می‌کنند. تفاوت با اندازه ورودی باز می‌شود و نه با اندازه کد، و شمردن کار، وقتی ورودی بزرگ می‌شود زبان گفتنش را می‌دهد.

پرانتزهای یک قالب فاکتور، با یک پشته

یک قالب فاکتور که جای متغیرها را با دو آکولاد نشان می‌دهد باید متوازن باشد. کل بررسی یک پشته است و شش سطر:

  • baz = []
  • for i, ch in enumerate(ghaleb):
  • if ch in "([{": baz.append((ch, i))
  • elif ch in ")]}":
  • if not baz or JOFT[ch] != baz.pop()[0]: return i
  • return baz[-1][1] if baz else -1

خروجی، جای دقیق اولین نویسه‌ای است که با چیزی نمی‌خواند، و اگر همه چیز بسته شده باشد منفی یک. چیزی که این را ممکن می‌کند خاصیت پشته است: هر بسته باید با آخرین بازِ هنوز بازمانده جفت شود، و دقیقا همان چیزی است که pop برمی‌گرداند. با یک شمارنده ساده نمی‌شود این را نوشت، چون شمارنده نمی‌داند کدام نوع براکت باز مانده.

جایی که کار گیر می‌کند، صفی است که فهرست است

در بیشتر کدهایی که یک تیم کوچک برای پردازش کارهای پس‌زمینه می‌نویسد، صف همان فهرست است و برداشتن با pop(0) انجام می‌شود. روی صد کار در روز هیچ‌وقت دیده نمی‌شود. وقتی همان سرویس روزی صد هزار کار می‌گیرد، مصرف پردازنده بالا می‌رود بی‌آنکه هیچ سطری عوض شده باشد، و چون هیچ خطایی در کار نیست، دنبال علت در شبکه و پایگاه داده می‌گردند. اولین جایی که باید نگاه کرد سرِ صف است. تشخیصش هم ارزان است: هر pop(0) یا del x[0] داخل یک حلقه، همین است.

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶ collections.deque در کتابخانه استاندارد و رایگان است و جای پیاده‌سازی دستی صف با فهرست را گرفته؛ هر دو سرش ارزان است و maxlen هم دارد، که یعنی یک صف با ظرفیت ثابت بدون هیچ کد اضافه‌ای. اگر لازم است بعدی نه قدیمی‌ترین باشد و نه تازه‌ترین بلکه کوچک‌ترین، ظرف سومی هست: heapq در همان کتابخانه، که صف اولویت می‌سازد و در هیچ سطری از مقایسه این صفحه نیست.

پرسش بعدی این است که وقتی هر قلم می‌تواند چند قلم دیگر را باز کند، این صف چه شکلی می‌شود و پیمایش از کجا شروع می‌شود، که سلسله‌مراتب، و گشتن در آن بدون خواندن همه‌اش جوابش را دارد. و هزینه‌ای که این صفحه اندازه گرفت از شکل خود فهرست می‌آید: ترتیب و اندیس، و هزینه‌ای که اندیس دارد.

کنار این

بالای این