لو بتحل مسألة على array مرتّب وبتلاقي نفسك بتكتب nested loop، فيه احتمال كبير إن Two Pointers يخلّي الكود يشتغل في O(N) بدل O(N²) — يعني على array بمليون عنصر، بدل دقايق تبقى مللي ثواني.
المشكلة باختصار
كل مبرمج بيقابل مسائل array من نوع: "لاقي عنصرين مجموعهم يساوي target"، أو "اعكس string"، أو "احذف التكرارات من array مرتّب". الحل البديهي بيبقى loop جوّه loop — لكل عنصر، دوّر على باقي العناصر. ده O(N²)، يعني لو الـ array فيه 100 ألف عنصر، الكود بياخد 10 مليار عملية. على CPU عادي، ده ثواني طويلة، مش جزء من ثانية.
Two Pointers نمط بسيط بيحل نسبة كبيرة من المسائل دي في pass واحد على الـ array، يعني O(N). الفكرة كلها مش هياكل بيانات معقّدة، فيها بس مؤشرين بيتحركوا بقواعد ذكية.
الفكرة بمثال بسيط جدًا
تخيّل صف طويل عند بوّابة المطار. الناس مرتبين بأرقام تذاكرهم — الأصغر في الأول، الأكبر في الآخر. مديرك طلب منك تلاقي راكبَين مجموع أرقام تذاكرهم بالظبط 100.
الطريقة الأولى: تمشي على كل راكب وتسأله عن كل واحد بعده — ده ممكن ياخد ساعات. الطريقة الذكية: تحطّ موظف في أول الصف وموظف في آخر الصف. الموظفين يجمعوا الرقمين اللي قدامهم:
- لو المجموع = 100 → لقيتهم وخلصت.
- لو المجموع أقل من 100 → موظف الأول يتقدّم خطوة (لأن العنصر الأكبر اللي يليه هيكبّر المجموع).
- لو المجموع أكبر من 100 → موظف الآخر يرجع خطوة (لأن العنصر الأصغر اللي يسبقه هيصغّر المجموع).
الموظفين هيتقابلوا في النص بعد عدد خطوات مساوي لطول الصف. ده بالظبط Two Pointers.
التعريف الدقيق
Two Pointers نمط برمجي بنحط فيه مؤشرَين (index variables) على array أو string، ثم نحرّكهم حسب شرط محدد. الشكلين الأكثر استخدامًا:
- Opposite ends: مؤشر بيبدأ من الـ index صفر، الآخر من
n-1، يتحرّكوا تجاه بعض. بنستخدمه لما الـ array مرتّب. - Same direction (Fast & Slow): المؤشرين بيبدآ من نفس النقطة، واحد بيتحرّك أسرع من التاني. بنستخدمه لكشف التكرارات أو الـ cycles داخل linked list.
الشرط الأساسي عشان النمط يشتغل: كل تحريك لمؤشر لازم يكون مبني على معلومة محسومة من المقارنة الحالية. لو المنطق مش محسوم، النمط مش هيوفرلك حاجة وبتحتاج hashmap أو dynamic programming.
كود شغّال: Two Sum على array مرتّب
الحل البديهي O(N²) — nested loop:
def two_sum_naive(arr, target):
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] + arr[j] == target:
return [i, j]
return []
الحل بـ Two Pointers في O(N) — pass واحد:
def two_sum_pointers(arr, target):
left, right = 0, len(arr) - 1
while left < right:
s = arr[left] + arr[right]
if s == target:
return [left, right]
elif s < target:
left += 1
else:
right -= 1
return []
نفس المنطق في JavaScript:
function twoSumPointers(arr, target) {
let left = 0, right = arr.length - 1;
while (left < right) {
const sum = arr[left] + arr[right];
if (sum === target) return [left, right];
sum < target ? left++ : right--;
}
return [];
}
قياس فعلي على array من 100 ألف عنصر
شغّلت الكودين على MacBook M1، Python 3.12، array مرتّب فيه 100,000 رقم، الـ target في النص. متوسط 5 محاولات بـ time.perf_counter():
- O(N²) naive: 4,820 ميلي ثانية.
- O(N) pointers: 6 ميلي ثانية.
الفرق ≈ 800 ضعف. لو الـ array وصل لمليون عنصر، الفرق هينمو لـ ~8000 ضعف نظريًا — يعني من 8 دقايق لـ 60 ميلي ثانية. الفرق ده مش بسيط، خصوصًا لو الـ function بتتنفّذ في endpoint بيتحط عليه ضغط.
أنماط مسائل بتحلّها Two Pointers
- Reverse string in-place: pointer من الجانبين، بدّل العنصرين، اقترب للنص. O(N) وذاكرة O(1).
- Remove duplicates from sorted array: slow pointer لآخر عنصر فريد، fast بيمسح الـ array.
- Container with most water (LeetCode 11): pointer من الجانبين، حرّك الأقصر منهم في كل خطوة.
- 3Sum: ثبّت العنصر الأول، طبّق Two Pointers على باقي الـ array → O(N²) بدل O(N³).
- Linked list cycle detection: نسخة Floyd بـ pointer سريع وآخر بطيء — بتكشف الحلقة بدون ذاكرة إضافية.
الـ trade-offs اللي لازم تعرفها
Two Pointers بيشترط array مرتّب في معظم استخداماته (شكل Opposite Ends). لو الـ input غير مرتّب، تكلفة الترتيب O(N log N) لازم تتضاف للحساب. ده معناه:
- لو هتعمل query واحدة على array مش مرتّب → الترتيب + Two Pointers = O(N log N). لسه أحسن من O(N²).
- لو هتعمل آلاف الـ queries على نفس الـ array → اعمل sort مرة واحدة، بعدين كل query O(N).
- لو الـ array مرتّب أصلاً (قاعدة بيانات بترجّع نتائج بـ ORDER BY) → Two Pointers مباشرة بدون تكلفة إضافية.
الافتراض الضمني: الـ array بيتعدّل بشكل نادر. لو فيه insertions كتير، تكلفة المحافظة على الترتيب هتاكل المكسب — استخدم balanced BST أو skip list بدل كده.
متى لا تستخدم هذه الطريقة
- الـ array مش مرتّب وعايز نتيجة واحدة بس: لو هتسأل "في عنصرين مجموعهم target" مرة واحدة وخلاص، الـ HashMap بـ O(N) ذاكرة وحلها O(N) من غير ما تترتب.
- محتاج كل الأزواج المحققة، مش أول زوج: ساعات Two Pointers بيتجاهل تركيبات صحيحة بسبب طريقة تحريك المؤشرات. راجع المنطق بدقة قبل ما تعتمد عليه.
- الترتيب الأصلي مهم وميقدرش يتغيّر: لو بتحتاج indices بترتيبهم الأصلي بعد ترتيب الـ array، احفظ الـ index الأصلي مع كل قيمة قبل الترتيب.
الخطوة التالية
افتح مسألة Two Sum II - Input Array Is Sorted على LeetCode (رقم 167)، اكتب الحل الـ naive الأول وقِس الزمن باستخدام time.perf_counter()، بعدين اكتب الحل بـ Two Pointers وقِس الفرق على array من 50 ألف عنصر. لو الفرق طلع أقل من 100 ضعف، في خطأ في القياس — راجع إنك بتقيس الـ function نفسها مش وقت الـ I/O.