SEN2211 · 14. hafta · Sorting Algorithms II

Sıralama II: merge sort, quick sort, heap sort

Böl ve yönet ile O(n log n) sıralama: merge sort'ta birleştirme adımları, quick sort'ta partition izi, heap sort'a genel bakış ve bütün sıralamaların karşılaştırma tablosu. Final sınavının sıralama sorularının çoğu buradan.

divide and conquermerge sortmergeauxiliary arrayquick sortpivotpartitionLomuto partitionworst case O(n^2)heap sortmax-heapsift downstablein-place
  1. haftadaki üç algoritma O(n²): bir milyon elemanda yaklaşık 10¹² adım, saatler. Bu hafta böl ve yönet (divide and conquer) fikriyle O(n log n)‘e iniyoruz: aynı milyon eleman 2 · 10⁷ adım, saniyenin altında. İki ana algoritma var: merge sort işi birleştirme adımında yapar, quick sort işi bölme adımında yapar. Üçüncüsü heap sort, bir sonraki dersin veri yapısı olan heap’i kullanır; burada genel fikrini görüyoruz. Sınavda merge adımlarının sırası, partition izi ve karşılaştırma tablosu sorulur.

Altın kurallar:

  1. Merge sort: böl (kolay), birleştir (asıl iş). Her durumda O(n log n), O(n) ek bellek, kararlı.
  2. Quick sort: bölümle (asıl iş), birleştirme yok. Ortalama O(n log n), kötü pivotta O(n²).
  3. Partition sonrası pivot nihai yerindedir. İki taraf kendi içinde sırasızdır.
  4. İz yazarken hangi partition şemasının kullanıldığına bak. Bu sayfada Lomuto, pivot = son eleman.

Merge sort

Fikir: tek elemanlı dizi zaten sıralıdır. Diziyi ikiye böl, her yarıyı (özyinelemeyle) sırala, sonra iki sıralı yarıyı birleştir.

static void mergeSort(int[] a, int[] tmp, int lo, int hi) {
    if (lo >= hi) return;               // 0 ya da 1 eleman
    int mid = (lo + hi) / 2;
    mergeSort(a, tmp, lo, mid);
    mergeSort(a, tmp, mid + 1, hi);
    merge(a, tmp, lo, mid, hi);
}

static void merge(int[] a, int[] tmp, int lo, int mid, int hi) {
    int i = lo, j = mid + 1, k = lo;
    while (i <= mid && j <= hi) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];
        else              tmp[k++] = a[j++];
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= hi)  tmp[k++] = a[j++];
    for (k = lo; k <= hi; k++) a[k] = tmp[k];
}

merge, 4. haftada bağlı listede yazdığımız birleştirmenin dizi versiyonu. Fark: dizide yerinde birleştiremezsin, yardımcı bir diziye (tmp) yazıp geri kopyalarsın.

İz: {38, 27, 43, 3, 9, 82, 10}

                [38, 27, 43, 3, 9, 82, 10]
               /                          \
      [38, 27, 43, 3]                [9, 82, 10]
       /          \                   /        \
   [38, 27]     [43, 3]           [9, 82]      [10]
    /    \       /   \             /   \
  [38]  [27]   [43]  [3]         [9]  [82]

Birleştirmeler (program çıktısı, gerçekleştiği sırayla):

merge [38] + [27] -> [27, 38]
merge [43] + [3] -> [3, 43]
merge [27, 38] + [3, 43] -> [3, 27, 38, 43]
merge [9] + [82] -> [9, 82]
merge [9, 82] + [10] -> [9, 10, 82]
merge [3, 27, 38, 43] + [9, 10, 82] -> [3, 9, 10, 27, 38, 43, 82]
[3, 9, 10, 27, 38, 43, 82] comps=14

TUZAK · Birleştirme sırası seviye seviye değildir. Özyineleme sol tarafı tamamen bitirip sonra sağa geçer. [9, 82]‘nin birleşmesi, [3, 27, 38, 43] oluştuktan sonra gelir. “Önce bütün ikilileri birleştir” diye yazarsan sıra yanlış olur (o, aşağıdan yukarı merge sort’un sırasıdır).

Karmaşıklık

