الفرز السلس


نواصل الانغماس في مجموعة متنوعة من أكوام.

سنحلل اليوم طريقة ترتيب أنيقة تستخدم أكوامًا خاصة استنادًا إلى أرقام ليوناردو.

سمع الكثير عن هذا التصنيف ، لكن قلة من الناس يعرفون بالضبط كيف يعمل. اليوم سنرى أنه لا يوجد شيء معقد فيه. تم اختراع هذه الطريقة من قبل الأسطوري Edsger Dijkstra. بالإضافة إلى العديد من الإنجازات الساطعة في نظرية الخوارزميات ، فهو أيضًا مؤلف بيان ذكي مثل: "الطلاب الذين درسوا سابقًا Basic ، يكاد يكون من المستحيل تعليم البرمجة الجيدة. كمبرمجين محتملين ، تعرضوا لتدهور عقلي لا رجعة فيه ". آمل أنه ليس من الكفر أن الرسوم المتحركة في المقالة تم إنشاؤها باستخدام VBA :-)







إديسون برمجيات - تطوير الويب
EDISON.

, Android iOS.

! ;-)

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

بشكل عام ، كان هناك سؤال على جدول الأعمال: هل من الممكن أن نفكر بحيث لا يكون التعقيد الزمني للفرز بواسطة كومة ، من ناحية ، أقل منO ( n log n ) ، ولكن في سيناريو موات (على وجه الخصوص ، إذا تمت معالجة مصفوفة تم فرزها تقريبًا) زادت إلى O ( n ) ؟

تم معالجة هذه المشكلة شخصيًا بواسطة Edsger Dijkstra نفسه ، الذي اكتشف أن نعم ، هذا ممكن.

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

ما هو الخطأ في كومة الذاكرة المؤقتة الثنائية


دعنا نلقي نظرة على كيفية فرز heapsort لمجموعة مرتبة تقريبًا ونرى لماذا لا تعالج هذه الخوارزمية هذه البيانات الواردة بشكل أسرع.


انقر على الرسم المتحرك للانتقال إلى مقالة "الفرز حسب هرم n".

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

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

أرقام ليوناردو


لحل كلا المشكلتين ، اقترح Dijkstra استخدام أكوام ثنائية خاصة تعتمد على أرقام ليوناردو.

أرقام ليوناردو تشبه تقريبًا أرقام فيبوناتشي ، لكنها أفضل.
يتم إعطاء سلسلة من أرقام Leonardo بشكل متكرر:

L 0 = 1
L 1 = 1
L n = L n - 1 + L n - 2 + 1

أول 20 رقم ليوناردو:
1 ، 1 ، 3 ، 5 ، 9 ، 15 ، 25 ، 41 ، 67 ، 109 ، 177 ، 287 ، 465 ، 753 ، 1219 ، 1973 ، 3193 ، 5167 ، 8361 ، 13529 على

الإطلاق يمكن تمثيل أي عدد صحيح كمجموع أرقام ليوناردو التي تحتوي على أرقام تسلسلية مختلفة.

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

فيما يلي مثال لصفيف من العنصر 21 ، يتكون من ثلاثة أكوام ليونارد. في كل كومة ، يتطابق عدد العقد مع أي عدد من ليوناردو.


نقاط مهمة يجب معرفتها:

  1. كل كومة ليوناردوف هي شجرة ثنائية غير متوازنة.
  2. جذر كل كومة هو العنصر الأخير (وليس الأول ، كما في كومة الذاكرة المؤقتة العادية) من المصفوفة الفرعية المقابلة.
  3. أي عقدة مع جميع أحفادها هي أيضًا كومة ليونارد ذات ترتيب أصغر.

بناء وتفكيك أكوام


في صيغة التكرار لأرقام ليوناردو ،

L n = L n - 1 + L n - 2 + 1

مسرور جدًا بالوحدة في النهاية.

ولهذا السبب. افترض أن لدينا صفين فرعيين متجاورين في الصفيف يتوافقان مع أكوام مبنية على رقمين متجاورين من ليوناردو. باستخدام العنصر مباشرة بعد هذه المصفوفات الفرعية ، يمكن دمج هذه المصفوفات الفرعية في كومة مشتركة ، والتي تتوافق مع رقم ليونارد التالي.



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

