0Pricing
Coding Interview Prep · درس

الفرز السريع واختيار المحور

أنشئ الفرز السريع باستخدام مخططي تقسيم Lomuto وHoare، وناقش الحالة الأسوأ O(n²) وكيف يخفف اختيار المحور العشوائي من أثرها

الفرز السريع واختيار المحور درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

الفرز السريع: التقسيم والغزو في المكان نفسه

يُعد الفرز السريع خوارزمية الفرز الأكثر استخدامًا عمليًا. وعلى خلاف فرز الدمج، يفرز البيانات في المكان نفسه من دون تخصيص مصفوفات إضافية. والفكرة الأساسية هي اختيار عنصر محورًا، ثم تقسيم المصفوفة بحيث تسبقه جميع العناصر الأصغر منه وتليه جميع العناصر الأكبر منه، ثم فرز كل قسم تكراريًا. تستغرق خطوة التقسيم O(n) من الزمن، ومع اختيار محور جيد يكون عمق التكرار O(log n).

def quick_sort(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo < hi:
        pivot_idx = partition(arr, lo, hi)
        quick_sort(arr, lo, pivot_idx - 1)  # sort left
        quick_sort(arr, pivot_idx + 1, hi)  # sort right

def partition(arr, lo, hi):
    pivot = arr[hi]  # Lomuto: choose last element as pivot
    i = lo - 1
    for j in range(lo, hi):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i+1], arr[hi] = arr[hi], arr[i+1]
    return i + 1

arr = [3, 6, 8, 10, 1, 2, 1]
quick_sort(arr)
print(arr)  # [1, 1, 2, 3, 6, 8, 10]

مخطط تقسيم Lomuto

يستخدم تقسيم Lomuto العنصر الأخير محورًا. ويتتبع مؤشر بطيء i حدود منطقة «العناصر الأصغر من المحور»، بينما يمسح مؤشر سريع j العناصر باتجاه الأمام. عندما يتحقق arr[j] <= pivot، زِد قيمة i وبدّل بين arr[i] وarr[j]، وبذلك توسّع منطقة العناصر الصغيرة. بعد انتهاء المسح، ضع المحور في الموضع i+1 من خلال تبديله مع arr[hi]. هذا المخطط سهل التنفيذ، لكنه يجري عمليات تبديل أكثر بثلاثة أضعاف من مخطط Hoare.

def lomuto_partition_traced(arr, lo, hi):
    pivot = arr[hi]
    i = lo - 1
    print(f'Pivot: {pivot}, array: {arr[lo:hi+1]}')
    for j in range(lo, hi):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i+1], arr[hi] = arr[hi], arr[i+1]
    print(f'After partition: {arr[lo:hi+1]}')
    return i + 1

arr = [3, 1, 4, 1, 5, 9, 2, 6]
lomuto_partition_traced(arr, 0, len(arr)-1)

مخطط تقسيم Hoare

يستخدم تقسيم Hoare مؤشرين يبدأ كل منهما عند أحد الطرفين ويتحركان نحو الداخل حتى يتجاوز أحدهما الآخر. ويختار المحور (عادةً العنصر الأول)، ثم ينقل العناصر الأصغر من المحور إلى اليسار والأكبر منه إلى اليمين. ويجري مخطط Hoare عمليات تبديل أقل بثلاثة أضعاف من Lomuto، كما يتعامل بصورة أفضل مع العناصر المتساوية، لكن المحور لا يصل إلى موضعه النهائي بعد التقسيم، ولذلك يتطلب استدعاءات تكرارية مختلفة قليلًا.

def hoare_partition(arr, lo, hi):
    pivot = arr[lo]  # first element as pivot
    i, j = lo - 1, hi + 1
    while True:
        i += 1
        while arr[i] < pivot: i += 1
        j -= 1
        while arr[j] > pivot: j -= 1
        if i >= j: return j
        arr[i], arr[j] = arr[j], arr[i]