T(n)=2T(n/2)+nT(n) = 2T(n/2) + n → O(nlog⁡n)O(n \log n) (11. hafta). Bölme her zaman ortadan olduğu için en iyi, ortalama, en kötü aynı. Tek merge’de karşılaştırma sayısı en az küçük yarının boyu, en fazla n1+n2−1n_1 + n_2 - 1:

merge [1, 2, 3, 4] + [5, 6, 7, 8] -> [1, 2, 3, 4, 5, 6, 7, 8]
comps=4
merge [1, 3, 5, 7] + [2, 4, 6, 8] -> [1, 2, 3, 4, 5, 6, 7, 8]
comps=7

Quick sort

Fikir: bir pivot seç. Diziyi, pivottan küçükler solda ve büyükler sağda olacak şekilde yeniden düzenle (partition). Pivot artık nihai yerindedir. Sol ve sağ tarafı özyinelemeyle sırala. Birleştirme adımı yok; her şey yerinde olur.

Lomuto partition (pivot = son eleman)

static int partition(int[] a, int lo, int hi) {
    int pivot = a[hi];
    int i = lo - 1;                 // küçükler bölgesinin sonu
    for (int j = lo; j < hi; j++) {
        if (a[j] < pivot) {
            i++;
            int t = a[i]; a[i] = a[j]; a[j] = t;
        }
    }
    int t = a[i + 1]; a[i + 1] = a[hi]; a[hi] = t;   // pivotu yerine koy
    return i + 1;
}

static void quickSort(int[] a, int lo, int hi) {
    if (lo >= hi) return;
    int p = partition(a, lo, hi);
    quickSort(a, lo, p - 1);
    quickSort(a, p + 1, hi);
}

{7, 2, 9, 4, 3, 8, 5} üzerinde ilk partition, adım adım:

pivot=5 start [7, 2, 9, 4, 3, 8, 5]
j=0 a[j]>=pivot, skip     [7, 2, 9, 4, 3, 8, 5]
j=1 a[j]<pivot, i=0 swap -> [2, 7, 9, 4, 3, 8, 5]
j=2 a[j]>=pivot, skip     [2, 7, 9, 4, 3, 8, 5]
j=3 a[j]<pivot, i=1 swap -> [2, 4, 9, 7, 3, 8, 5]
j=4 a[j]<pivot, i=2 swap -> [2, 4, 3, 7, 9, 8, 5]
j=5 a[j]>=pivot, skip     [2, 4, 3, 7, 9, 8, 5]
place pivot at 3 -> [2, 4, 3, 5, 9, 8, 7]

Döngü boyunca dizi dört bölgeye ayrılır:

[ lo .. i ]     < pivot
[ i+1 .. j-1 ]  >= pivot
[ j .. hi-1 ]   henüz bakılmadı
[ hi ]          pivot

Bütün sıralama:

partition [0..6] pivot 5 at 3 -> [2, 4, 3, 5, 9, 8, 7]
partition [0..2] pivot 3 at 1 -> [2, 3, 4, 5, 9, 8, 7]
partition [4..6] pivot 7 at 4 -> [2, 3, 4, 5, 7, 8, 9]
partition [5..6] pivot 9 at 6 -> [2, 3, 4, 5, 7, 8, 9]
[2, 3, 4, 5, 7, 8, 9] comps=11

İPUCU · Başka partition şemaları. Ders kitabı ve bazı hocalar ilk elemanı ya da üç elemanın medyanını (median-of-three) pivot alır ve iki uçtan içeri yürüyen Hoare tarzı bir partition kullanır. Ara durumlar farklı çıkar ama değişmeyen kural aynı: partition sonunda pivot yerinde, solu küçük, sağı büyük. Sınavda hangi şemanın istendiğini oku.

En kötü durum

Pivot her seferinde en büyük (ya da en küçük) elemansa bölme “n − 1 ve 0” olur. Son eleman pivot alınan quick sort’a sıralı bir dizi vermek tam bunu yapar:

partition [0..5] pivot 6 at 5 -> [1, 2, 3, 4, 5, 6]
partition [0..4] pivot 5 at 4 -> [1, 2, 3, 4, 5, 6]
partition [0..3] pivot 4 at 3 -> [1, 2, 3, 4, 5, 6]
partition [0..2] pivot 3 at 2 -> [1, 2, 3, 4, 5, 6]
partition [0..1] pivot 2 at 1 -> [1, 2, 3, 4, 5, 6]
comps=15

