Situs web Haskell memperkenalkan fungsi quicksort 5 baris yang sangat menarik , seperti yang terlihat di bawah ini.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
Mereka juga menyertakan "True quicksort in C" .
// To sort array a[] of size n: qsort(a,0,n-1)
void qsort(int a[], int lo, int hi)
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do {
while ((l < h) && (a[l] <= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
}
} while (l < h);
a[hi] = a[l];
a[l] = p;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
Link di bawah versi C mengarahkan ke halaman yang menyatakan 'Quicksort yang dikutip dalam Pendahuluan bukanlah quicksort "sebenarnya" dan tidak menskalakan untuk daftar yang lebih panjang seperti yang dilakukan kode c.'
Mengapa fungsi Haskell di atas bukan quicksort yang sebenarnya? Bagaimana cara gagal menskalakan daftar yang lebih panjang?
O(N^2)
runtime.Jawaban:
Quicksort yang sebenarnya memiliki dua aspek yang indah:
Contoh singkat Haskell menunjukkan (1), tetapi tidak (2). Bagaimana (2) dilakukan mungkin tidak jelas jika Anda belum mengetahui tekniknya!
sumber
Quicksort inplace yang sebenarnya di Haskell:
sumber
unstablePartition
sangat mirip denganpartition
forquicksort
, tetapi tidak menjamin elemen dim
posisi itu adilp
.Berikut ini adalah transliterasi dari kode C quicksort "benar" ke Haskell. Persiapkan dirimu.
Itu menyenangkan, bukan? Saya benar-benar memotong ini besar
let
di awal, sertawhere
di akhir fungsi, mendefinisikan semua pembantu untuk membuat kode sebelumnya agak cantik.Dan di sini, tes bodoh untuk melihat apakah itu berhasil.
Saya tidak terlalu sering menulis kode imperatif di Haskell, jadi saya yakin ada banyak cara untuk membersihkan kode ini.
Terus?
Anda akan melihat bahwa kode di atas sangat, sangat panjang. Inti dari itu adalah sepanjang kode C, meskipun setiap baris seringkali sedikit lebih bertele-tele. Ini karena C diam-diam melakukan banyak hal buruk yang mungkin Anda anggap remeh. Misalnya
a[l] = a[h];
,. Ini mengakses variabel yang bisa berubahl
danh
, lalu mengakses array yang bisa berubaha
, dan kemudian mengubah array yang bisa berubaha
. Mutasi suci, batman! Di Haskell, mutasi dan mengakses variabel yang bisa berubah itu eksplisit. Qsort "palsu" menarik karena berbagai alasan, tetapi yang paling utama adalah tidak menggunakan mutasi; pembatasan yang diberlakukan sendiri ini membuatnya lebih mudah untuk dipahami dalam sekejap.sumber
Menurut pendapat saya, mengatakan bahwa ini "bukan quicksort yang sebenarnya" terlalu melebih-lebihkan kasusnya. Saya pikir ini adalah implementasi yang valid dari algoritma Quicksort , hanya saja bukan yang sangat efisien.
sumber
Saya pikir kasus yang coba dibuat argumen ini adalah alasan mengapa quicksort umum digunakan adalah karena itu ada di tempat dan sebagai hasilnya cukup ramah-cache. Karena Anda tidak memiliki manfaat tersebut dengan daftar Haskell, alasan utamanya hilang, dan Anda mungkin juga menggunakan jenis gabungan, yang menjamin O (n log n) , sedangkan dengan quicksort Anda harus menggunakan pengacakan atau rumit skema partisi untuk menghindari run time O (n 2 ) dalam kasus terburuk.
sumber
Berkat evaluasi malas, program Haskell tidak (hampir tidak bisa ) melakukan apa yang terlihat seperti yang dilakukannya.
Pertimbangkan program ini:
Dalam bahasa yang bersemangat, pertama
quicksort
akan lari, lalushow
, laluputStrLn
. Argumen fungsi dihitung sebelum fungsi tersebut mulai berjalan.Di Haskell, justru sebaliknya. Fungsi tersebut mulai berjalan lebih dulu. Argumen hanya dihitung saat fungsi benar-benar menggunakannya. Dan argumen gabungan, seperti daftar, dihitung satu per satu, karena setiap bagian digunakan.
Jadi hal pertama yang terjadi dalam program ini adalah yang
putStrLn
mulai berjalan.Implementasi GHC dari
putStrLn
bekerja dengan menyalin karakter dari argumen String ke dalam buffer keluaran. Tapi saat memasuki loop ini,show
belum berjalan. Oleh karena itu, saat akan menyalin karakter pertama dari string, Haskell mengevaluasi pecahan dari panggilanshow
dan yangquicksort
diperlukan untuk menghitung karakter itu . KemudianputStrLn
beralih ke karakter berikutnya. Jadi eksekusi ketiga functions-putStrLn
,show
danquicksort
- disisipkan.quicksort
dieksekusi secara bertahap, meninggalkan grafik halangan yang tidak dievaluasi untuk mengingat di mana ia tinggalkan.Sekarang ini sangat berbeda dari apa yang mungkin Anda harapkan jika Anda terbiasa dengan, Anda tahu, bahasa pemrograman lain yang pernah ada. Tidak mudah untuk memvisualisasikan bagaimana
quicksort
sebenarnya berperilaku di Haskell dalam hal akses memori atau bahkan urutan perbandingan. Jika Anda hanya dapat mengamati perilakunya, dan bukan kode sumbernya, Anda tidak akan mengenali apa yang dilakukannya sebagai quicksort .Misalnya, versi C dari quicksort mempartisi semua data sebelum panggilan rekursif pertama. Dalam versi Haskell, elemen pertama dari hasil akan dihitung (dan bahkan dapat muncul di layar Anda) sebelum partisi pertama selesai dijalankan — bahkan sebelum pekerjaan apa pun diselesaikan
greater
.PS Kode Haskell akan lebih seperti quicksort jika ia melakukan jumlah perbandingan yang sama dengan quicksort; kode seperti yang tertulis melakukan perbandingan dua kali lebih banyak karena
lesser
dangreater
ditentukan untuk dihitung secara independen, melakukan dua pemindaian linier melalui daftar. Tentu saja pada prinsipnya mungkin bagi compiler untuk menjadi cukup pintar untuk menghilangkan perbandingan ekstra; atau kode dapat diubah untuk digunakanData.List.partition
.PPS Contoh klasik dari algoritma Haskell ternyata tidak berperilaku seperti yang Anda harapkan adalah saringan Eratosthenes untuk menghitung bilangan prima.
sumber
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, masalah paling langsungnya mungkin akan lebih jelas. Dan itu sebelum kami mempertimbangkan untuk beralih ke algoritma saringan yang sebenarnya.putStrLn
yang merupakan aplikasi berpotonganshow
ke aplikasi berpotonganquicksort
ke daftar literal --- dan itulah yang dilakukannya! (sebelum pengoptimalan --- tetapi bandingkan kode C dengan assembler yang dioptimalkan kapan-kapan!). Mungkin maksud Anda "berkat evaluasi malas, program Haskell tidak melakukan apa yang dilakukan kode yang tampak serupa dalam bahasa lain"?Saya percaya bahwa alasan kebanyakan orang mengatakan bahwa Haskell Quicksort yang cantik bukanlah Quicksort yang "benar" adalah kenyataan bahwa ia tidak ada pada tempatnya - jelas, tidak mungkin ketika menggunakan tipe data yang tidak dapat diubah. Tetapi ada juga keberatan bahwa ini tidak "cepat": sebagian karena ++ mahal, dan juga karena ada kebocoran spasi - Anda tetap berpegang pada daftar masukan saat melakukan panggilan rekursif pada elemen yang lebih rendah, dan dalam beberapa kasus - misalnya ketika daftar menurun - ini menghasilkan penggunaan ruang kuadrat. (Anda mungkin mengatakan bahwa membuatnya berjalan dalam ruang linier adalah cara terdekat yang bisa Anda dapatkan "di tempat" menggunakan data yang tidak dapat diubah.) Ada solusi yang tepat untuk kedua masalah tersebut, dengan menggunakan parameter yang terakumulasi, tupling, dan fusi; lihat S7.6.1 dari Richard Bird '
sumber
Ini bukanlah gagasan mutasi elemen di tempat dalam pengaturan fungsional murni. Metode alternatif di utas ini dengan array yang bisa berubah kehilangan semangat kemurnian.
Setidaknya ada dua langkah untuk mengoptimalkan versi dasar (yang merupakan versi paling ekspresif) dari penyortiran cepat.
Optimalkan penggabungan (++), yang merupakan operasi linier, dengan akumulator:
Optimalkan ke pengurutan cepat terner (partisi 3 arah, disebutkan oleh Bentley dan Sedgewick), untuk menangani elemen duplikat:
Gabungkan 2 dan 3, lihat buku Richard Bird:
Atau sebagai alternatif jika elemen yang diduplikasi bukan mayoritas:
Sayangnya, median-of-three tidak dapat diimplementasikan dengan efek yang sama, misalnya:
karena kinerjanya masih buruk untuk 4 kasus berikut:
[1, 2, 3, 4, ...., n]
[n, n-1, n-2, ..., 1]
[m-1, m-2, ... 3, 2, 1, m + 1, m + 2, ..., n]
[n, 1, n-1, 2, ...]
Semua 4 kasus ini ditangani dengan baik dengan pendekatan median-of-three yang penting.
Sebenarnya, algoritme pengurutan yang paling cocok untuk pengaturan yang murni berfungsi adalah jenis gabungan, tetapi bukan pengurutan cepat.
Untuk detailnya, silakan kunjungi tulisan saya yang sedang berlangsung di: https://sites.google.com/site/algoxy/dcsort
sumber
Tidak ada definisi yang jelas tentang apa itu quicksort dan apa yang bukan quicksort yang sebenarnya.
Mereka menyebutnya bukan quicksort yang sebenarnya, karena tidak diurutkan di tempat:
sumber
Karena mengambil elemen pertama dari daftar menghasilkan waktu proses yang sangat buruk. Gunakan median dari 3: pertama, tengah, terakhir.
sumber
O(n^2)
Minta siapa saja untuk menulis quicksort di Haskell, dan pada dasarnya Anda akan mendapatkan program yang sama - ini jelas quicksort. Berikut beberapa kelebihan dan kekurangannya:
Pro: Ini meningkatkan quicksort "benar" dengan menjadi stabil, yaitu mempertahankan urutan urutan di antara elemen yang sama.
Pro: Adalah sepele untuk menggeneralisasi menjadi tiga arah split (<=>), yang menghindari perilaku kuadrat karena beberapa nilai yang terjadi O (n) kali.
Pro: Lebih mudah dibaca - bahkan jika harus menyertakan definisi filter.
Kontra: Menggunakan lebih banyak memori.
Kontra: Sangat mahal untuk menggeneralisasi pilihan pivot dengan pengambilan sampel lebih lanjut, yang dapat menghindari perilaku kuadrat pada urutan entropi rendah tertentu.
sumber