14-bo‘lim

Samarali saralash algoritmlari

Merge sort va quick sort - bo'l va hukmronlik qil yondashuvi, ularning taqqoslanishi va Python Timsort algoritmi.

🕑 11 daqiqa o‘qish 📄 721 so‘z 👁 6 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Bo'l va hukmronlik qil
  2. Merge Sort (birlashtirish bilan saralash)
  3. Birlashtirish qanday ishlaydi?
  4. Quick Sort (tez saralash)
  5. Merge sort va Quick sort
  6. Timsort - Python algoritmi
  7. Saralash chegarasi
  8. Xulosa

O(n²) algoritmlar 10 000 ta elementda ham qiynaladi. Endi O(n log n) algoritmlarga o'tamiz - ular millionlab elementni bir soniyada saralaydi.

Bo'l va hukmronlik qil #

Ikkala algoritm ham bitta g'oyaga asoslanadi:

  1. Bo'l - masalani kichikroq bo'laklarga ajrat
  2. Hukmronlik qil - har bir bo'lakni rekursiv yech
  3. Birlashtir - natijalarni yig'
[5, 2, 8, 1, 9, 3] [5, 2, 8] [1, 9, 3] [5] [2, 8] [1] [9, 3] ↓ birlashtiramiz ↓ [2, 5, 8] [1, 3, 9] [1, 2, 3, 5, 8, 9]
Massiv bitta elementgacha bo'linadi, so'ng tartibli holda birlashtiriladi

Merge Sort (birlashtirish bilan saralash) #

Python
def birlashtirib_sarala(massiv):
    """Merge sort. Kafolatlangan O(n log n), barqaror, O(n) qo'shimcha xotira."""
    if len(massiv) <= 1:
        return list(massiv)

    orta = len(massiv) // 2
    chap = birlashtirib_sarala(massiv[:orta])
    ong = birlashtirib_sarala(massiv[orta:])

    return _birlashtir(chap, ong)


def _birlashtir(chap, ong):
    """Ikkita tartiblangan ro'yxatni bittaga birlashtiradi. O(n)."""
    natija = []
    i = j = 0

    while i < len(chap) and j < len(ong):
        if chap[i] <= ong[j]:          # <= barqarorlikni ta'minlaydi
            natija.append(chap[i])
            i += 1
        else:
            natija.append(ong[j])
            j += 1

    natija.extend(chap[i:])            # qolganini qo'shamiz
    natija.extend(ong[j:])
    return natija


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

Birlashtirish qanday ishlaydi? #

Ikkita tartiblangan ro'yxatning boshidagi elementlarni solishtirib, kichigini olamiz:

Python
def birlashtirishni_korsat(chap, ong):
    natija = []
    i = j = 0

    while i < len(chap) and j < len(ong):
        if chap[i] <= ong[j]:
            print(f"  {chap[i]} <= {ong[j]}  ->  {chap[i]} olindi")
            natija.append(chap[i])
            i += 1
        else:
            print(f"  {chap[i]} >  {ong[j]}  ->  {ong[j]} olindi")
            natija.append(ong[j])
            j += 1

    qolgan = chap[i:] + ong[j:]
    if qolgan:
        print(f"  qolgani qo'shildi: {qolgan}")
    return natija + qolgan


print("Natija:", birlashtirishni_korsat([2, 5, 8], [1, 3, 9]))
Natija
  2 >  1  ->  1 olindi
  2 <= 3  ->  2 olindi
  5 >  3  ->  3 olindi
  5 <= 9  ->  5 olindi
  8 <= 9  ->  8 olindi
  qolgani qo'shildi: [9]
Natija: [1, 2, 3, 5, 8, 9]
Nima uchun O(n log n)?

Massivni bitta elementgacha bo'lish uchun log₂(n) daraja kerak (har safar ikkiga bo'linadi). Har bir darajada barcha n ta elementni birlashtiramiz - bu O(n). Natijada: log n daraja × n amal = O(n log n).

Quick Sort (tez saralash) #

Merge sort massivni o'rtadan bo'ladi. Quick sort esa tayanch element (pivot) tanlab, undan kichiklarni chapga, kattalarni o'ngga ajratadi.