T(n)=T(n−1)+nT(n) = T(n-1) + n → O(n²). n = 1000, 2000, 4000 için 499 500, 1 999 000, 7 998 000 karşılaştırma: tam n(n−1)/2. Çözüm: pivotu rastgele seç ya da ilk, orta ve son elemanın medyanını al. Ortalama durumda bölmeler dengeli olur ve O(n log n) elde edilir.

Heap sort (genel bakış)

Max-heap: her düğümün kendi çocuklarından büyük ya da eşit olduğu, dizide saklanan tam ikili ağaç. Dizide indeks i’nin çocukları 2i + 1 ve 2i + 2, ebeveyni (i − 1) / 2. En büyük eleman her zaman kökte (indeks 0).

Heap sort iki aşamalıdır:

  1. Heap kur. Son iç düğümden (indeks n/2 − 1) başlayıp köke doğru her düğümü aşağı süz (sift down): düğüm büyük çocuğundan küçükse takas et, devam et. Toplam O(n).
  2. Çıkar. Kökü (en büyük) dizinin sonuyla takas et, heap’i bir küçült, yeni kökü aşağı süz. n − 1 kez tekrarla. Her süzme O(log n).
static void siftDown(int[] a, int i, int n) {
    while (2 * i + 1 < n) {
        int c = 2 * i + 1;                         // sol çocuk
        if (c + 1 < n && a[c + 1] > a[c]) c++;     // büyük çocuk
        if (a[i] >= a[c]) break;
        int t = a[i]; a[i] = a[c]; a[c] = t;
        i = c;
    }
}

static void heapSort(int[] a) {
    int n = a.length;
    for (int i = n / 2 - 1; i >= 0; i--) siftDown(a, i, n);
    for (int end = n - 1; end > 0; end--) {
        int t = a[0]; a[0] = a[end]; a[end] = t;
        siftDown(a, 0, end);
    }
}

{4, 10, 3, 5, 1}:

max-heap: [10, 5, 3, 4, 1]
extract 10 -> [5, 4, 3, 1, 10]
extract 5 -> [4, 1, 3, 5, 10]
extract 4 -> [3, 1, 4, 5, 10]
extract 3 -> [1, 3, 4, 5, 10]

Her zaman O(n log n), O(1) ek bellek. Ama kararsız ve önbellek dostu olmadığı için pratikte quick sort’tan yavaştır. Heap yapısının ayrıntısı SEN2212’de.

Ölçüm: n log n

Rastgele dizilerde karşılaştırma sayısı:

n=10000 merge comps=120464 quick comps=154970  n*log2(n)=132877
n=20000 merge comps=260792 quick comps=342401  n*log2(n)=285754
n=40000 merge comps=561703 quick comps=701837  n*log2(n)=611508
n=80000 merge comps=1203361 quick comps=1545944  n*log2(n)=1303017

İkisi de n log₂ n ile aynı büyüklükte ve n iki katına çıkınca yaklaşık 2.1-2.2 katına çıkıyor (karesel olsaydı 4). Quick sort daha çok karşılaştırma yapmasına rağmen pratikte genellikle daha hızlıdır: yardımcı dizi yok, kopyalama yok.

Bütün sıralamalar: karşılaştırma tablosu

AlgoritmaEn iyiOrtalamaEn kötüEk bellekKararlı
Bubble (bayraklı)O(n)O(n²)O(n²)O(1)evet
SelectionO(n²)O(n²)O(n²)O(1)hayır
InsertionO(n)O(n²)O(n²)O(1)evet
MergeO(n log n)O(n log n)O(n log n)O(n)evet
QuickO(n log n)O(n log n)O(n²)O(log n) yığınhayır
HeapO(n log n)O(n log n)O(n log n)O(1)hayır

Kararlılık testi: anahtarları (2a, 2b, 1) olan kayıtlara tek bir Lomuto partition uygulanınca

after one Lomuto partition: [1, 2b, 2a]