في المرحلة الثانية من الخوارزمية ، تحدث العملية العكسية - نقوم بتحليل أكوام. إذا قمنا بإزالة الجذر في كومة الذاكرة المؤقتة ، فإننا نحصل على كومة كومة أصغر تتوافق مع رقمي ليوناردو السابقين. يمكن القيام بذلك للأسباب التالية:

L n - 1 = L n - 1+ L n - 2

في أرقام فيبوناتشي لا توجد مثل هذه الوحدة المفيدة ، لذلك لا نستخدم كومة فيبوناتشي.

الفرز السلس :: الفرز السلس


الخوارزمية النهائية:

  • 1. نقوم بإنشاء مجموعة من أكوام Leonard من المصفوفة ، كل منها عبارة عن شجرة فرز.
    • I.1. كرر فوق عناصر المصفوفة من اليسار إلى اليمين.
    • II.1. تحقق مما إذا كان العنصر الحالي يمكن أن يجمع بين كومة الذاكرة المؤقتة الموجودة في أقصى اليسار في كومة الذاكرة المؤقتة الموجودة في كومة Leonard:
      • II.1.a. إذا كانت الإجابة بنعم ، فإننا نجمع الكمينين في أقصى اليسار في عنصر واحد ، ويصبح العنصر الحالي هو جذر هذا الكومة ، ونقوم بعمل شبكة للكومة المجمعة.
      • II.1.b. إذا لم يكن الأمر كذلك ، فأضف العنصر الحالي ككومة جديدة (تتكون حتى الآن من عقدة واحدة) إلى كومة الذاكرة المؤقتة الموجودة في Leonard.
  • II. , :
    • II.1. . , .
    • II.2. ( ) ( ).
    • II.3. , . .
    • II.4. ( ), .
    • II.5. بعد تحريك العنصر الأقصى إلى النهاية ، زاد الجزء المصنف من الصفيف ، وانخفض الجزء غير المصنف. كرر الخطوات من II.1-II.4 للجزء المتبقي غير المصنف من الصفيف.



مثال على تطبيق Python


import random

def smoothsort(lst):

    #    
    leo_nums = leonardo_numbers(len(lst))


    #       
    heap = []

    #   
    #       
    #       
    for i in range(len(lst)):
        if len(heap) >= 2 and heap[-2] == heap[-1] + 1:
            heap.pop()
            heap[-1] += 1
        else:
            if len(heap) >= 1 and heap[-1] == 1:
                heap.append(0)
            else:
                heap.append(1)
        restore_heap(lst, i, heap, leo_nums)

    #  
    for i in reversed(range(len(lst))):
        if heap[-1] < 2:
            heap.pop()
        else:
            k = heap.pop()
            t_r, k_r, t_l, k_l = get_child_trees(i, k, leo_nums)
            heap.append(k_l)
            restore_heap(lst, t_l, heap, leo_nums)
            heap.append(k_r)
            restore_heap(lst, t_r, heap, leo_nums)

#   ,     
def leonardo_numbers(hi):

    a, b = 1, 1
    numbers = []
    while a <= hi:
        numbers.append(a)
        a, b = b, a + b + 1
    return numbers

#        
def restore_heap(lst, i, heap, leo_nums):
    
    #      
    
    current = len(heap) - 1
    k = heap[current]

    while current > 0:
        j = i - leo_nums[k]
        if (lst[j] > lst[i] and
            (k < 2 or lst[j] > lst[i-1] and lst[j] > lst[i-2])):
            lst[i], lst[j] = lst[j], lst[i]
            i = j
            current -= 1
            k = heap[current]
        else:
            break

    # 
    
    while k >= 2:
        t_r, k_r, t_l, k_l = get_child_trees(i, k, leo_nums)
        if lst[i] < lst[t_r] or lst[i] < lst[t_l]:
            if lst[t_r] > lst[t_l]:
                lst[i], lst[t_r] = lst[t_r], lst[i]
                i, k = t_r, k_r
            else:
                lst[i], lst[t_l] = lst[t_l], lst[i]
                i, k = t_l, k_l
        else:
            break

