البحث الثنائي في المصفوفات المدورة وغير المرتبة
حل search-in-rotated-sorted-array وfind-minimum-in-rotated-array بتحديد النصف المرتب في كل خطوة
البحث الثنائي في المصفوفات المدورة وغير المرتبة درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ما المصفوفة المرتبة المدوّرة؟
المصفوفة المرتبة المدوّرة هي مصفوفة مرتبة قُطعت عند نقطة محورية ثم بُدّل ترتيب الجزأين. فعلى سبيل المثال، تمثل [4, 5, 6, 7, 0, 1, 2] المصفوفة المرتبة [0,1,2,4,5,6,7] بعد تدويرها عند الفهرس 4. يفشل البحث الثنائي القياسي هنا لأن المصفوفة لم تعد مرتبة على مستوى عناصرها جميعًا.
تتمثل الفكرة الأساسية في أن نصفًا واحدًا على الأقل من المصفوفة يكون مرتبًا دائمًا بعد أي عملية تدوير. لذلك يجب أن يحدد البحث الثنائي أي نصف مرتب قبل تقرير كيفية تحريك الحدود.
# A rotated sorted array — one half is always sorted
arr = [4, 5, 6, 7, 0, 1, 2]
# Left half [4,5,6,7] is sorted
# Right half [0,1,2] is also sorted
# But left[0]=4 > right[-1]=2 => rotation happened in left-to-right crossingتحديد النصف المرتب
بعد حساب mid، قارن arr[lo] بـ arr[mid]. إذا كان arr[lo] <= arr[mid]، فهذا يعني أن النصف الأيسر مرتب؛ وإلا فسيكون النصف الأيمن مرتبًا. بعد معرفة النصف المرتب، يمكنك التحقق مما إذا كان الهدف يقع ضمن هذا النطاق المرتب، ثم تضييق نطاق البحث وفقًا لذلك.
تتيح لك شجرة القرارات هذه استبعاد نصف المصفوفة بالضبط في كل خطوة، مع الحفاظ على التعقيد O(log n) حتى في المصفوفة المدورة.
def search_rotated(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
# Left half is sorted
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
# Right half is sorted
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0)) # 4
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 3)) # -1تتبّع مثال
لنتتبّع تنفيذ search_rotated([4,5,6,7,0,1,2], 0) خطوةً خطوة. في البداية تكون القيم lo=0, hi=6, mid=3, arr[mid]=7. هل يوجد الهدف 0 في النصف الأيسر المرتب [4..7]؟ لا، لذا نحرّك lo=4. الآن تصبح القيم lo=4, hi=6, mid=5, arr[mid]=1. النصف الأيسر [0,1] مرتب (arr[lo]=0 <= arr[mid]=1). هل يوجد 0 ضمن [0..1)؟ نعم، لذا نعيّن hi=4. الآن تصبح القيم lo=4, hi=4, mid=4, arr[4]=0 — وقد عُثر عليه عند الفهرس 4.
# Step-by-step trace
nums = [4, 5, 6, 7, 0, 1, 2]
target = 0
steps = []
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
steps.append(f'lo={lo} hi={hi} mid={mid} val={nums[mid]}')
if nums[mid] == target:
steps.append(f'Found at {mid}')
break
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
for s in steps:
print(s)التعامل مع العناصر المكررة في التدوير
عندما يمكن أن تحتوي المصفوفة المدورة على عناصر مكررة (مثل [1,3,1,1,1])، يصبح الشرط nums[lo] == nums[mid] غير حاسم — إذ لا يمكنك معرفة أي النصفين مرتب. والحل الآمن هو زيادة lo (أو إنقاص hi) بمقدار واحد ثم إعادة المحاولة. يؤدي هذا إلى تدهور الزمن في أسوأ الحالات إلى O(n)، وعليكم ذكر ذلك للمحاوِر.
def search_rotated_with_dups(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return True
# Ambiguous: shrink left boundary
if nums[lo] == nums[mid] == nums[hi]:
lo += 1
hi -= 1
elif nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return False
print(search_rotated_with_dups([1, 3, 1, 1, 1], 3)) # True
print(search_rotated_with_dups([2, 2, 2, 0, 2], 0)) # Trueالعثور على أصغر عنصر في مصفوفة مرتبة مدورة
تطلب مسألة مرتبطة بذلك العثور على العنصر الأصغر في مصفوفة مرتبة مدورة، من دون البحث عن هدف محدد. يوجد العنصر الأصغر دائمًا في النصف غير المرتب. في كل خطوة: إذا كان arr[mid] > arr[hi]، فالعنصر الأصغر موجود في النصف الأيمن (lo = mid + 1)؛ وإلا فهو موجود في النصف الأيسر، بما في ذلك mid (hi = mid). عندما يصبح lo == hi، تكون قد عثرت على العنصر الأصغر.
def find_min(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] > nums[hi]:
lo = mid + 1 # min is in right half
else:
hi = mid # min is at mid or left of mid
return nums[lo]
print(find_min([3, 4, 5, 1, 2])) # 1
print(find_min([4, 5, 6, 7, 0, 1, 2])) # 0
print(find_min([11, 13, 15, 17])) # 11 (no rotation)لماذا يكتشف arr[lo] <= arr[mid] أن النصف الأيسر مرتب
يعمل الشرط arr[lo] <= arr[mid] لأن العنصر الأول في مقطع مرتب (أو مقطع مرتب لم يُدوَّر) يكون دائمًا الأصغر. إذا كان arr[lo] <= arr[mid]، فهذا يعني أن التدوير لم يحدث داخل [lo..mid]، ولذلك يكون هذا النصف مرتبًا. وتتعامل المساواة مع الحالة التي يكون فيها lo == mid (إذ يكون المقطع المكوّن من عنصر واحد مرتبًا بحكم التعريف).
وعلى العكس، إذا كان arr[lo] > arr[mid]، فلا بد أن يكون محور التدوير واقعًا بين lo و mid، ما يعني أن النصف الأيمن [mid..hi] هو المقطع المرتب المتصل.
# Visualise: detect which half is sorted
examples = [
([4, 5, 6, 7, 0, 1, 2], 0, 6), # mid=3, val=7 => left sorted
([6, 7, 0, 1, 2, 4, 5], 0, 6), # mid=3, val=1 => right sorted
]
for arr, lo, hi in examples:
mid = lo + (hi - lo) // 2
if arr[lo] <= arr[mid]:
print(f'arr[{lo}]={arr[lo]} <= arr[{mid}]={arr[mid]} => LEFT half sorted')
else:
print(f'arr[{lo}]={arr[lo]} > arr[{mid}]={arr[mid]} => RIGHT half sorted')تحليل التعقيد
يظل البحث في مصفوفة مرتبة مدورة باستخدام البحث الثنائي بزمن O(log n) ومساحة O(1)، لأننا نستمر في تقسيم مساحة البحث إلى النصف في كل تكرار. والفرق الوحيد عن البحث الثنائي التقليدي هو إجراء فحص إضافي بزمن ثابت لتحديد أي النصفين مرتب.
مع وجود العناصر المكررة، يتدهور الأداء في أسوأ الحالات إلى O(n)، لأننا قد لا نزيد lo إلا بمقدار واحد في كل خطوة. اذكروا هذه المفاضلة صراحةً — فهذا يوضح للمحاوِر أنكم تفكرون في الحالات الحدية التي تتجاوز المسار المعتاد.
شرح تطبيقي للمسألة LeetCode 33
تُعد مسألة LeetCode 33، 'Search in Rotated Sorted Array'، الصيغة الأساسية لهذه المسألة. تضمن القيود عدم وجود عناصر مكررة ووجود تدوير واحد بالضبط. الحل هو الدالة search_rotated التي كتبناها سابقًا. ومن النقاط المهمة في المقابلة: اذكروا دائمًا افتراض عدم وجود عناصر مكررة، وتحققوا من صحة المتباينات باستخدام مثال فعلي عند الحدود، وتأكدوا من أن الفهرس المُعاد صحيح في حالتي العثور على الهدف وعدم العثور عليه.
# LeetCode 33 — complete solution
def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]: # left half sorted
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else: # right half sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
# Tests
print(search([4,5,6,7,0,1,2], 0)) # 4
print(search([4,5,6,7,0,1,2], 3)) # -1
print(search([1], 0)) # -1LeetCode 153: العثور على الأصغر دون تكرار
تطلب مسألة LeetCode 153، 'Find Minimum in Rotated Sorted Array'، العثور على العنصر الأصغر من دون عناصر مكررة. تتمثل الطريقة في مقارنة arr[mid] بـ arr[hi] (وليس بـ arr[lo]) لتحديد الجهة التي يوجد فيها العنصر الأصغر. إذا كان arr[mid] > arr[hi]، فالعنصر الأصغر موجود إلى اليمين؛ وإلا فهو عند mid أو إلى اليسار. تتقارب هذه العملية إلى العنصر الأصغر في O(log n).
def findMin(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] > nums[hi]:
lo = mid + 1
else:
hi = mid
return nums[lo]
print(findMin([3,4,5,1,2])) # 1
print(findMin([4,5,6,7,0,1,2])) # 0
print(findMin([11,13,15,17])) # 11عدد مرات التدوير وفهرس المحور
بمجرد أن تتمكن من العثور على العنصر الأصغر، ستعرف أيضًا عدد مرات التدوير: ففهرس العنصر الأصغر يساوي تمامًا عدد المواضع التي دُوّرت بها المصفوفة نحو اليمين. على سبيل المثال، في [4,5,6,7,0,1,2] يوجد العنصر الأصغر عند الفهرس 4، ولذلك دُوّرت المصفوفة أربع مواضع.
يتيح لك معرفة المحور تطبيق البحث الثنائي القياسي مع التعامل مع الفهارس وفقًا لباقي القسمة على n: real_idx = (mid + pivot) % n. وقد تصوغ هذه الطريقة البديلة عملية التفكير بصورة أبسط عند التعامل مع بُنى ذات فهرسة دائرية.
def search_via_pivot(nums, target):
n = len(nums)
# Find pivot (index of minimum)
lo, hi = 0, n - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] > nums[hi]:
lo = mid + 1
else:
hi = mid
pivot = lo
# Binary search with offset
lo, hi = 0, n - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
real_mid = (mid + pivot) % n
if nums[real_mid] == target:
return real_mid
elif nums[real_mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
print(search_via_pivot([4,5,6,7,0,1,2], 0)) # 4جمع الأجزاء كلها
عندما تواجهون مسألة عن مصفوفة مدورة في مقابلة، اتبعوا شجرة القرارات التالية. أولًا، حدّدوا ما إذا كنتم بحاجة إلى العثور على هدف أو العثور على العنصر الأصغر. للعثور على هدف، استخدموا طريقة تحديد النصف المرتب. وللعثور على العنصر الأصغر، قارنوا mid بـ hi. إذا كانت العناصر المكررة ممكنة، اذكروا أن أسوأ حالة هي O(n)، وأضيفوا إجراءً احتياطيًا لتقليص الحدود.
تدرّبوا على تتبّع تنفيذ الكود باستخدام الأمثلة الكلاسيكية الثلاثة: من دون تدوير، والتدوير مرة واحدة، والتدوير بحيث يصبح العنصر الأصغر في الموضع الأخير.
اختبار سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلّمتم في هذا الدرس أن: المصفوفة المرتبة المدورة تحتوي دائمًا على نصف مرتب واحد على الأقل، ومقارنة arr[lo] بـ arr[mid] تحدد النصف المرتب قبل اتخاذ قرار بشأن موضع البحث، والعثور على العنصر الأصغر يستخدم arr[mid] مقارنةً بـ arr[hi] لتحديد محور التدوير. سنتناول بعد ذلك صيغتي البحث الثنائي للحد الأدنى والحد الأعلى.
الأسئلة الشائعة
هل درس «البحث الثنائي في المصفوفات المدورة وغير المرتبة» مجاني؟
نعم — نص درس «البحث الثنائي في المصفوفات المدورة وغير المرتبة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «البحث الثنائي في المصفوفات المدورة وغير المرتبة»؟
حل search-in-rotated-sorted-array وfind-minimum-in-rotated-array بتحديد النصف المرتب في كل خطوة تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «البحث الثنائي في المصفوفات المدورة وغير المرتبة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- البحث الثنائي الكلاسيكي: اليسار واليمين والوسط
- البحث الثنائي في المصفوفات المدورة وغير المرتبة
- الحد الأدنى والحد الأعلى
- البحث الثنائي في فضاء الإجابات