Pivot 1 başa takas edilirken 2a sona fırladı. Merge sort aynı girdide 1 2a 2b verir.

Sık yapılan hatalar

YanlışDoğrusu
Merge sort’ta önce bütün ikilileri birleştirmekSol alt ağaç tamamen bitince sağa geçilir
merge’de kalanları kopyalamamakİki while ile kalan sol ve sağ
tmp’den a’ya geri kopyalamamakfor (k = lo; k <= hi; k++) a[k] = tmp[k];
merge’de < kullanmak<= kararlılığı korur
Merge sort in-placeO(n) yardımcı dizi
quickSort(a, lo, p)Pivot hariç: (lo, p − 1) ve (p + 1, hi)
Partition sonrası iki taraf sıralıSadece pivot yerinde; taraflar sırasız
Quick sort her zaman O(n log n)Kötü pivotta (sıralı girdi + son eleman) O(n²)
Heap sort kararlıDeğil; kök-son takası eşitlerin sırasını bozar

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

The sorted halves [3, 8, 12] and [5, 9, 15, 20] are merged. What is the result, and how many key comparisons are made?

Cevabı göster
[3, 5, 8, 9, 12, 15, 20], 5 comparisons

3 < 5 → 3; 8 > 5 → 5; 8 < 9 → 8; 12 > 9 → 9; 12 < 15 → 12. Sol yarı bitti, 15 ve 20 karşılaştırmasız kopyalanır. 5 karşılaştırma.

Soru 2orta

Merging two sorted halves of 4 elements each: what are the minimum and maximum possible numbers of comparisons? Give an example of each.

Cevabı göster
min 4 ([1,2,3,4]+[5,6,7,8]), max 7 ([1,3,5,7]+[2,4,6,8])

Bir yarı tamamen küçükse, o yarı 4 karşılaştırmada biter ve diğeri kopyalanır. Elemanlar dişli çark gibi iç içeyse son elemana kadar karşılaştırma sürer: n1+n2−1=7n_1 + n_2 - 1 = 7. Program iki durumu da saydı: 4 ve 7.

Soru 3orta

Top-down merge sort (mid = (lo + hi) / 2, left half first) is applied to {38, 27, 43, 3, 9, 82, 10}. List the merges in the order they happen.

Cevabı göster
[38]+[27], [43]+[3], [27,38]+[3,43], [9]+[82], [9,82]+[10], [3,27,38,43]+[9,10,82]

Özyineleme önce sol yarıyı sonuna kadar böler ve birleştirir, sonra sağ yarıya geçer.

merge [38] + [27] -> [27, 38]
merge [43] + [3] -> [3, 43]
merge [27, 38] + [3, 43] -> [3, 27, 38, 43]
merge [9] + [82] -> [9, 82]
merge [9, 82] + [10] -> [9, 10, 82]
merge [3, 27, 38, 43] + [9, 10, 82] -> [3, 9, 10, 27, 38, 43, 82]
7 eleman 4 + 3 diye bölünür (mid = 3). Sağ yarı [9, 82, 10] ise [9, 82] + [10] olur.

Soru 4kolay

Give the best, average and worst case time of merge sort, and its extra space.

Cevabı göster
O(n log n) in all cases, O(n) extra space

Bağıntı her zaman T(n) = 2T(n/2) + n: girdi sırası bölmeyi değiştirmez. log n seviye, her seviyede n iş. Birleştirme için n'lik yardımcı dizi gerekir (in-place değil). Rastgele dizide program n = 80 000 için 1 203 361 karşılaştırma saydı; n log₂ n ≈ 1 303 017.

Soru 5orta

Lomuto partition with the last element as pivot is applied to {7, 2, 9, 4, 3, 8, 5}. What is the array after partitioning, and at which index does the pivot end up?

Cevabı göster
[2, 4, 3, 5, 9, 8, 7], pivot 5 at index 3

i, pivottan küçüklerin son konumunu tutar (başta −1). j soldan sağa gezer; a[j] < pivot ise i'yi artırıp takas eder.

