Faragiri

همهٔ زبان‌ها

آموزش آرایه و لیستساختمان داده

خواندن خانه اول و خانه یک‌میلیونی یک زمان می‌برد. افزودن به ته فهرست ده نانوثانیه است و افزودن به سر آن پنجاه هزار.

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

بیست‌وپنج افزودن

فهرست جهشی بزرگ می‌شود، نه خانه به خانه

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

بایتی که sys.getsizeof گزارش می‌کند، در همان شش طولی که عدد عوض شد

  1. ۵۶۰
  2. ۸۸۱
  3. ۱۲۰۵
  4. ۱۸۴۹
  5. ۲۴۸۱۷
  6. ۳۱۲۲۵

این ستون‌ها خانه‌ها و سرآیند را می‌شمارند و نه خود مقدارها را. مقدارها شی‌های جدایی هستند که خانه‌ها به آن‌ها اشاره می‌کنند و هیچ بایتی از آن‌ها در این نمودار نیست.

یک فهرست پایتون آرایه‌ای از اشاره‌گرهاست که پشت سر هم در حافظه نشسته‌اند، و آدرس خانه شماره n یک ضرب و یک جمع است. برای همین خواندن با اندیس به جای خانه ربطی ندارد. روی یک فهرست یک‌میلیونی، اندازه‌گیری‌شده روی یک مک‌بوک با تراشه M1 Max و پایتون 3.14 در تاریخ ۲۰۲۶-۰۹-۰۳: خانه اول ۳۰٫۹ نانوثانیه، خانه پانصدهزارم ۲۹٫۹ و خانه آخر ۲۹٫۷.

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

  • L.append(1) حدود ۹٫۶ نانوثانیه
  • L.insert(0, 1) حدود ۴۹۵۶۰ نانوثانیه
  • L.pop() حدود ۱۷٫۳ نانوثانیه
  • L.pop(0) حدود ۵۹۴۲۶ نانوثانیه

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

راه‌حل یک ایمپورت است

collections.deque هر دو سر را ارزان نگه می‌دارد: appendleft حدود ۱۲٫۵ نانوثانیه و popleft حدود ۱۹ نانوثانیه، یعنی هم‌ردیف کار کردن با ته یک فهرست. بهایش را جای دیگری می‌دهید: خواندن با اندیس از وسط یک deque دیگر آنی نیست، چون خانه‌ها پشت سر هم نیستند. انتخاب همیشه همین است و ساختمان داده‌ای که هر دو را ارزان بدهد وجود ندارد.

فهرست جا را جهشی می‌خرد

افزودن به ته فهرست همیشه ارزان نیست، به‌طور میانگین ارزان است. پایتون هر بار که جا کم بیاید بیشتر از یکی می‌خرد، و sys.getsizeof این را لو می‌دهد: در طول صفر ۵۶ بایت، و بعد از آن ۸۸، ۱۲۰، ۱۸۴، ۲۴۸ و ۳۱۲ بایت، در حالی که بیست‌وپنج قلم وارد می‌شود. یعنی در آن بیست‌وپنج افزودن، اندازه فقط شش بار عوض شد و بقیه افزودن‌ها در جایی نوشتند که از قبل خریده شده بود. اگر تعداد نهایی را از قبل می‌دانید، ساختن فهرست در یک عبارت به‌جای یک حلقه append، همین چند بار جابه‌جایی را هم حذف می‌کند.

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

جایی که کار گیر می‌کند، عادت یک ظرف برای همه چیز است

خیلی از برنامه‌نویس‌های ایران از PHP یا جاوااسکریپت شروع می‌کنند، معمولا سر یک پروژه سایت. آرایه در PHP هم فهرست است و هم نگاشت، و آرایه جاوااسکریپت هم تقریبا هر کاری را قبول می‌کند. عادتی که با آدم می‌آید این است که یک ظرف برای همه چیز کافی است، و در پایتون هم اولین ساختاری که یاد گرفته می‌شود فهرست است و آخرینش هم همان می‌ماند. تا وقتی داده کوچک است هیچ نشانه‌ای نمی‌دهد. علامت هشدار عملی این است: هر جا داخل یک حلقه نوشتید if x in lst، دارید برای هر قلم کل فهرست را می‌خوانید، و ظرف درست چیز دیگری است.

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

در سال ۲۰۲۶ برای همین یک پرسش، timeit در کتابخانه استاندارد و رایگان است و جای گذاشتن time.time() دور یک تکه کد را گرفته؛ آن روش روی کاری که نانوثانیه طول می‌کشد چیزی جز نویز اندازه نمی‌گیرد. برای دیدن اندازه واقعی یک ساختار، sys.getsizeof در همان کتابخانه است. هیچ‌کدام به شبکه نیازی ندارند، پس همین صفحه را می‌شود بدون هیچ نصبی روی ماشین خودتان دوباره اندازه گرفت، و ارزشش را دارد چون عددها روی هر پردازنده‌ای فرق می‌کنند.

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

کنار این

بالای این