Python
def tez_sarala(massiv):
    """Quick sort - sodda, o'qish oson variant. O'rtacha O(n log n)."""
    if len(massiv) <= 1:
        return list(massiv)

    tayanch = massiv[len(massiv) // 2]

    kichiklar = [x for x in massiv if x < tayanch]
    tenglar   = [x for x in massiv if x == tayanch]
    kattalar  = [x for x in massiv if x > tayanch]

    return tez_sarala(kichiklar) + tenglar + tez_sarala(kattalar)


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

Bu variant tushunarli, lekin qo'shimcha xotira sarflaydi. Joyida (in-place) ishlaydigan variant:

Python
def tez_sarala_joyida(massiv, chap=None, ong=None):
    """Quick sort - joyida ishlaydi, O(log n) xotira (stek uchun)."""
    if chap is None:
        massiv = list(massiv)
        chap, ong = 0, len(massiv) - 1

    if chap < ong:
        bolgich = _bol(massiv, chap, ong)
        tez_sarala_joyida(massiv, chap, bolgich - 1)
        tez_sarala_joyida(massiv, bolgich + 1, ong)

    return massiv


def _bol(massiv, chap, ong):
    """Lomuto usuli: oxirgi elementni tayanch qilib, massivni ikkiga ajratadi."""
    tayanch = massiv[ong]
    i = chap - 1

    for j in range(chap, ong):
        if massiv[j] <= tayanch:
            i += 1
            massiv[i], massiv[j] = massiv[j], massiv[i]

    massiv[i + 1], massiv[ong] = massiv[ong], massiv[i + 1]
    return i + 1


print(tez_sarala_joyida([5, 2, 8, 1, 9, 3, 7]))
Natija
[1, 2, 3, 5, 7, 8, 9]
Tayanch = 5 5 2 8 1 9 3 bo'lamiz 2 1 3 5 8 9 5 dan kichiklar o'z joyida! 5 dan kattalar Tayanch element bir marta joylashgach, boshqa ko'chirilmaydi
Har bo'lishda tayanch element o'zining yakuniy o'rniga tushadi
Quick sort ning eng yomon holati

Agar tayanch har doim eng kichik yoki eng katta element bo'lsa, massiv 1 va n-1 ga bo'linadi va murakkablik O(n²) ga tushadi. Bu tartiblangan massivda birinchi elementni tayanch qilib olsangiz sodir bo'ladi.

Yechimlar:

  • Tayanchni tasodifiy tanlash
  • Uchtaning o'rtachasi (birinchi, o'rta, oxirgi elementlardan medianasi)
Python
import random


def tez_sarala_tasodifiy(massiv):
    """Tasodifiy tayanch - eng yomon holat ehtimolini deyarli nolga tushiradi."""
    if len(massiv) <= 1:
        return list(massiv)

    tayanch = random.choice(massiv)
    kichiklar = [x for x in massiv if x < tayanch]
    tenglar   = [x for x in massiv if x == tayanch]
    kattalar  = [x for x in massiv if x > tayanch]

    return tez_sarala_tasodifiy(kichiklar) + tenglar + tez_sarala_tasodifiy(kattalar)


tartiblangan = list(range(1, 11))
print(tez_sarala_tasodifiy(tartiblangan))
Natija
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Merge sort va Quick sort #

XususiyatMerge SortQuick Sort
Eng yaxshiO(n log n)O(n log n)
O'rtachaO(n log n)O(n log n)
Eng yomonO(n log n)O(n²)
Qo'shimcha xotiraO(n)O(log n)
BarqarorHaYo'q
Amaldagi tezlikTezroq emasOdatda tezroq
Tashqi saralashMosMos emas
Qaysi birini tanlash kerak?
  • Kafolat kerakmi (eng yomon holat ham O(n log n))? → Merge sort
  • Barqarorlik kerakmi? → Merge sort
  • Xotira cheklanganmi va o'rtacha tezlik muhimmi? → Quick sort
  • Ma'lumot xotiraga sig'maydimi (fayl, baza)? → Merge sort

Amalda: C++ std::sort - introsort (quick sort + heap sort aralashmasi), Java obyektlar uchun - Timsort, primitivlar uchun - dual-pivot quicksort.

Timsort - Python algoritmi #