j=0  7 >= 5 atla       [7, 2, 9, 4, 3, 8, 5]
j=1  2 < 5, i=0 takas  [2, 7, 9, 4, 3, 8, 5]
j=2  9 atla
j=3  4 < 5, i=1 takas  [2, 4, 9, 7, 3, 8, 5]
j=4  3 < 5, i=2 takas  [2, 4, 3, 7, 9, 8, 5]
j=5  8 atla
pivot'u i+1 = 3'e koy  [2, 4, 3, 5, 9, 8, 7]
Pivot nihai yerinde; solundakiler küçük, sağındakiler büyük ama kendi aralarında sırasız.

Soru 6orta

Continuing quick sort on that array, which subarrays are partitioned next, and with which pivots?

Cevabı göster
[0..2] pivot 3, then [4..6] pivot 7, then [5..6] pivot 9

partition [0..6] pivot 5 at 3 -> [2, 4, 3, 5, 9, 8, 7]
partition [0..2] pivot 3 at 1 -> [2, 3, 4, 5, 9, 8, 7]
partition [4..6] pivot 7 at 4 -> [2, 3, 4, 5, 7, 8, 9]
partition [5..6] pivot 9 at 6 -> [2, 3, 4, 5, 7, 8, 9]
Tek elemanlı ya da boş parçalar (örneğin [0..0], [2..2]) taban durumdur, partition edilmez. Toplam 11 karşılaştırma.

Soru 7orta

Quick sort with last-element pivot is run on the already sorted array {1, 2, 3, 4, 5, 6}. How many comparisons are made, and what is the Big-O of this case?

Cevabı göster
15, O(n^2)

Pivot her seferinde en büyük eleman; bölme 'n − 1 eleman + boş' olur. Karşılaştırmalar 5 + 4 + 3 + 2 + 1 = 15 = n(n−1)/2. Bağıntı T(n) = T(n−1) + n → O(n²). n = 1000, 2000, 4000 için 499 500, 1 999 000, 7 998 000 sayıldı. Çare: rastgele pivot ya da medyan-of-three.

Soru 8zor

Write static void merge(int[] a, int[] tmp, int lo, int mid, int hi) that merges the sorted ranges a[lo..mid] and a[mid+1..hi].

Cevabı göster
Two indices i, j; copy the smaller (use <= for stability) into tmp; copy leftovers; copy tmp back.

static void merge(int[] a, int[] tmp, int lo, int mid, int hi) {
    int i = lo, j = mid + 1, k = lo;
    while (i <= mid && j <= hi) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];   // <= kararlılık için
        else              tmp[k++] = a[j++];
    }
    while (i <= mid) tmp[k++] = a[i++];          // kalan sol
    while (j <= hi)  tmp[k++] = a[j++];          // kalan sağ
    for (k = lo; k <= hi; k++) a[k] = tmp[k];    // geri kopyala
}

static void mergeSort(int[] a, int[] tmp, int lo, int hi) {
    if (lo >= hi) return;
    int mid = (lo + hi) / 2;
    mergeSort(a, tmp, lo, mid);
    mergeSort(a, tmp, mid + 1, hi);
    merge(a, tmp, lo, mid, hi);
}
4. haftadaki bağlı liste merge'ünün dizi versiyonu. Geri kopyalamayı unutursan a değişmez.

Soru 9zor

Write the Lomuto static int partition(int[] a, int lo, int hi) (pivot = a[hi]) and the quickSort method that uses it.

Cevabı göster
i = lo-1; for j in lo..hi-1 if a[j] < pivot swap a[++i], a[j]; swap a[i+1], a[hi]; return i+1.

static int partition(int[] a, int lo, int hi) {
    int pivot = a[hi];
    int i = lo - 1;
    for (int j = lo; j < hi; j++) {
        if (a[j] < pivot) {
            i++;
            int t = a[i]; a[i] = a[j]; a[j] = t;
        }
    }
    int t = a[i + 1]; a[i + 1] = a[hi]; a[hi] = t;
    return i + 1;
}

static void quickSort(int[] a, int lo, int hi) {
    if (lo >= hi) return;
    int p = partition(a, lo, hi);
    quickSort(a, lo, p - 1);    // pivot HARİÇ
    quickSort(a, p + 1, hi);
}
quickSort(a, lo, p) yazarsan pivot tekrar işlenir ve bazı girdilerde sonsuz özyineleme olur.

Soru 10zor