#         ,
#     
def get_child_trees(i, k, leo_nums):

    t_r, k_r = i - 1, k - 2
    t_l, k_l = t_r - leo_nums[k_r], k - 1
    return t_r, k_r, t_l, k_l

#  
def main(n):
    lst = list(range(n))
    random.shuffle(lst)
    print(lst)
    smoothsort(lst)
    print(lst)

تعقيد الوقت


إذا أخذنا مصفوفة مرتبة تقريبًا كمدخلات ، فإن التصور يوضح سبب معالجة مثل هذا الصفيف بشكل أسرع.



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

دعونا نقدر التعقيد الكلي للوقت.

في المرحلة الأولى ، نكرر أكثر من n من العناصر ، ونضيفها إلى أكوام موجودة بالفعل على اليسار. يتم تجاوز الإضافة إلى كومة الذاكرة المؤقتة نفسها في O (1) ، ولكن بعد ذلك تحتاج إلى كومة فرز. في البيانات المرتبة ، غالبًا ما تكلف عملية الغربلة الضحلة O (1) لعنصر واحد تمت إضافته إلى الكومة. في البيانات غير المرتبة ، يتم حساب تكلفة غربلة كل إضافة في O (تسجيل n )نظرًا لأنه نتيجة للعشوائية ، يجب أن تمر الغربلة بمستويات الشجرة في كثير من الأحيان إلى القاع.

لذلك ، في المرحلة الأولى ، يكون أفضل تعقيد زمني هو:
للبيانات المرتبة تقريبًا - O ( n ) ،
للبيانات العشوائية - O ( n log n ).

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

في المرحلة الثانية ، يكون أفضل تعقيد زمني هو نفسه كما في الأولى:
للبيانات المرتبة تقريبًا - O ( n ) ،
للبيانات العشوائية - O ( n log n ).

إضافة تعقيد الوقت للمرحلتين الأولى والثانية:
للبيانات المرتبة تقريبًا - O (2 n ) = O ( n ) ،
للبيانات العشوائية - O (2 n log n ) = O ( n log n ).

بشكل عام ، أسوأ وتعقيد متوسط ​​الوقت للفرز السلس هو O ( n log n ).
أثبت Dijkstra في حساباته (التي لن أتحملها) أن أفضل تعقيد يميل بسلاسة إلى O ( n ) من البيانات الأكثر طلبًا الواردة. ومن هنا جاء الاسم - الفرز السلس.

تعقيد الذاكرة الزائدة


لتحليل البيانات إلى مجموعة من أكوام Leonard ، تحتاج فقط إلى تذكر أرقام Leonardo التي يتم تضمينها في كل خطوة. بمعرفة هذه الأرقام ، يتم محاذاة أكوام نفسها خوارزمية. تنمو سلسلة الأرقام هذه بسرعة كبيرة ، لذلك حتى بالنسبة للمصفوفات الكبيرة ، ستحتاج إلى مجموعة صغيرة جدًا من أرقام Leonard.

فرز كومة ذات الحدين :: فرز كومة ذي الحدين


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

من حيث المبدأ ، يمكنك إجراء فرز سلس بناءً على القيم الثنائية:



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

من غير المعروف ما إذا كان كومة Dijkstra ذات الحدين يُنظر إليها عمومًا كأساس محتمل لخوارزميتها. مع ذلك ، ربما تكون كومة Leonardov أكثر مثالية.

مقطورة السلسلة التالية


ومع ذلك ، حتى إذا لم يكن كومة ذات الحدين الخيار الأفضل للفرز السلس ، فلا يجب التخلص منه تمامًا.

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


انقر على الرسم المتحرك للانتقال إلى المقالة مع الفرز التالي حسب الكومة.

المراجع


والسلس / السلس

عدد يوناردو ، ذات الحدين كومة / كومة ذات الحدين

مقالات سلسلة:



تمت إضافة التصنيف السلس اليوم إلى تطبيق AlgoLab. بالإضافة إلى المكافأة - والفرز مع كومة ذات الحدين. لذا من يريد أن يقود البيانات على أكوام الذاكرة الشخصية - قم بتحديث ملف Excel باستخدام وحدات الماكرو.

All Articles