Two-Sum ومتغيراته العديدة
حل two-sum وthree-sum وfour-sum وtwo-sum with sorted array باستخدام خرائط التجزئة والمؤشرين، مع مقارنة تكلفتي الزمن والمساحة
Two-Sum ومتغيراته العديدة درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
Two-Sum: مسألة المقابلات الكلاسيكية
في مسألة LeetCode 1 'Two Sum'، تُعطى مصفوفة غير مرتبة وقيمة مستهدفة، ويُطلب إرجاع فهرسي عنصرين مجموعهما يساوي القيمة المستهدفة. تفحص الطريقة المباشرة جميع الأزواج بتكلفة O(n²). أما الطريقة المثلى، فتكلفتها O(n) وتستخدم خريطة تجزئة: لكل عنصر x، افحصوا ما إذا كانت القيمة target - x موجودة مسبقًا في الخريطة. إذا كانت موجودة، فأرجعوا فهرسي الزوج. وإذا لم تكن موجودة، فخزّنوا x وفهرسه في الخريطة.
غالبًا ما تكون مسألة two-sum أول مسألة تُطرح في المقابلة — وإتقانها التام يدل على استعدادكم للانتقال إلى مسائل أصعب.
def twoSum(nums, target):
seen = {} # val -> index
for i, x in enumerate(nums):
complement = target - x
if complement in seen:
return [seen[complement], i]
seen[x] = i
return []
print(twoSum([2, 7, 11, 15], 9)) # [0, 1]
print(twoSum([3, 2, 4], 6)) # [1, 2]
print(twoSum([3, 3], 6)) # [0, 1]لماذا تنجح خريطة التجزئة في مسألة Two-Sum؟
تخزّن خريطة التجزئة كل عنصر تمت رؤيته حتى الآن. عند معالجة العنصر x، إذا كانت القيمة target - x موجودة في الخريطة، فإن هذين العنصرين يشكلان زوجًا صالحًا. والأهم أن فحص العنصر المكمل يتم قبل تخزين x، مما يمنع اقتران عنصر واحد بنفسه. فمثلًا، إذا كان x == target/2، فسيحدث فحص الخريطة قبل تخزين x، ولذلك لن يطابق نفسه إلا إذا وُجدت نسختان منه.
# Trace two-sum on [2, 7, 11, 15], target=9
nums, target = [2, 7, 11, 15], 9
seen = {}
for i, x in enumerate(nums):
complement = target - x
print(f'i={i} x={x} complement={complement} seen={seen}')
if complement in seen:
print(f' Found: indices [{seen[complement]}, {i}]')
break
seen[x] = iTwo-Sum على مصفوفة مرتبة (مؤشران)
إذا كانت المصفوفة مرتبة مسبقًا، وكنتم تحتاجون إلى فهارس القيم لا الفهارس الأصلية، فاستخدموا تقنية المؤشرين: مؤشرا left وright، يبدأ كل منهما من طرف معاكس. إذا كان المجموع مساويًا للقيمة المستهدفة، فأرجعوا النتيجة. وإذا كان المجموع أصغر من القيمة المستهدفة، فحرّكوا left إلى اليمين. وإذا كان أكبر منها، فحرّكوا right إلى اليسار. تبلغ تكلفة هذه الطريقة O(n) زمنيًا وO(1) من حيث المساحة، وهي أفضل من طريقة خريطة التجزئة عندما تكون المصفوفة مرتبة وتكون الذاكرة محدودة.
def twoSumSorted(numbers, target):
lo, hi = 0, len(numbers) - 1
while lo < hi:
s = numbers[lo] + numbers[hi]
if s == target:
return [lo + 1, hi + 1] # 1-indexed as per LeetCode 167
elif s < target:
lo += 1
else:
hi -= 1
return []
print(twoSumSorted([2, 7, 11, 15], 9)) # [1, 2]
print(twoSumSorted([2, 3, 4], 6)) # [1, 3]
print(twoSumSorted([-1, 0], -1)) # [1, 2]Three-Sum (LeetCode 15)
في مسألة LeetCode 15 'Three Sum'، ابحثوا عن جميع الثلاثيات الفريدة التي يساوي مجموعها صفرًا. رتّبوا المصفوفة، وثبّتوا عنصرًا واحدًا في كل مرة، ثم طبّقوا تقنية المؤشرين على المصفوفة الفرعية المرتبة المتبقية. تخطّوا القيم المكررة لتجنب الثلاثيات المكررة. الزمن: O(n²)، وهو أمثل لهذه المسألة، لأن الناتج نفسه قد يحتوي على O(n²) ثلاثيات.
def threeSum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i-1]: # skip duplicates
continue
lo, hi = i + 1, len(nums) - 1
while lo < hi:
s = nums[i] + nums[lo] + nums[hi]
if s == 0:
result.append([nums[i], nums[lo], nums[hi]])
while lo < hi and nums[lo] == nums[lo+1]: lo += 1
while lo < hi and nums[hi] == nums[hi-1]: hi -= 1
lo += 1; hi -= 1
elif s < 0:
lo += 1
else:
hi -= 1
return result
print(threeSum([-1, 0, 1, 2, -1, -4])) # [[-1,-1,2],[-1,0,1]]
print(threeSum([0, 0, 0, 0])) # [[0,0,0]]Four-Sum (LeetCode 18)
في مسألة LeetCode 18 'Four Sum'، ابحثوا عن جميع الرباعيات الفريدة التي يساوي مجموعها القيمة المستهدفة. وسّعوا فكرة three-sum: ثبّتوا عنصرين باستخدام حلقتين متداخلتين، مع تخطي التكرارات، ثم طبّقوا تقنية المؤشرين على المصفوفة الفرعية الداخلية. الزمن: O(n³). وبالنسبة إلى k-sum عمومًا، يتمثل النمط في التكرار k-2 مرة، ثم تطبيق تقنية المؤشرين، مما يعطي زمنًا مقداره O(n^(k-1)).
def fourSum(nums, target):
nums.sort()
n, result = len(nums), []
for i in range(n - 3):
if i > 0 and nums[i] == nums[i-1]:
continue
for j in range(i+1, n-2):
if j > i+1 and nums[j] == nums[j-1]:
continue
lo, hi = j+1, n-1
while lo < hi:
s = nums[i]+nums[j]+nums[lo]+nums[hi]
if s == target:
result.append([nums[i],nums[j],nums[lo],nums[hi]])
while lo < hi and nums[lo] == nums[lo+1]: lo += 1
while lo < hi and nums[hi] == nums[hi-1]: hi -= 1
lo += 1; hi -= 1
elif s < target: lo += 1
else: hi -= 1
return result
print(fourSum([1,0,-1,0,-2,2], 0))
# [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]Two-Sum الأقرب إلى القيمة المستهدفة
من الصيغ الشائعة للمسألة: إيجاد الزوج الذي يكون مجموعه الأقرب إلى القيمة المستهدفة، وقد لا يساويها تمامًا. رتّبوا المصفوفة واستخدموا مؤشرين. سجّلوا أقرب مجموع عُثر عليه حتى الآن، وحدّثوه كلما وجدتم زوجًا أصغر فرقُه المطلق عن القيمة المستهدفة. وتُعد هذه الطريقة، التي تبلغ كلفتها O(n log n)، مباشرة بعد الترتيب.
def twoSumClosest(nums, target):
nums.sort()
lo, hi = 0, len(nums) - 1
best = float('inf')
best_pair = None
while lo < hi:
s = nums[lo] + nums[hi]
if abs(s - target) < abs(best - target):
best = s
best_pair = (nums[lo], nums[hi])
if s < target:
lo += 1
elif s > target:
hi -= 1
else:
return best_pair # exact match
return best_pair
print(twoSumClosest([1, 3, 4, 7, 10], 15)) # (7, 10) => 17, closest to 15
print(twoSumClosest([2, 5, 8, 11], 10)) # (2, 8) => 10, exact!Two-Sum مع أزواج متعددة (جميع الأزواج)
لإيجاد جميع الأزواج التي يساوي مجموعها قيمة مستهدفة، رتّبوا المصفوفة واستخدموا مؤشرين لجمع جميع الأزواج. بعد العثور على زوج صالح، تخطّوا القيم المكررة من الطرفين قبل المتابعة. يعطي ذلك تكلفة O(n log n) للترتيب، إضافة إلى O(n) للفحص، أي O(n log n) إجمالًا. ويُعد استخدام خريطة تجزئة لجمع الأزواج صالحًا أيضًا، لكنه يتطلب الانتباه إلى التكرارات.
def twoSumAllPairs(nums, target):
nums.sort()
lo, hi = 0, len(nums) - 1
pairs = []
while lo < hi:
s = nums[lo] + nums[hi]
if s == target:
pairs.append((nums[lo], nums[hi]))
while lo < hi and nums[lo] == nums[lo+1]: lo += 1
while lo < hi and nums[hi] == nums[hi-1]: hi -= 1
lo += 1; hi -= 1
elif s < target:
lo += 1
else:
hi -= 1
return pairs
print(twoSumAllPairs([1,1,2,3,4,4,5], 5)) # [(1,4),(1,4)-deduped,(2,3)]
# After duplicate-skipping: [(1,4),(2,3)]عدّ الأزواج التي يقل مجموعها عن K
من الصيغ الأخرى للمسألة: عدّ عدد الأزواج التي يقل مجموعها عن k. رتّبوا المصفوفة واستخدموا مؤشرين. عندما يتحقق الشرط nums[lo] + nums[hi] < k، تكون جميع الأزواج (lo, lo+1) و(lo, lo+2) ... و(lo, hi) صالحة، أي ما مجموعه hi - lo من الأزواج. حرّكوا lo إلى الأمام، وإلا فقلّصوا hi. الزمن الإجمالي: O(n log n) للترتيب، إضافة إلى O(n) للعدّ.
def countPairsLessThan(nums, k):
nums.sort()
lo, hi = 0, len(nums) - 1
count = 0
while lo < hi:
if nums[lo] + nums[hi] < k:
count += hi - lo # all (lo, lo+1)...(lo, hi) are valid
lo += 1
else:
hi -= 1
return count
print(countPairsLessThan([1, 3, 7, 11, 12], 10)) # (1,3),(1,7),(3,7) => 3
print(countPairsLessThan([3, 5, 2, 3], 7)) # (2,3),(2,3) => 2... verifyTwo-Sum باستخدام خريطة تجزئة: معالجة التكرارات
عندما يمكن أن تظهر القيمة نفسها عدة مرات، وتحتاجون إلى عدّ الأزواج الصالحة لا مجرد التحقق من وجودها، خزّنوا تكرارات القيم في الخريطة. بالنسبة إلى الأزواج التي يتساوى عنصراها، يكون عدد الأزواج الناتجة عن تكرار مقداره f هو f*(f-1)//2. أما إذا اختلف العنصران، فاضربوا تكراريهما. يتيح ذلك عدّ جميع الأزواج الصالحة بتكلفة O(n).
from collections import Counter
def countTwoSumPairs(nums, target):
freq = Counter(nums)
count = 0
seen = set()
for x in freq:
y = target - x
if y in freq and (x, y) not in seen:
if x == y:
count += freq[x] * (freq[x] - 1) // 2
else:
count += freq[x] * freq[y]
seen.add((x, y))
seen.add((y, x))
return count
print(countTwoSumPairs([1,1,2,3,4,4,3], 4))
# Pairs summing to 4: (1,3)x2x2=4, (0+more)...التعرّف على صيغ نمط Two-Sum
يظهر نمط two-sum في صور عديدة. تعرّفوا عليه عندما تطلب المسألة إيجاد عنصرين أو أكثر يحققون علاقة عددية، مثل الجمع أو الضرب أو الطرح. تتمثل الاستراتيجية الأساسية دائمًا في تثبيت عنصر واحد، ثم إيجاد مكمله في بنية محسوبة مسبقًا، مثل خريطة تجزئة أو مصفوفة مرتبة مع مؤشر. ولتوسيع النمط إلى k-sum، ثبّتوا k-2 من العناصر باستخدام حلقات متداخلة، ثم طبّقوا الحالة الأساسية.
# Summary of approaches by scenario
scenarios = [
('Unsorted array, any indices, one pair', 'hash map O(n) time O(n) space'),
('Sorted array, any indices, one pair', 'two pointers O(n) time O(1) space'),
('All unique pairs summing to target', 'sort + two pointers O(n log n)'),
('Three numbers summing to zero (3-sum)', 'sort + fix + two pointers O(n^2)'),
('k numbers summing to target (k-sum)', 'sort + k-2 loops + two pointers O(n^(k-1))')
]
for scenario, approach in scenarios:
print(f'{scenario}\n => {approach}\n')التواصل في المقابلة حول Two-Sum
عندما تظهر مسألة two-sum في المقابلة، اشرحوا طريقة تفكيركم بصوت مسموع: «أحتاج إلى عددين مجموعهما يساوي القيمة المستهدفة. ولكل عدد x، أحتاج إلى التحقق مما إذا كانت القيمة target-x موجودة. ويمكنني الإجابة عن ذلك بتكلفة O(1) باستخدام خريطة تجزئة، مما يعطي زمنًا إجماليًا قدره O(n) ومساحة قدرها O(n). وبدلًا من ذلك، إذا كانت المصفوفة مرتبة، فيمكنني استخدام مؤشرين مع استهلاك O(1) من المساحة». اذكروا الطريقتين واسألوا عن قيود المساحة قبل الاختيار.
تحقق سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.
ملخص الدرس
تعلمتم في هذا الدرس أن: two-sum يستخدم جدول تجزئة للتحقق من وجود المتمم في O(1)، مما يمنحه تعقيدًا إجماليًا قدره O(n)، والمصفوفات المرتبة تستفيد من مؤشرين لتحقيق مساحة قدرها O(1)، كما أن three-sum وfour-sum يُختزلان إلى two-sum باستخدام الفرز والحلقات المتداخلة، ويعملان بتعقيدين قدرهما O(n²) وO(n³) على التوالي. بعد ذلك، سنستكشف أنماط عدّ التكرارات والتجميع باستخدام defaultdict وCounter.
الأسئلة الشائعة
هل درس «Two-Sum ومتغيراته العديدة» مجاني؟
نعم — نص درس «Two-Sum ومتغيراته العديدة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «Two-Sum ومتغيراته العديدة»؟
حل two-sum وthree-sum وfour-sum وtwo-sum with sorted array باستخدام خرائط التجزئة والمؤشرين، مع مقارنة تكلفتي الزمن والمساحة تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «Two-Sum ومتغيراته العديدة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- آليات دوال التجزئة ومعالجة التصادمات
- Two-Sum ومتغيراته العديدة
- عدّ التكرارات والتجميع
- أطول تسلسل متتالٍ وذاكرة LRU المؤقتة