Heap sort first builds a max-heap from {4, 10, 3, 5, 1} (children of index i are 2i+1 and 2i+2), then repeatedly swaps the root to the end. What is the array after building the heap, and after the first extraction?

Cevabı göster
[10, 5, 3, 4, 1], then [5, 4, 3, 1, 10]

Heap kurma son iç düğümden (indeks 1) geriye doğru sift down yapar: 10 > 5 olduğu için indeks 1 yerinde kalır; kök 4, büyük çocuk 10 ile takas, sonra 4, çocukları 5 ve 1'den 5 ile takas: [10, 5, 3, 4, 1]. İlk çıkarma: kök 10 ile son eleman 1 yer değiştirir, 1 aşağı süzülür (5 ile, sonra 4 ile): [5, 4, 3, 1, 10]. 10 artık yerinde.

Soru 11kolay

Which of these are stable: merge sort, quick sort (Lomuto), heap sort, insertion sort?

Cevabı göster
merge sort and insertion sort

Merge, eşitlikte soldan aldığı (<=) için kararlıdır. Quick sort uzak takas yapar: (2a, 2b, 1) dizisinde pivot 1 başa gelirken 2a sona atılır, sonuç 1 2b 2a. Heap sort da kökü sona takas ederken eşitlerin sırasını bozar. Insertion kararlıdır (13. hafta).

Soru 12orta

Pick an algorithm: (a) sort 1 million records, stability required, O(n log n) guaranteed; (b) fastest in-place sort on average for random arrays; (c) in-place with guaranteed O(n log n); (d) 20 elements that are almost sorted.

Cevabı göster
(a) merge sort (b) quick sort (c) heap sort (d) insertion sort

(a) Merge her durumda n log n ve kararlı; bedeli O(n) bellek. (b) Quick sort'un iç döngüsü çok basit ve önbellek dostu; ortalamada en hızlısı ama en kötü O(n²). (c) Heap sort O(1) ek bellekle her zaman n log n, ama kararsız ve pratikte quick'ten yavaş. (d) Insertion neredeyse sıralı veride O(n)'e yakın; küçük n'de sabitleri en düşük olan. Java'nın Arrays.sort'u nesneler için merge tabanlı (TimSort), ilkel tipler için quick sort tabanlıdır.

Doğrulama: Merge sort, quick sort (Lomuto, pivot = son eleman) ve heap sort verify/SEN2211/W14.java içinde JDK ile çalıştırıldı (java -Xss64m W14.java). Birleştirme adımları, partition'ın her j adımı ve heap durumları programın izinden birebir alındı. Karşılaştırma sayıları sayaçla ölçüldü: rastgele dizide n = 10 000 ... 80 000 için n log₂ n ile aynı büyüklükte, sıralı girdide quick sort için n(n−1)/2.

Deftere not kartı

BÖL VE YÖNET: böl → alt problemleri özyinelemeyle çöz → birleştir MERGE SORT: ortadan böl, iki yarıyı sırala, MERGE et; T(n)=2T(n/2)+n → HER DURUMDA O(n log n) merge: i, j; küçük olanı tmp'ye (<= kararlı); kalanları kopyala; tmp → a merge karşılaştırma: min = küçük yarının boyu, max = n1 + n2 - 1 O(n) ek bellek, STABLE; merge sırası: önce sol alt ağaç tamamen QUICK SORT: pivot seç, PARTITION (küçükler sol, büyükler sağ), iki tarafı özyinele; birleştirme yok Lomuto: pivot = a[hi]; i = lo-1; a[j] < pivot → i++, swap(i,j); sonda swap(i+1, hi) özyineleme (lo, p-1) ve (p+1, hi) pivot HARİÇ ortalama O(n log n), sıralı girdide son-eleman pivotla O(n²) (n(n-1)/2 karşılaştırma) in-place (O(log n) yığın), NOT STABLE HEAP SORT: max-heap kur (n/2-1'den geriye sift down), kökü sona at, heap'i küçült, tekrar çocuklar 2i+1, 2i+2; her durumda O(n log n), O(1) bellek, NOT STABLE ÖZET: bubble/selection/insertion O(n²) | merge/heap O(n log n) her zaman | quick ort n log n, kötü n²