الحد الأدنى والحد الأعلى
نفّذ bisect_left وbisect_right من الصفر، ثم طبّقهما للعثور على الموضعين الأول والأخير لقيمة مستهدفة
الحد الأدنى والحد الأعلى درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ما الحد الأدنى والحد الأعلى؟
الـحد الأدنى لقيمة هدف في مصفوفة مرتبة هو فهرس أول عنصر أكبر من الهدف أو يساويه (ويُسمى غالبًا bisect_left). أما الـحد الأعلى فهو فهرس أول عنصر أكبر من الهدف تمامًا (bisect_right). ويحددان معًا نطاق جميع تكرارات الهدف ويمكّنان من تنفيذ استعلامات النطاق في O(log n).
تشكل هاتان العمليتان أساسًا للعديد من مسائل المقابلات: عدّ التكرارات، والعثور على النطاق، وتحديد موضع الإدراج، وغير ذلك.
arr = [1, 2, 2, 2, 3, 5]
# lower bound of 2 => index 1 (first element >= 2)
# upper bound of 2 => index 4 (first element > 2)
# occurrences of 2 => upper - lower = 4 - 1 = 3
print('lower bound of 2:', 1)
print('upper bound of 2:', 4)
print('count of 2:', 4 - 1)تنفيذ الحد الأدنى (bisect_left)
تعيد bisect_left(arr, x) أقصى اليسار للفهرس i الذي يحقق arr[i] >= x، أو تعيد len(arr) إذا كانت جميع العناصر أصغر. يستخدم التنفيذ حدًا علويًا حصريًا: hi = len(arr)، وشرط الحلقة lo < hi، وتحديث hi = mid عندما يكون arr[mid] >= x. ويضمن ذلك تقارب الإجابة إلى أول موضع صالح من اليسار.
def bisect_left(arr, x):
lo, hi = 0, len(arr)
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < x:
lo = mid + 1
else:
hi = mid # arr[mid] >= x, so potential answer
return lo # lo == hi == insertion point
arr = [1, 2, 2, 2, 3, 5]
print(bisect_left(arr, 2)) # 1
print(bisect_left(arr, 0)) # 0 (before all)
print(bisect_left(arr, 6)) # 6 (after all)
print(bisect_left(arr, 3)) # 4تنفيذ الحد الأعلى (bisect_right)
تعيد bisect_right(arr, x) أقصى اليسار للفهرس i الذي يحقق arr[i] > x. يختلف هذا التنفيذ عن bisect_left في سطر واحد فقط: يتغير الشرط من arr[mid] < x إلى arr[mid] <= x. عندما يكون arr[mid] <= x، تكون الإجابة إلى يمين mid تمامًا، لذا نعيّن lo = mid + 1؛ وإلا نضيّق النطاق من اليمين.
def bisect_right(arr, x):
lo, hi = 0, len(arr)
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] <= x:
lo = mid + 1 # arr[mid] <= x, so answer is strictly right
else:
hi = mid
return lo
arr = [1, 2, 2, 2, 3, 5]
print(bisect_right(arr, 2)) # 4
print(bisect_right(arr, 0)) # 0
print(bisect_right(arr, 5)) # 6
print(bisect_right(arr, 4)) # 5عدّ التكرارات باستخدام الحدين
لعدّ تكرارات هدف في مصفوفة مرتبة بزمن O(log n)، طبّقوا الحدين معًا: count = bisect_right(arr, target) - bisect_left(arr, target). إذا كان العدد 0، فهذا يعني أن الهدف غير موجود. وتُعد هذه الطريقة أسرع بكثير من المسح الخطي، وهي الأسلوب القياسي لتنفيذ استعلامات التكرار على البيانات المرتبة.
import bisect
def count_occurrences(arr, target):
left = bisect.bisect_left(arr, target)
right = bisect.bisect_right(arr, target)
return right - left
arr = [1, 2, 2, 2, 3, 3, 5]
print(count_occurrences(arr, 2)) # 3
print(count_occurrences(arr, 3)) # 2
print(count_occurrences(arr, 4)) # 0
print(count_occurrences(arr, 1)) # 1العثور على الموضعين الأول والأخير للهدف
تطلب مسألة LeetCode 34، 'Find First and Last Position of Element in Sorted Array'، إعادة [first_idx, last_idx] في زمن O(log n). الموضع الأول هو bisect_left(arr, target) — ولكن بشرط أن يكون arr[result] == target. أما الموضع الأخير فهو bisect_right(arr, target) - 1. إذا فشل أي من الفحصين، فأعيدوا [-1, -1].
import bisect
def search_range(nums, target):
left = bisect.bisect_left(nums, target)
if left == len(nums) or nums[left] != target:
return [-1, -1]
right = bisect.bisect_right(nums, target) - 1
return [left, right]
print(search_range([5,7,7,8,8,10], 8)) # [3, 4]
print(search_range([5,7,7,8,8,10], 6)) # [-1, -1]
print(search_range([], 0)) # [-1, -1]موضع الإدراج (LeetCode 35)
تسأل مسألة LeetCode 35، 'Search Insert Position'، عن الموضع الذي سيُدرج فيه الهدف للحفاظ على ترتيب المصفوفة. وهذا يساوي تمامًا bisect_left(arr, target). إذا كان الهدف موجودًا، يعيد bisect_left فهرسه. وإذا لم يكن موجودًا، يعيد الفهرس الذي سيُدرج الهدف عنده. لا حاجة إلى معالجة خاصة — فالدالة نفسها تتعامل مع الحالتين.
import bisect
def searchInsert(nums, target):
return bisect.bisect_left(nums, target)
print(searchInsert([1,3,5,6], 5)) # 2 (exists at index 2)
print(searchInsert([1,3,5,6], 2)) # 1 (would insert between 1 and 3)
print(searchInsert([1,3,5,6], 7)) # 4 (would append at end)
print(searchInsert([1,3,5,6], 0)) # 0 (would prepend)الفرق بين bisect_left و bisect_right
عند عدم وجود عناصر مكررة، تعيد bisect_left وbisect_right الفهرس نفسه. ولا يظهر الفرق إلا عندما يظهر الهدف عدة مرات. يشير bisect_left إلى أول نسخة؛ بينما يشير bisect_right إلى الموضع الذي يلي آخر نسخة. اختاروا دائمًا الدالة وفقًا لما إذا كنتم تريدون الإدراج قبل النسخ الموجودة (اليسار) أو بعدها (اليمين).
import bisect
arr = [1, 2, 2, 2, 3]
# Insert a new 2 before all existing 2s
print(bisect.bisect_left(arr, 2)) # 1
# Insert a new 2 after all existing 2s
print(bisect.bisect_right(arr, 2)) # 4
# For a value not in array, both give same insertion point
print(bisect.bisect_left(arr, 2.5)) # 4
print(bisect.bisect_right(arr, 2.5)) # 4تطبيق الحدود على استعلامات التكرار في البيانات المرتبة
عندما تحتاجون إلى الإجابة بكفاءة عن العديد من استعلامات التكرار ضمن نطاقات في مصفوفة مرتبة، احسبوا ترتيب المصفوفة مسبقًا مرة واحدة، ثم استخدموا bisect لكل استعلام. يجيب كل استعلام عن سؤال «كم عنصرًا يقع ضمن [lo, hi]؟» في O(log n) بدلًا من O(n). يظهر هذا النمط في المسائل التي تتعلق بعدّ العناصر الواقعة ضمن نطاق من القيم بعد الترتيب.
import bisect
def count_in_range(arr, lo, hi):
'''Count elements in arr with lo <= val <= hi. arr must be sorted.'''
left = bisect.bisect_left(arr, lo)
right = bisect.bisect_right(arr, hi)
return right - left
arr = sorted([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])
print(arr) # [1,1,2,3,3,4,5,5,5,6,9]
print(count_in_range(arr, 3, 5)) # 6 (3,3,4,5,5,5)
print(count_in_range(arr, 1, 2)) # 3 (1,1,2)البحث الثنائي باستخدام مفتاح مخصص
أحيانًا لا تكون قيمة البحث هي القيمة المخزنة نفسها، بل خاصية مشتقة منها. لا تدعم الوحدة bisect في Python دالة مفتاح مباشرة، ولكن يمكنكم تنفيذ البحث الثنائي يدويًا عبر تطبيق المفتاح داخل الحلقة. يظهر هذا النمط عند البحث في قائمة من الكائنات باستخدام إحدى سماتها.
# Binary search on a list of (score, name) tuples by score
def lower_bound_by_score(records, min_score):
lo, hi = 0, len(records)
while lo < hi:
mid = lo + (hi - lo) // 2
if records[mid][0] < min_score:
lo = mid + 1
else:
hi = mid
return lo
records = [(50, 'Alice'), (72, 'Bob'), (72, 'Carol'), (88, 'Dave'), (95, 'Eve')]
idx = lower_bound_by_score(records, 72)
print(idx) # 1 (first record with score >= 72)
print(records[idx:]) # [(72,'Bob'),(72,'Carol'),(88,'Dave'),(95,'Eve')]أخطاء المقابلات الشائعة عند استخدام الحدود
الخطأ الأكثر شيوعًا هو نسيان التحقق بعد استدعاء bisect_left. فالدالة تعيد دائمًا فهرس إدراج صالحًا، لكنها لا تضمن أن العنصر الموجود عند ذلك الفهرس يساوي الهدف. تحققوا دائمًا من arr[result] == target قبل افتراض العثور على الهدف.
أما الخطأ الثاني فهو استخدام bisect_right عند الحاجة إلى أول تكرار — إذ تعيد bisect_right الموضع الذي يلي آخر تكرار، ولذلك فإن طرح 1 يعطي الموضع الأخير، لا الأول.
import bisect
arr = [1, 3, 5, 7]
target = 4
# bisect_left returns 2 (insertion point for 4 between 3 and 5)
idx = bisect.bisect_left(arr, target)
print(idx) # 2
# Validate: arr[2] is 5, not 4 => target absent
found = idx < len(arr) and arr[idx] == target
print('Found:', found) # Falseالملخص: متى تستخدمون bisect_left ومتى تستخدمون bisect_right؟
استخدموا bisect_left عندما تحتاجون إلى: أول تكرار للهدف، أو موضع إدراج يدفع النسخ الموجودة إلى اليمين، أو التحقق من وجود الهدف. واستخدموا bisect_right عندما تحتاجون إلى: الموضع الذي يلي آخر تكرار، أو موضع الإدراج بعد جميع النسخ الموجودة، أو عدد العناصر <= الهدف (وهو يساوي bisect_right(arr, target)).
تعمل كلتا الدالتين في O(log n)، وهما جزء من مكتبة Python القياسية، لذا يمكنكم استيرادهما واستخدامهما مباشرةً ما لم يطلب منكم المحاوِر تنفيذهما من الصفر.
اختبار سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلّمتم في هذا الدرس أن: bisect_left تعثر على أول عنصر >= الهدف، وbisect_right تعثر على أول عنصر > الهدف (أي الموضع الذي يلي آخر تكرار)، والفرق بينهما يعطي عدد التكرارات في O(log n). سنتناول بعد ذلك البحث الثنائي في فضاء الإجابات، حيث يكون فضاء البحث نطاقًا من الإجابات الممكنة، لا فهرسًا في مصفوفة.
الأسئلة الشائعة
هل درس «الحد الأدنى والحد الأعلى» مجاني؟
نعم — نص درس «الحد الأدنى والحد الأعلى» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «الحد الأدنى والحد الأعلى»؟
نفّذ bisect_left وbisect_right من الصفر، ثم طبّقهما للعثور على الموضعين الأول والأخير لقيمة مستهدفة تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.
كم من الوقت يستغرق درس «الحد الأدنى والحد الأعلى»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- البحث الثنائي الكلاسيكي: اليسار واليمين والوسط
- البحث الثنائي في المصفوفات المدورة وغير المرتبة
- الحد الأدنى والحد الأعلى
- البحث الثنائي في فضاء الإجابات