def quick_sort_hoare(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo < hi:
        p = hoare_partition(arr, lo, hi)
        quick_sort_hoare(arr, lo, p)      # note: p not p-1
        quick_sort_hoare(arr, p+1, hi)

arr = [3, 6, 8, 10, 1, 2, 1]
quick_sort_hoare(arr)
print(arr)  # [1, 1, 2, 3, 6, 8, 10]

أسوأ حالة O(n²): إدخال مرتب مسبقًا

تحدث أسوأ حالات الفرز السريع عندما يكون المحور باستمرار أصغر عنصر أو أكبر عنصر في القسم. وعند استخدام محور العنصر الأخير في Lomuto مع مصفوفة مرتبة مسبقًا، يضع التقسيم دائمًا 0 من العناصر إلى اليسار وn-1 إلى اليمين؛ فتتحول شجرة الاستدعاءات التكرارية إلى سلسلة بعمق n، ما يعطي O(n²) من المقارنات. ولهذا يُعد اختيار المحور أمرًا بالغ الأهمية، ولهذا تختار تطبيقات الإنتاج المحور عشوائيًا.

import sys
sys.setrecursionlimit(5000)

def quick_sort_naive(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    comparisons = [0]
    def _qs(lo, hi):
        if lo >= hi: return
        pivot = arr[hi]  # last element pivot
        i = lo - 1
        for j in range(lo, hi):
            comparisons[0] += 1
            if arr[j] <= pivot:
                i += 1; arr[i], arr[j] = arr[j], arr[i]
        arr[i+1], arr[hi] = arr[hi], arr[i+1]
        p = i + 1
        _qs(lo, p-1); _qs(p+1, hi)
    _qs(lo, hi)
    return comparisons[0]

import math
n = 100
sorted_arr = list(range(n))
ops = quick_sort_naive(sorted_arr)
print(f'n={n}, ops={ops}, n^2={n**2}')  # ops close to n*(n-1)/2

محور عشوائي: O(n log n) متوقع

باختيار المحور عشوائيًا بالتوزيع المنتظم (بدّل عنصرًا عشوائيًا مع arr[hi] قبل التقسيم)، تتناقص احتمالية اختيار محاور سيئة باستمرار بصورة أسية. ويبلغ العدد المتوقع للمقارنات 2n ln(n) ≈ 1.39 n log₂(n)، ما يعطي زمنًا متوقعًا قدره O(n log n) في الغالبية الساحقة من الحالات. ولهذا يُستخدم الفرز السريع العشوائي عمليًا، إذ يتجنب أسوأ الحالات المرضية التي يمكن لخصم أن يصممها لاستراتيجيات المحور الثابت.

import random

def quick_sort_random(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo < hi:
        # Randomise pivot
        rand_i = random.randint(lo, hi)
        arr[rand_i], arr[hi] = arr[hi], arr[rand_i]
        # Lomuto partition with last element as pivot
        pivot = arr[hi]
        i = lo - 1
        for j in range(lo, hi):
            if arr[j] <= pivot:
                i += 1; arr[i], arr[j] = arr[j], arr[i]
        arr[i+1], arr[hi] = arr[hi], arr[i+1]
        p = i + 1
        quick_sort_random(arr, lo, p - 1)
        quick_sort_random(arr, p + 1, hi)

arr = list(range(100, 0, -1))  # worst case for naive
quick_sort_random(arr)
print(arr[:10])  # [1,2,3,4,5,6,7,8,9,10]

محور وسيط العناصر الثلاثة

تتمثل استراتيجية أخرى للمحور في اختيار وسيط العنصر الأول والأوسط والأخير. ويجنب ذلك السلوك الأسوأ مع المدخلات المرتبة أو المرتبة عكسيًا (وهي أكثر المدخلات الخصمية شيوعًا)، مع تجنب تكلفة توليد الأعداد العشوائية. وتستخدم العديد من تطبيقات الإنتاج وسيط العناصر الثلاثة أو ninther (وسيط ثلاثة أوساط) للمصفوفات الكبيرة، وتعود إلى فرز الإدراج للمصفوفات الفرعية الصغيرة التي تقل عن عتبة تقارب 10 عناصر.

def median_of_three(arr, lo, hi):
    mid = (lo + hi) // 2
    # Sort lo, mid, hi values in place
    if arr[lo] > arr[mid]:  arr[lo], arr[mid] = arr[mid], arr[lo]
    if arr[lo] > arr[hi]:   arr[lo], arr[hi]  = arr[hi],  arr[lo]
    if arr[mid] > arr[hi]:  arr[mid], arr[hi] = arr[hi],  arr[mid]
    # Median is now at arr[mid]; swap to arr[hi-1] as pivot
    arr[mid], arr[hi] = arr[hi], arr[mid]
    return arr[hi]  # pivot value

arr = [3, 9, 1]
print(median_of_three(arr, 0, 2), arr)  # 3, [1,3,9] (sorted)

العلم الوطني الهولندي: التقسيم ثلاثي الاتجاهات

يضع التقسيم القياسي العناصر الأصغر من المحور إلى اليسار والأكبر منه إلى اليمين، لكن العناصر المساوية للمحور تتوزع بينهما. ينشئ التقسيم ثلاثي الاتجاهات (العلم الوطني الهولندي) ثلاث مناطق: <المحور، ==المحور، >المحور. ويكتسب ذلك أهمية حاسمة للمصفوفات التي تحتوي على تكرارات كثيرة، إذ ينخفض أداء الفرز السريع القياسي فيها إلى O(n²)، بينما يحقق الفرز السريع ثلاثي الاتجاهات O(n) للمدخلات التي تتساوى فيها جميع القيم.

def three_way_partition(arr, lo, hi):
    pivot = arr[lo]
    lt = lo      # arr[lo..lt-1] < pivot
    gt = hi      # arr[gt+1..hi] > pivot
    i = lo       # current
    while i <= gt:
        if arr[i] < pivot:
            arr[lt], arr[i] = arr[i], arr[lt]
            lt += 1; i += 1
        elif arr[i] > pivot:
            arr[i], arr[gt] = arr[gt], arr[i]
            gt -= 1  # don't advance i
        else:
            i += 1
    return lt, gt  # pivot occupies arr[lt..gt]

arr = [3, 1, 4, 1, 5, 9, 2, 6, 3, 3]
lt, gt = three_way_partition(arr, 0, len(arr)-1)
print(arr, '| pivot region:', lt, 'to', gt)

Quickselect: إيجاد العنصر الأصغر من المرتبة k في O(n)

يستخدم Quickselect خطوة التقسيم في الفرز السريع للعثور على العنصر الأصغر من المرتبة k في زمن متوسط قدره O(n)، من دون فرز المصفوفة بالكامل. بعد التقسيم، يكون المحور في موضعه النهائي p. إذا كان p == k، فأعد arr[p]. وإذا كان k < p، فنفّذ التكرار على القسم الأيسر، وإذا كان k > p، فنفّذه على القسم الأيمن. في المتوسط، تنصّف كل جولة تكرارية المسألة: O(n) + O(n/2) + O(n/4) + ... = O(2n) = O(n).

import random

def quickselect(nums, k):
    '''Find kth smallest (0-indexed) in O(n) average.'''
    def _select(lo, hi):
        if lo == hi: return nums[lo]
        rand_i = random.randint(lo, hi)
        nums[rand_i], nums[hi] = nums[hi], nums[rand_i]
        pivot = nums[hi]
        i = lo - 1
        for j in range(lo, hi):
            if nums[j] <= pivot:
                i += 1; nums[i], nums[j] = nums[j], nums[i]
        p = i + 1
        nums[p], nums[hi] = nums[hi], nums[p]
        if p == k:    return nums[p]
        elif k < p:   return _select(lo, p - 1)
        else:         return _select(p + 1, hi)
    return _select(0, len(nums) - 1)

print(quickselect([3,2,1,5,6,4], 1))  # 2  (2nd smallest)

التعقيد المكاني للفرز السريع

يُسمّى الفرز السريع «في المكان نفسه»، لكنه يستخدم مساحة مكدس متوسطة قدرها O(log n) للتكرار (إطار واحد لكل مستوى من مستويات شجرة الاستدعاءات التكرارية). وفي أسوأ الحالات، يبلغ عمق المكدس O(n). ولضمان مساحة مكدس قدرها O(log n) في أسوأ الحالات، نفّذ التكرار دائمًا على القسم الأصغر أولًا، واستخدم تحسين استدعاء الذيل للقسم الأكبر. ويجعل حد التكرار في Python عمليات التكرار العميقة جدًا للفرز السريع محفوفة بالمخاطر، ومن المفيد ذكر ذلك في المقابلات.

def quick_sort_optimised(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    while lo < hi:
        p = lomuto_partition_qs(arr, lo, hi)
        # Recurse on smaller partition; iterate on larger
        if p - lo < hi - p:
            quick_sort_optimised(arr, lo, p - 1)
            lo = p + 1  # tail-call elimination
        else:
            quick_sort_optimised(arr, p + 1, hi)
            hi = p - 1

def lomuto_partition_qs(arr, lo, hi):
    pivot = arr[hi]; i = lo - 1
    for j in range(lo, hi):
        if arr[j] <= pivot: i += 1; arr[i], arr[j] = arr[j], arr[i]
    arr[i+1], arr[hi] = arr[hi], arr[i+1]
    return i + 1

مقارنة خوارزميات الفرز

لخّص معرفتك:

  • الفرز السريع: O(n log n) متوقع، وأسوأ حالاته O(n²)، ومساحته O(log n)، وغير مستقر، وهو الأسرع عمليًا للبيانات العشوائية
  • فرز الدمج: يضمن O(n log n)، ومساحته O(n)، ومستقر، وهو الأفضل للقوائم المرتبطة والفرز الخارجي
  • فرز الكومة: يضمن O(n log n)، ومساحته O(1)، وغير مستقر، وأبطأ عمليًا بسبب إخفاقات الذاكرة المخبئية
  • فرز الإدراج: يحقق O(n) في أفضل الحالات، وهو مثالي للقيم الصغيرة لـ n أو للبيانات المرتبة تقريبًا
في المقابلات، برّر اختيارك استنادًا إلى هذه المفاضلات.

# Python's sorted() uses Timsort:
# - Hybrid: merge sort for large runs, insertion sort for small (< 64 elements)
# - Stable, O(n log n) worst case
# - O(n) best case for sorted/reverse-sorted/nearly-sorted
# - O(n) extra space

import random
arr = random.sample(range(10000), 1000)
sorted_arr = sorted(arr)  # Timsort
print(sorted_arr[:5], '...')  # first 5 elements

Introsort: الجمع بين الخوارزميات الثلاث

يجمع Introsort (المستخدم في C++ STL std::sort) بين الفرز السريع وفرز الكومة وفرز الإدراج: ابدأ بفرز سريع عشوائي؛ وإذا تجاوز عمق التكرار 2 log n (ما يشير إلى سلسلة محاور سيئة)، فانتقل إلى فرز الكومة لضمان O(n log n)؛ واستخدم فرز الإدراج للمصفوفات الفرعية التي يقل عدد عناصرها عن 16. ويحقق ذلك أسوأ حالة قدرها O(n log n)، مع سرعة الفرز السريع في الحالات المتوسطة وكفاءة فرز الإدراج للمصفوفات الفرعية الصغيرة.

# Introsort hybrid (simplified)
def introsort(arr, depth_limit=None):
    if depth_limit is None:
        import math
        depth_limit = 2 * int(math.log2(len(arr) + 1)) if arr else 0
    if len(arr) <= 16:
        # insertion sort for small arrays
        for i in range(1, len(arr)):
            key = arr[i]; j = i - 1
            while j >= 0 and arr[j] > key:
                arr[j+1] = arr[j]; j -= 1
            arr[j+1] = key
        return arr
    if depth_limit == 0:
        arr.sort()  # fall back to heapsort equivalent
        return arr
    # Otherwise quick sort
    pivot = arr[-1]
    small = [x for x in arr[:-1] if x <= pivot]
    large = [x for x in arr[:-1] if x > pivot]
    return introsort(small, depth_limit-1) + [pivot] + introsort(large, depth_limit-1)

print(introsort([5,3,8,1,9,2,7]))

اختبار سريع

اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلّمت في هذا الدرس أن الفرز السريع يقسّم البيانات في المكان نفسه حول محور، وينفّذ التكرار على كل جانب، محققًا زمنًا متوقعًا قدره O(n log n) مع مساحة مكدس قدرها O(log n)، وهو أسرع عمليًا من فرز الدمج للبيانات العشوائية، وأن أسوأ حالة O(n²) تحدث مع الإدخال المرتب عند استخدام محور ثابت، ويمكن تجنبها باختيار المحور عشوائيًا أو باستخدام وسيط العناصر الثلاثة، وأن التقسيم ثلاثي الاتجاهات يتعامل بكفاءة مع العناصر المكررة، كما توسّع Quickselect فكرة التقسيم للعثور على العنصر الأصغر من المرتبة k في زمن متوسط قدره O(n) من دون فرز كامل. سنستكشف بعد ذلك خوارزميات الفرز غير المعتمدة على المقارنة وفرز Python المدمج.

الأسئلة الشائعة

هل درس «الفرز السريع واختيار المحور» مجاني؟

نعم — نص درس «الفرز السريع واختيار المحور» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «الفرز السريع واختيار المحور»؟

أنشئ الفرز السريع باستخدام مخططي تقسيم Lomuto وHoare، وناقش الحالة الأسوأ O(n²) وكيف يخفف اختيار المحور العشوائي من أثرها تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «الفرز السريع واختيار المحور»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. فرز الفقاعات وفرز الإدراج
  2. فرز الدمج: قسّم وفرز وادمج
  3. الفرز السريع واختيار المحور
  4. فرز بلا مقارنة ودالة sort() في Python
← العودة إلى Coding Interview Prep