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.
- 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:
- Merge sort: böl (kolay), birleştir (asıl iş). Her durumda O(n log n), O(n) ek bellek, kararlı.
- Quick sort: bölümle (asıl iş), birleştirme yok. Ortalama O(n log n), kötü pivotta O(n²).
- Partition sonrası pivot nihai yerindedir. İki taraf kendi içinde sırasızdır.
- İ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
→ (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 :
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
→ 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:
- 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).
- Çı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
| Algoritma | En iyi | Ortalama | En kötü | Ek bellek | Kararlı |
|---|---|---|---|---|---|
| Bubble (bayraklı) | O(n) | O(n²) | O(n²) | O(1) | evet |
| Selection | O(n²) | O(n²) | O(n²) | O(1) | hayır |
| Insertion | O(n) | O(n²) | O(n²) | O(1) | evet |
| Merge | O(n log n) | O(n log n) | O(n log n) | O(n) | evet |
| Quick | O(n log n) | O(n log n) | O(n²) | O(log n) yığın | hayır |
| Heap | O(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ştirmek | Sol 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 kopyalamamak | for (k = lo; k <= hi; k++) a[k] = tmp[k]; |
merge’de < kullanmak | <= kararlılığı korur |
| Merge sort in-place | O(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ç.
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 → 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.
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
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: . Program iki durumu da saydı: 4 ve 7.
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
Ö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.Give the best, average and worst case time of merge sort, and its extra space.
Cevabı göster
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.
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
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.Continuing quick sort on that array, which subarrays are partitioned next, and with which pivots?
Cevabı göster
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.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
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.
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
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.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
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.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
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.
Which of these are stable: merge sort, quick sort (Lomuto), heap sort, insertion sort?
Cevabı göster
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).
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 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.