Faragiri

همهٔ زبان‌ها

آموزش پیچیدگی زمانیالگوریتم‌ها

سه اندازه ورودی و پنج نرخ رشد در یک شبکه. تفاوت میان دو ستون کناری چیزی است که سرعت پردازنده هرگز جبرانش نمی‌کند.

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

سه اندازه ورودی

همان پنج نرخ، وقتی ورودی صد و ده هزار و یک میلیون است

دو سطر اول با بزرگ شدن ورودی تقریبا تکان نمی‌خورند و دو سطر آخر از دسترس خارج می‌شوند.

تعداد عملیات پایه، شمرده‌شده و نه اندازه‌گیری‌شده با ساعت

صد قلمده هزار قلمیک میلیون قلم
خواندن با کلید۱۱۱
نصف کردن بازه۷۱۳۲۰
یک حلقه روی همه۱۰۰۱۰٬۰۰۰۱٬۰۰۰٬۰۰۰
مرتب کردن۶۶۴۱۳۲٬۸۷۷۱۹٬۹۳۱٬۵۶۹
حلقه داخل حلقه۱۰٬۰۰۰۱۰۰٬۰۰۰٬۰۰۰۱٬۰۰۰٬۰۰۰٬۰۰۰٬۰۰۰

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

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

پنج نرخی که تقریبا همه کدها در یکی از آن‌ها می‌افتند، با شمردن روی یک فهرست:

  • خواندن با اندیس یا با کلید، که به اندازه ورودی کاری ندارد
  • نصف کردن بازه در هر گام، که برای یک میلیون قلم ۲۰ گام است
  • یک حلقه ساده روی همه سطرها
  • مرتب کردن، که یک حلقه است ضربدر همان تعداد نصف کردن
  • حلقه داخل حلقه، که هر سطر را با هر سطر دیگر مقایسه می‌کند

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

ضریب‌ها را دور بیندازید، ولی نه همیشه

حذف ضریب فقط وقتی معتبر است که ورودی بتواند بزرگ شود. اگر می‌دانید هر سطر یک فاکتور است و فاکتور هیچ‌وقت بیش از پنجاه قلم ندارد، الگوریتم مرتبه دومی روی پنجاه قلم دو هزار و پانصد کار است و آن هیچ است؛ سراغ ساختار داده پیچیده رفتن اینجا کد را بدتر می‌کند بی‌آنکه چیزی بخرد. پرسش درست هیچ‌وقت «کدام مرتبه بهتر است» نیست، بلکه این است که ورودی این کد تا کجا می‌تواند بزرگ شود.

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

یک نکته که در کد فارسی زیاد دیده می‌شود

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

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

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

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

در سال ۲۰۲۶، برای تبدیل حدس به عدد، timeit در کتابخانه استاندارد و رایگان است و جای گذاشتن ساعت دور کد را گرفته. ولی ابزاری که مستقیما به این صفحه مربوط است cProfile در همان کتابخانه است: به‌جای گفتن اینکه کل برنامه چقدر طول کشید، می‌گوید هر تابع چند بار صدا زده شد، و همان ستون تعداد فراخوانی است که مرتبه را لو می‌دهد. اگر با دو برابر کردن ورودی، تعداد فراخوانی چهار برابر شد، مرتبه دوم است و لازم نیست هیچ کاغذی بیاورید.

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

کنار این

بالای این