Python sorted() va list.sort() Timsort ishlatadi. Uni 2002-yilda Tim Peters aynan Python uchun yaratgan va keyinchalik Java, Android va Swift ham qabul qilgan.

G'oyasi: real ma'lumot hech qachon butunlay tasodifiy bo'lmaydi. Unda har doim tartiblangan bo'laklar bor.

BosqichNima qilinadi
1Massivda tabiiy tartiblangan bo'laklar ("run") topiladi
2Qisqa bo'laklar qo'shish usuli bilan uzaytiriladi
3Bo'laklar merge sort kabi birlashtiriladi
Python
import random
import time


def olcha(nomi, ma_lumot):
    boshlanish = time.perf_counter()
    sorted(ma_lumot)
    print(f"{nomi:<28}{time.perf_counter() - boshlanish:.5f} soniya")


N = 1_000_000
tasodifiy = random.sample(range(N * 2), N)
tartiblangan = sorted(tasodifiy)
teskari = tartiblangan[::-1]
deyarli = tartiblangan[:]
for _ in range(100):                       # 100 ta elementni aralashtiramiz
    i, j = random.randrange(N), random.randrange(N)
    deyarli[i], deyarli[j] = deyarli[j], deyarli[i]

olcha("Tasodifiy", tasodifiy)
olcha("Tartiblangan", tartiblangan)
olcha("Teskari tartiblangan", teskari)
olcha("Deyarli tartiblangan", deyarli)
Natija
Tasodifiy                   0.42718 soniya
Tartiblangan                0.00841 soniya
Teskari tartiblangan        0.01043 soniya
Deyarli tartiblangan        0.05219 soniya

Tartiblangan ma'lumotda 50 barobar tez - Timsort tayyor tartibni sezib, ishni tejaydi.

Saralash chegarasi #

Nima uchun O(n log n) dan tez saralab bo'lmaydi?

Solishtirishga asoslangan har qanday algoritm uchun O(n log n) - nazariy chegara. Isbot: n ta elementning n! ta joylashuvi bor. Har bir solishtirish variantlarni ikkiga bo'ladi, demak kamida log₂(n!) ≈ n log n ta solishtirish kerak.

Solishtirmaydigan algoritmlar (counting sort, radix sort) O(n) ga erisha oladi, lekin ular faqat maxsus ma'lumotda (masalan, chegaralangan butun sonlarda) ishlaydi.

Python
def sanash_bilan_sarala(sonlar, eng_katta):
    """Counting sort. O(n + k) - lekin faqat kichik butun sonlar uchun."""
    hisoblagich = [0] * (eng_katta + 1)

    for son in sonlar:
        hisoblagich[son] += 1

    natija = []
    for qiymat, soni in enumerate(hisoblagich):
        natija.extend([qiymat] * soni)

    return natija


baholar = [85, 92, 78, 95, 85, 78, 92, 100, 60]
print(sanash_bilan_sarala(baholar, 100))
Natija
[60, 78, 78, 85, 85, 92, 92, 95, 100]

Baholar 0-100 oralig'ida bo'lgani uchun bu O(n) ishlaydi - solishtirishsiz.

Amaliy topshiriq
  1. Merge sort ni joyida (in-place) ishlaydigan qilib yozishga urinib ko'ring. Nima qiyin?
  2. Quick sort ga "uchtaning o'rtachasi" tayanch tanlash usulini qo'shing.
  3. Merge sort yordamida massivdagi inversiyalar sonini sanang (tartibsiz juftliklar).
  4. radix sort algoritmini o'rganing va uni butun sonlar uchun yozing.

Xulosa #

  • Merge sort: kafolatlangan O(n log n), barqaror, lekin O(n) qo'shimcha xotira.
  • Quick sort: o'rtacha O(n log n) va amalda tezroq, lekin eng yomon holatda O(n²).
  • Quick sort da tayanchni tasodifiy tanlash eng yomon holatdan himoya qiladi.
  • Timsort (Python) real ma'lumotdagi tartiblangan bo'laklardan foydalanadi.
  • Solishtirishga asoslangan saralashning nazariy chegarasi - O(n log n).
  • Counting sort kabi maxsus algoritmlar chegaralangan ma'lumotda O(n) beradi.

Keyingi bo'limda tartiblangan ma'lumotda qidiruvni ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.