- Medyan, sıralama yapıldığında kolayca bulunabilir; ancak sıralama maliyeti nedeniyle tek bir eleman seçme probleminde bile O(n log n) sınırına takılır
- quickselect, yalnızca gerekli tarafı özyinelemeli olarak arayarak kth elemanı ya da medyanı ortalama O(n) sürede bulabilir
- Rastgele pivot pratikte iyi çalışır; ancak sürekli kötü pivot seçilirse her seferinde yalnızca bir eleman elenir ve süre O(n²) seviyesine kadar kötüleşir
- median-of-medians, 5'li grupların medyanlarından yeniden bir medyan seçerek en kötü durumda bile elemanların en az %30'unun elenmesini sağlar
- Gerçek uygulamalarda pivot hesaplama maliyeti yüksek olabilir; bu yüzden C++ standart kütüphanesindeki gibi quickselect ile heapselect'i birleştiren introselect daha pratik bir tercih olabilir
Sıralamayla medyan bulmanın sınırları
- En basit medyan hesaplama yöntemi, listeyi sıraladıktan sonra orta indeksteki değeri seçmektir
- Tek uzunluklu listede ortadaki eleman döndürülür, çift uzunluklu listede ise ortadaki iki elemanın ortalaması alınır
- Karşılaştırma tabanlı sıralamanın en iyi zaman karmaşıklığı O(n log n) olduğundan, bu yöntemin çalışma süresine de sıralama hâkim olur
- Kodunun basit olması avantajdır, ancak yalnızca tek bir medyan bulmak için gerekenden fazla iş yapar
Ortalama O(n) sağlayan quickselect
- quickselect, Tony Hoare tarafından geliştirilen özyinelemeli bir algoritmadır; yalnızca medyanı değil, listenin herhangi bir kth elemanını da bulabilir
- Temel akış, listeyi bir pivot etrafında böldükten sonra yalnızca kth elemanın bulunduğu tarafı aramaya devam etmektir
- Listeden bir pivot seçilir
- Liste, pivottan küçük veya eşit elemanlar ile pivottan büyük elemanlar olarak ayrılır
- Aranan kth elemanın hangi tarafta olduğuna karar verilip yalnızca o alt listede özyinelemeli çağrı yapılır
- Sağ alt listeye inilirse, dışarıda bırakılan sol taraftaki eleman sayısı kadar
k değeri ayarlanır
- Örnek liste
[9,1,0,2,3,4,6,8,7,10,5] için uzunluk 11 olduğundan 6. en küçük eleman aranır; pivotlara göre aralık daraltılarak sonunda 5 döndürülür
quickselect_median, listenin uzunluğu tekse tek bir orta indeksi quickselect ile bulur; çiftse ortadaki iki indeksi bulup ortalamalarını alır
- Pivot listeyi yaklaşık iki eşit parçaya bölerse toplam iş miktarı
n + n/2 + n/4 + ... = 2n olur ve süre O(n) olur
En kötü durumdan kaçınmak için iyi bir pivot gerekir
- quickselect'in ortalama O(n) olması, pivot seçiminin yeterince iyi olmasına bağlıdır
- Her adımda maksimum değeri pivot seçmek gibi kötü şanslı bir durumda, her aşamada yalnızca bir eleman elenir ve süre O(n²) olur
- En kötü durumda bile doğrusal zamanı garanti etmek için, quickselect'e doğrusal zamanda yeterince iyi bir pivot vermek gerekir
- Bu pivot seçme algoritması 1973'te Blum, Floyd, Pratt, Rivest ve Tarjan tarafından geliştirildi; ilgili makale 1973 paper bağlantısında yer alıyor
median-of-medians ile pivot seçimi
- median-of-medians, quickselect'in kullanacağı iyi bir pivot seçme prosedürüdür
- Uygulama akışı şöyledir
- Eleman sayısı 5'ten azsa mevcut sıralama tabanlı medyan fonksiyonu kullanılır
- Liste 5'erli gruplara ayrılır
- 5 elemana ulaşmayan eksik grup, basitleştirme amacıyla atılır
- Her grup sıralanır ve indeks 2'deki medyanlar toplanır
- Toplanan medyanlar listesinin medyanı yeniden bulunup pivot olarak döndürülür
- Her grubun boyutu sabit 5 olduğundan, grup başına sıralama sabit zaman sayılır ve toplamda işlem O(n) olur
- Medyanların medyanını bulmak için yapılan özyinelemeli çağrı, boyutu
n/5 olan bir alt problem olarak analize dahil edilir
Neden en az %30 elenebilir?
- 5'li grupları sıralayıp sütunlar halinde düşündüğümüzde ve bu sütunların medyanlarını tekrar sıralayıp medyanların medyanını seçtiğimizde, pivotun kalitesi analiz edilebilir
- Pivot mümkün olduğunca öne kaymış en kötü durumda bile, belirli bir bölgedeki elemanların pivottan küçük ya da büyük olduğu garanti edilir
- Her sütundan 3 eleman alıp sütunların yarısını dikkate aldığımızda, en az
3/5 * 1/2 * n = 3/10 n eleman elenebilir
- Eleme garanti oranı: {p:30}
- Toplam çalışma süresi şu özyineleme bağıntısıyla ifade edilir
T(n) = n + T(n/5) + T(7n/10)
- Burada
n bölme işlemini, T(n/5) median-of-medians hesaplamasını, T(7n/10) ise quickselect'in özyinelemeli aramasını temsil eder
- Bu bağıntıda iki özyineleme terimi olduğundan basit Master teoremi doğrudan uygulanamaz; tümevarım daha sezgisel bir ispat yöntemidir
Birleşimin sonucu: doğrusal zamanda medyan bulma
- quickselect, yeterince iyi bir pivot verildiğinde medyanı doğrusal zamanda bulabilir
- median-of-medians, quickselect'in ihtiyaç duyduğu iyi pivotu O(n) sürede seçebilir
- İki algoritma birleştirildiğinde, medyanı veya listenin n'inci elemanını doğrusal zamanda bulan bir algoritma elde edilir
Gerçek uygulamalardaki tercih
- Pratikte rastgele pivot seçimi neredeyse her zaman yeterince iyidir
- median-of-medians da doğrusal zamanlıdır, ancak pratikte pivot hesaplama maliyeti yüksek olduğu için yavaş olabilir
- C++ standart kütüphanesi introselect kullanır; bu yaklaşım heapselect ile quickselect'i birleştirir ve O(n log n) üst sınıra sahiptir
- introselect genellikle hızlıdır; ancak üst sınırı kötü bir algoritmayla başlayıp etkili bir pivot seçemediğinde daha yavaş ama üst sınırı daha iyi olan bir algoritmaya geçer
- quickselect fonksiyonunun incelediği eleman sayısına dair karşılaştırmada, deterministik pivot neredeyse her zaman rastgele pivottan daha az elemanı dikkate aldı; ancak bu karşılaştırmaya median-of-medians hesaplama maliyeti dahil değil
- 2017 tarihli new paper, median-of-medians yaklaşımını diğer seçim algoritmalarıyla rekabet edebilir hâle getiren bir çalışmayı anlatıyor
1 yorum
Hacker News yorumları
Yaklaşık 4 yıl önce birkaç medyan algoritmasını karşılaştırmıştım; yazı beklediğimden çok daha uzun olmuştu :)
https://danlark.org/2020/11/11/miniselect-practical-and-generic-selection-algorithms/
10–15 yıl önce, çok kilobaytlık günlük girdilerinden ayrıştırılan milyarlarca değerin medyanını düzenli olarak bulmam gerekiyordu. O dönemde büyük ölçekli işleme için MapReduce kullanıyorduk ve bu hacimde veri için yalnızca doğrusal zaman değil, mümkünse tek geçişte birden fazla makineye dağıtılabilen bir yöntem gerekiyordu
Verinin hassasiyetini ve aralığını bilmek işe yarıyordu. Değerler tam sayı milisaniye cinsinden sürelerdi, bu yüzden negatif değillerdi; ayrıca 90. yüzdeliğin 1 saniyeden çok daha küçük olduğunu da biliyorduk
Normalde medyan bulmak sıralamaya benzer bir iş gerektirir, ama bu koşullarda bucket sort mümkün hale geliyor. Anahtarı tam sayı milisaniye süresi, değeri de görülme sayısı olan bir sözlük, yani histogram oluşturmak yeterli
Maksimum süreyi bilmediğimiz için sözlük boyutunun patlamasını önlemek adına 999 ms’yi aşan tüm değerleri 999 ms kovasına koyduk; böylece 0–999 anahtarları ve değerleriyle toplamda yaklaşık 2000 tam sayıyla sınırlı kaldı. Bu kısım klasik bucket sort’tan farklıydı; MapReduce ile dağıtılsa bile tek geçişte çok kolay işlenebiliyordu, ardından medyanı histogramdan çıkarmak yeterliydi
10.000 sayısını gelişigüzel verdim, ama istenen güven düzeyi için gereken örnek sayısı istatistiksel olarak hesaplanabilir ve sanırım o kadar da büyük olmaz
Uğraştığım bazı sistemlerde Prometheus gecikme için yaklaşık 10 saniyelik bir sınır koyuyormuş gibi davranıyordu. Bu yüzden o sınırı aşan istekler gerçekte daha uzun sürse bile hepsi 10 saniye olarak kaydediliyordu. İlginç
2017’de median-of-medians yaklaşımını diğer seçim algoritmalarıyla rekabet edebilir hale getiren yeni bir makale çıktığına ve bunu makalenin yazarı Andrei Alexandrescu’nun bildirdiğine dair bir not var
Kendisi 2016’da kendi algoritması hakkında bir sunum da yapmıştı. Sunumları eğlenceli biri, şiddetle tavsiye ederim
There's Treasure Everywhere - Andrei Alexandrescu
https://www.youtube.com/watch?v=fd1_Miy1Clg
Yazılım kullanan biriyseniz Andrei’nin bulabildiğiniz tüm yazılarını ve sunumlarını dinleyip okumanızı öneririm. Bu sunum da gerçekten bir hazine
Lisans döneminde median-of-medians quickselect algoritmasını öğrenmiştim ve gerçekten çok etkilenmiştim. Kendim uyguladım ama korkunç derecede yavaştı. Çalışma süresi doğrusal artıyordu, ama bunun anlamlı olması için listede en az milyarlarca öğe olması gerekiyordu
Bunu yüksek lisans öğrencisi bir arkadaşımla konuşurken bana şöyle bir şey demişti: “Yavaş olabilir, ama önemli olan sıralanmamış bir listede seçimin O(n) zamanda yapılabildiğini kanıtlaması. Bir zamanlar bunun mümkün olup olmadığını bile bilmiyorduk; artık mümkün olduğunu bildiğimize göre daha hızlı doğrusal algoritmalar da olabilir.”
O kadar basit ama derin bir dersti ki neredeyse yüksek lisansa başvuracaktım. O arkadaşım bu konuşmayı hatırlıyor mu bilmiyorum, ama benim eğitimimde dönüm noktası gibi bir andı
“Zaten bir algoritmanın var olduğunu bildiğimize göre daha hızlı bir algoritma da olabilir” diye de düşünülebilir. O(n) bir algoritmanın varlığı neden O(n log n) bir algoritmanın varlığından daha güçlü bir sinyal olsun?
Bu algoritmanın sabit katsayısı 22 civarındaydı diye hatırlıyorum; gerçi ilgili başka bir algoritma da olabilir
median-of-medians algoritmasının ilginç yanlarından biri, yazar listesinin tamamen yıldızlar geçidi olması
Manuel Blum - 1995 Turing Ödülü sahibi
Robert Floyd - 1978 Turing Ödülü sahibi
Ron Rivest - 2002 Turing Ödülü sahibi
Bob Tarjan - 1986 Turing Ödülü sahibi ve 1982'de ilk Nevanlinna Ödülü sahibi
Vaughan Pratt - listedeki Turing Ödülü sahibi olmayan tek kişi; ama Stanford'da emeritus profesör, Sun Microsystems olmadan önce SUN projesine liderlik etti, Sun'ın ilk dönemlerinde araştırma direktörü ve Sun logosunun tasarımcısı olarak önemli rol oynadı; Pratt asallık sertifikaları gibi pek çok havalı şey de bıraktı
Dört bağımsız Turing Ödülü ve üstüne bir de SPARCstation; bu makalede hepsi var
Yazar listesi kesinlikle etkileyici
KMP algoritmasındaki “P” de Pratt
return l[len(l) / 2]Python uzmanı değilim ama Python'da
/operatörü kayan noktalı sayı döndürmüyor mu? Dizi indeksi olarak kayan noktalı sayı kullanmak yerine tamsayı bölmesi olan//neden kullanılmıyor?Çok büyük bir dizi değilse sorun olmayabilir, ama yine de ciddi bir kod kokusu var. Python'da yeniysen ve iki operatörün ayrı olduğunu bilmiyorsan geçilebilir; ama yazıda bir dalda tamsayı bölmesi, diğer dalda kayan noktalı bölme kullanan daha da tuhaf kod da var
def quickselect_median(l, pivot_fn=random.choice):if len(l) % 2 == 1:return quickselect(l, len(l) // 2, pivot_fn)else:return 0.5 * (quickselect(l, len(l) / 2 - 1, pivot_fn) +quickselect(l, len(l) / 2, pivot_fn))50 yorum olmuş ve kimse bunu görmemiş gibi; bu da ortalama Python kod kalitesi hakkındaki mevcut önyargılarımı sadece güçlendiriyor
Bir diziyi kayan noktalı sayıyla indekslemenin istisna fırlatacağını düşünüyorum
Sözde kod yerine sözde kod gibi görünen gerçek bir programlama dili seçilmiş; açıklama amacı için de muhtemelen iyi çalışan bir kod
Orijinal yazıyı gerçekten keyifle okudum ama “her adımda en büyük elemanı pivot seçerseniz O(n) yerine O(n²) olabilir” kısmı takıldı
Kötücül girdiden endişe ediyorsanız veriyi önce O(n)'de shuffle etmek bunun zorla gerçekleşmesini engelleyebilir. Veri shuffle etmek için çok büyükse, bucket'lar shuffle edilebilir boyuta indiğinde bir kez karıştırmak yeterli olur
Shuffle yaptıysanız olasılık, en kötü durumun pratikte gerçekleşmeyeceğini garanti eder. Biri “teknik olarak” mümkün derse, ben de “teknik olarak” bir saldırgan 256 bitlik özel anahtarın tüm bitlerini doğru tahmin edebilir diye cevap veririm
Dünyamız olasılıklar üzerine kurulu. Tüm özel anahtarlar, birinin onları tam olarak tahmin edemeyeceğine dair matematiksel imkânsızlık tarafından korunuyor
Benim okuduğum kadarıyla, shuffle sonrası quickselect pratikte O(n)
Yine de rastgeleliğe güvenebiliyorsak O(n)'yi aşan çalışma süresi görme olasılığı çok düşük
Floyd-Rivest de aynı işi başarıyor. Yanlış hatırlamıyorsam biraz daha verimli
Ama ben nasıl çalıştığını bir türlü anlayamadım
https://en.m.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm
n'inci elemanı seçerken n çok küçük ya da çok büyükse median-of-medians en iyi seçenek olmayabilir
Bunun yerine [1]'deki gibi yanlı bir pivot kullanabilir ya da benim “k içinden j'inci” dediğim yöntemi uygulayabilirsiniz. Floyd-Rivest de hızı artırabilir
İyi uygulanmış quickselect'e kıyasla 1,2~2,0 kat throughput veren bir hobi projem var: https://github.com/koskinev/turboselect
Hızlı, genel amaçlı, in-place seçim algoritmaları hakkında kaynak varsa ilgilenirim
[1] https://doi.org/10.4230/LIPIcs.SEA.2017.24
Tüm veriyi bellekte tutmadan da rastgele bir quantile'ın yaklaşık değerini hesaplayabilen streaming algoritmalar kullanabilirsiniz
Yaklaşık hesaplamaya izin verebilir misiniz? Hata sınırını belirlemek için veri hakkında hangi varsayımlar gerekir? Bu varsayımların geçerliliğini sürdürdüğünü nasıl doğrulayacaksınız?
Şahsen streaming median yaklaşımı düşünmek zorunda kalacağım bir durum gelene kadar, orijinal yazıdaki quickselect algoritmasına yönelirim