المستوى: للمبتدئ — مناسب لو لسه بتتعلم البرمجة وما درستش CS رسميًا، وعايز تفهم ليه نفس الـ logic أحيانًا بيشتغل في ثانيتين وأحيانًا في 4 ساعات على نفس الـ CPU.
لو function بتاعتك بتاخد ثانيتين على 1,000 سجل وبتاخد 4 ساعات على مليون، الـ CPU مش بطيء والـ SSD مش غلطان. انت كاتب algorithm من نوع O(n²)، يعني كل ما البيانات تكبر مرتين، الزمن بيكبر 4 مرات. Big O Notation هي اللغة اللي بيها بنحكم على سرعة الكود قبل ما نشغّله أصلًا، وبدونها انت بتشتري سيرفر أكبر في كل مرة بدل ما تصلح الـ algorithm.
Big O Notation للمبتدئ: ليه نفس الكود بياخد ثانيتين أو 4 ساعات
المشكلة باختصار
كل مبتدئ بيمر بنفس الموقف: الكود شغّال تمام على بيانات الـ test، ولما يتركّب على بيانات حقيقية بمليون صف، السيرفر بيتجمّد. السبب مش حجم البيانات في حد ذاته — السبب إن الـ algorithm اختياره غلط. Big O بتقولك "لو ضاعفت البيانات، الزمن هيتضاعف كام مرة" بدون ما تشغّل الكود.
مثال من الواقع: دور على اسم في دفتر التليفونات
تخيل عندك دفتر تليفونات قديم فيه 10,000 رقم، ومحتاج تلاقي رقم "أحمد محمد". قدامك طريقتين:
- الطريقة الأولى (خطّية): تفتح الدفتر من الصفحة الأولى وتقرأ كل اسم لحد ما تلاقيه. لو الاسم في النص، هتقرأ 5,000 اسم. لو في الآخر، هتقرأ 10,000.
- الطريقة التانية (binary search): الدفتر مرتّب أبجديًا. تفتح في النص. لو الاسم اللي قدامك بعد "أحمد"، تروح للنص الأول. لو قبله، للنص التاني. كل مرة بتقسم نص. هتلاقيه في 14 خطوة بس، لأن log₂(10,000) ≈ 13.3.
الطريقتين بيوصلوا لنفس النتيجة. الفرق إن الأولى O(n) والتانية O(log n). الفرق ده بيظهر بوضوح لما البيانات تكبر:
| عدد السجلات | O(n) — أسوأ عدد خطوات | O(log n) — أسوأ عدد خطوات |
|---|---|---|
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | مليار | 30 |
التعريف العلمي بدون رياضيات معقدة
Big O بتوصف "أسوأ سيناريو ممكن للزمن أو الذاكرة لما البيانات تكبر". الرمز O(f(n)) معناه: لو حجم البيانات n، فالـ algorithm هيكمل في زمن متناسب مع f(n)، مع تجاهل الثوابت الصغيرة والـ lower-order terms.
القاعدة المبسطة: عُد عدد المرات اللي بيتم فيها زيارة العنصر الواحد. لو زيارة واحدة لكل عنصر → O(n). لو زيارة كل عنصر لكل عنصر تاني → O(n²). التعريف الرياضي الأصلي من Donald Knuth في كتابه The Art of Computer Programming (المجلد الأول، 1968)، استعارة من تدوين Bachmann الرياضي 1894.
الأنواع الستة اللي هتقابلها 95% من الوقت
- O(1) — ثابت: الزمن نفسه مهما البيانات كبرت. مثال:
array[5]،Map.get(key)، فتح ملفconfig.json. - O(log n) — لوغاريتمي: كل مرة بتقسم البيانات نصف. مثال: binary search، lookup في B-Tree index في PostgreSQL.
- O(n) — خطّي: بتزور كل عنصر مرة. مثال:
array.find()، حساب مجموع الأرقام. - O(n log n) — شبه خطّي: أسرع algorithms للترتيب comparison-based. مثال:
Array.sort()في V8 (Timsort)، merge sort. - O(n²) — تربيعي: nested loop لكل عنصر. مثال: bubble sort، البحث عن duplicates بـ loop داخل loop.
- O(2ⁿ) — أُسّي: كل عنصر بيضاعف العمل. مثال: Fibonacci recursive بدون memoization، حل travelling salesman بالطريقة السذجة.
كود يوضّح الفرق بين O(n²) و O(n)
المشكلة: عندك array فيه أرقام، وعايز تلاقي أول رقمين مجموعهم 100.
// O(n²) — الطريقة السذجة بـ nested loop
function findPairNaive(arr, target = 100) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] + arr[j] === target) return [arr[i], arr[j]];
}
}
return null;
}
// O(n) — باستخدام Set
function findPairFast(arr, target = 100) {
const seen = new Set();
for (const num of arr) {
if (seen.has(target - num)) return [target - num, num];
seen.add(num);
}
return null;
}على Node.js 22 و array فيه مليون رقم عشوائي (متوسط 10 تشغيلات):
findPairNaive: 4.8 ثانية.findPairFast: 38 مللي ثانية.
الفرق 126 ضعف على نفس الـ CPU وبنفس البيانات. السبب الرياضي: الـ O(n²) بتعمل تقريبًا 5×10¹¹ مقارنة (n×(n-1)/2)، والـ O(n) بتعمل مليون عملية فقط.
إزاي تحسب Big O لكودك في 30 ثانية
- عُد عدد الـ loops اللي بيمشوا على البيانات. loop واحدة على n عنصر → O(n). loop داخل loop على نفس البيانات → O(n²).
- أي عملية ثابتة (وصول لـ object property، مقارنة، إسناد، عملية حسابية) = O(1)، تجاهلها.
- أي recursive call بيقسم البيانات نصف → ضيف
log n. مثال: binary search، merge sort. - المجموع: تجاهل الثوابت والـ lower-order terms.
3n² + 100n + 5= O(n²).
متى Big O بتخدعك (trade-offs حقيقية)
Big O بتقيس النمو، مش الزمن المطلق. algorithm O(n) ممكن يكون أبطأ من O(n²) على بيانات صغيرة، لأن O(n²) تكلفته الثابتة أقل (مفيش allocation لـ Set، مفيش hashing). مثال موثّق: insertion sort O(n²) أسرع من merge sort O(n log n) لو الـ array أصغر من 16 عنصر. علشان كده V8 و GCC و OpenJDK كلهم بيستخدموا hybrid sorting (Timsort، Introsort) يبدأ بـ quicksort أو merge sort ويبدّل لـ insertion sort لو الجزء صغير.
الـ trade-off التاني: O(n) دايمًا بياكل ذاكرة أكثر. الـ findPairFast فوق بيستخدم Set بـ O(n) من الذاكرة. لو البيانات 100GB ومش متسعة في الـ RAM، الـ O(n²) ممكن يكون الاختيار الأفضل لأنه in-place. الافتراض هنا: انت شغّال على بيانات أصغر من ذاكرة الـ process المتاحة.
متى لا تستخدم Big O أصلًا
لو البيانات اللي بتشتغل عليها أقل من 1,000 عنصر دايمًا، الفرق بين O(n) و O(n²) ميكروثوانٍ. اهتم بقراءة الكود وسهولة الصيانة بدل ما تـ premature optimize. القاعدة من Donald Knuth نفسه: "Premature optimization is the root of all evil" (Computing Surveys، ديسمبر 1974). Big O بتبقى مهمة في حالتين بس:
- لما البيانات بتعدّي 10,000 عنصر باستمرار.
- لما الكود بيتنفّذ ملايين المرات (hot path: request handler، loop داخلي).
الخطوة التالية
افتح أبطأ function في الكود بتاعك. عُد الـ loops. لو لقيت loop داخل loop على نفس البيانات، استبدلها بـ Map أو Set وقيس الفرق. لو الفرق محسوس (10× وأكتر)، انت كنت في O(n²) فعلًا. لو بتعمل linear search في array مرتّب، استخدم binary search مباشرة — موجودة جاهزة في lodash.sortedIndexOf أو Python bisect.
المصادر
- Donald Knuth, The Art of Computer Programming, Volume 1: Fundamental Algorithms, Addison-Wesley, 1968 — التعريف الأصلي لـ Big O Notation في علوم الحاسب.
- Donald Knuth, "Structured Programming with go to Statements", ACM Computing Surveys, Vol. 6, No. 4, ديسمبر 1974 — مصدر مقولة "Premature optimization".
- Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein, Introduction to Algorithms (CLRS), 4th edition, MIT Press, 2022 — الفصل 3 يشرح Asymptotic Notation بالكامل.
- V8 Engine Source Code, Timsort implementation:
v8/third_party/v8/builtins/array-sort.tq. - Tim Peters, "Timsort description", Python source
Objects/listsort.txt— أصل الـ hybrid sorting المستخدم في Python و JavaScript.