Minimalist, örnek Haskell hızlı sıralama neden "gerçek" bir hızlı sıralama değil?


118

Haskell'in web sitesi , aşağıda görüldüğü gibi çok çekici bir 5 satırlık hızlı sıralama işlevi sunar .

quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
    where
        lesser = filter (< p) xs
        greater = filter (>= p) xs

Ayrıca "C'de Gerçek Hızlı Sıralama" içerirler .

// 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 );
  }
}

C sürümünün altındaki bir bağlantı, "Giriş'te alıntılanan hızlı sıralama" gerçek "hızlı sıralama değildir ve c kodunun yaptığı gibi daha uzun listeler için ölçeklenmez" şeklinde bir sayfaya yönlendirir .

Yukarıdaki Haskell işlevi neden gerçek bir hızlı sıralama değil? Daha uzun listeler için ölçeklendirilemez?


Tam olarak bahsettiğiniz sayfaya bir bağlantı eklemelisiniz.
Staven

14
Yerinde değil, yani oldukça yavaş mı? Aslında güzel soru!
Fuz

4
@FUZxxl: Haskell listeleri değiştirilemez, bu nedenle varsayılan veri türleri kullanılırken hiçbir işlem yerinde olmayacaktır. Hızına gelince - daha yavaş olmayacak; GHC, etkileyici bir derleyici teknolojisi parçasıdır ve çoğu zaman değişmez veri yapılarını kullanan haskell çözümleri, diğer dillerdeki diğer değiştirilebilir olanlarla hızlanır.
Callum Rogers

1
Aslında qsort değil mi? Qsort'un O(N^2)çalışma zamanı olduğunu unutmayın .
Thomas Eding

2
Yukarıdaki örneğin Haskell'e giriş niteliğinde bir örnek olduğu ve bu hızlı sıralamanın listeleri sıralamak için çok kötü bir seçim olduğu unutulmamalıdır. Data.List'teki sıralama, 2002'de yeniden birleştirilecek şekilde değiştirildi: hackage.haskell.org/packages/archive/base/3.0.3.1/doc/html/src/… , burada önceki hızlı sıralama uygulamasını da görebilirsiniz. Mevcut uygulama, 2009 yılında yapılmış bir birleştirme sıralamasıdır: hackage.haskell.org/packages/archive/base/4.4.0.0/doc/html/src/… .
HaskellElephant

Yanıtlar:


75

Gerçek hızlı sıralamanın iki güzel yönü vardır:

  1. Böl ve fethet: sorunu iki küçük probleme bölün.
  2. Öğeleri yerinde bölümlere ayırın.

Kısa Haskell örneği (1) 'i gösterir, ancak (2)' yi göstermez. Tekniği henüz bilmiyorsanız, (2) 'nin nasıl yapıldığı açık olmayabilir!



Yerinde bölümleme işleminin net bir açıklaması için, interaktivepython.org/courselib/static/pythonds/SortSearch/… bakın .
pvillela

57

Haskell'de gerçek yerinde hızlı sıralama:

import qualified Data.Vector.Generic as V 
import qualified Data.Vector.Generic.Mutable as M 

qsort :: (V.Vector v a, Ord a) => v a -> v a
qsort = V.modify go where
    go xs | M.length xs < 2 = return ()
          | otherwise = do
            p <- M.read xs (M.length xs `div` 2)
            j <- M.unstablePartition (< p) xs
            let (l, pr) = M.splitAt j xs 
            k <- M.unstablePartition (== p) pr
            go l; go $ M.drop k pr

Kaynak unstablePartition o (bildiğim kadarıyla söyleyebilirim gibi) gerçekten de yerinde tekniğini takas aynı olduğunu ortaya koymaktadır.
Dan Burton

3
Bu çözüm yanlıştır. unstablePartitionçok benzer partitioniçin quicksort, ama en eleman garanti etmez minci pozisyon adildir p.
nymk

29

İşte "gerçek" hızlı sıralama C kodunun Haskell'e çevirisi. Kendinizi hazırlayın.

import Control.Monad
import Data.Array.IO
import Data.IORef

qsort :: IOUArray Int Int -> Int -> Int -> IO ()
qsort a lo hi = do
  (h,l,p,t) <- liftM4 (,,,) z z z z

  when (lo < hi) $ do
    l .= lo
    h .= hi
    p .=. (a!hi)

    doWhile (get l .< get h) $ do
      while ((get l .< get h) .&& ((a.!l) .<= get p)) $ do
        modifyIORef l succ
      while ((get h .> get l) .&& ((a.!h) .>= get p)) $ do
        modifyIORef h pred
      b <- get l .< get h
      when b $ do
        t .=. (a.!l)
        lVal <- get l
        hVal <- get h
        writeArray a lVal =<< a!hVal
        writeArray a hVal =<< get t

    lVal <- get l
    writeArray a hi =<< a!lVal
    writeArray a lVal =<< get p

    hi' <- fmap pred (get l)
    qsort a lo hi'
    lo' <- fmap succ (get l)
    qsort a lo' hi

Eğlenceliydi, değil mi? Aslında bu büyük kesip letyanı sıra başında wherebiraz tatlı önceki kod yapmak yardımcıları tümünü tanımlamak, işlevi sonunda.

  let z :: IO (IORef Int)
      z = newIORef 0
      (.=) = writeIORef
      ref .=. action = do v <- action; ref .= v
      (!) = readArray
      (.!) a ref = readArray a =<< get ref
      get = readIORef
      (.<) = liftM2 (<)
      (.>) = liftM2 (>)
      (.<=) = liftM2 (<=)
      (.>=) = liftM2 (>=)
      (.&&) = liftM2 (&&)
  -- ...
  where doWhile cond foo = do
          foo
          b <- cond
          when b $ doWhile cond foo
        while cond foo = do
          b <- cond
          when b $ foo >> while cond foo

Ve burada, işe yarayıp yaramadığını görmek için aptal bir test.

main = do
    a <- (newListArray (0,9) [10,9..1]) :: IO (IOUArray Int Int)
    printArr a
    putStrLn "Sorting..."
    qsort a 0 9
    putStrLn "Sorted."
    printArr a
  where printArr a = mapM_ (\x -> print =<< readArray a x) [0..9]

Haskell'de çok sık zorunlu kod yazmıyorum, bu yüzden bu kodu temizlemenin birçok yolu olduğundan eminim.

Ne olmuş yani?

Yukarıdaki kodun çok çok uzun olduğunu fark edeceksiniz. Kalbi C kodu kadar uzundur, ancak her satır genellikle biraz daha ayrıntılıdır. Bunun nedeni, C'nin, verilmiş sayabileceğiniz pek çok kötü şeyi gizlice yapmasıdır. Örneğin a[l] = a[h];,. Bu değişken değişkenleri kere lve hve daha sonra kesilebilir dizisine erişen asonra mutasyona uğraması ve değişken dizi, ve a. Kutsal mutasyon Batman! Haskell'de mutasyon ve değişken değişkenlere erişim açıktır. "Sahte" qsort, çeşitli nedenlerden dolayı çekicidir, ancak bunların başında mutasyon kullanmamasıdır; Bu kendi kendine uygulanan kısıtlama, bir bakışta anlamayı çok daha kolay hale getirir.


3
Bu, mide bulandırıcı bir şekilde harika. GHC'nin böyle bir şeyden ne tür bir kod ürettiğini merak ediyorum.
Ian Ross

@IanRoss: Saf olmayan hızlı diziden mi? GHC aslında oldukça iyi kod üretir.
JD

"Sahte" qsort çeşitli nedenlerden dolayı çekici ... "Yerinde manipülasyon olmadan performansının (daha önce belirtildiği gibi) korkunç olacağından korkuyorum. Ve her zaman 1. elementi pivot olarak almak da yardımcı olmuyor.
dbaltor

25

Bana göre bunun "gerçek bir hızlı sıralama olmadığını" söylemek vakayı abartıyor. Bence bu, Quicksort algoritmasının geçerli bir uygulaması, ancak özellikle verimli bir uygulama değil.


9
Bir keresinde biriyle bu tartışmam vardı: QuickSort'u belirten ve gerçekten yerinde olan gerçek kağıda baktım.
ivanm

2
@ivanm köprüler ya da olmadı :)
Dan Burton

1
Bu makalenin nasıl zorunlu olduğunu ve hatta logaritmik alan kullanımını garanti etmek için (pek çok kişinin bilmediği) hileyi içerirken, ALGOL'daki (artık popüler) yinelemeli versiyonu sadece bir dipnottur. Sanırım şimdi diğer makaleye bakmam gerekecek ... :)
hugomg

6
Herhangi bir algoritmanın "geçerli" uygulaması aynı asimptotik sınırlara sahip olmalıdır, sence de öyle değil mi? Piç kurusu Haskell hızlı sıralaması, orijinal algoritmanın bellek karmaşıklığını korumaz. Yakınında bile değil. Bu yüzden Sedgewick'in C'deki orijinal Quicksort'tan 1.000 kat daha yavaş
JD

16

Bence bu argümanın yapmaya çalıştığı durum, quicksort'un yaygın olarak kullanılmasının nedeninin yerinde olması ve sonuç olarak oldukça önbellek dostu olması. Haskell listeleriyle bu avantajlara sahip olmadığınız için, ana varoluş nedeni ortadan kalktı ve O (n log n) garanti eden birleştirme sıralaması da kullanabilirsiniz , oysa hızlı sıralama ile ya rasgele sıralamayı ya da karmaşık en kötü durumda O (n 2 ) çalışma süresinden kaçınmak için bölümleme şemaları .


5
Ve Mergesort, yardımcı dizilerle çalışma ihtiyacından kurtulmuş (değişmez) sevilen listeler için çok daha doğal bir sıralama algoritmasıdır.
hugomg

16

Tembel değerlendirme sayesinde Haskell programı bunu yapmaz (neredeyse yapamaz) göründüğü şeyi yapmaz ).

Bu programı düşünün:

main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))

Hevesli bir dilde, önce quicksortkoşardı, sonra showsonraputStrLn . Bir fonksiyonun argümanları, o fonksiyon çalışmaya başlamadan önce hesaplanır.

Haskell'de durum tam tersi. İlk önce işlev çalışmaya başlar. Bağımsız değişkenler yalnızca işlev onları gerçekten kullandığında hesaplanır. Ve bir liste gibi bileşik bir argüman, her bir parçası kullanıldıkça, her seferinde tek parça hesaplanır.

Yani ilk bu programda olur şey olduğunu putStrLnbaşlar koşuyor.

GHC'nin gerçeklemesi,putStrLn String argümanının karakterlerini bir çıktı tamponuna kopyalayarak çalışır. Ancak bu döngüye girdiğinde showhenüz çalışmadı. Bu nedenle, dizeden ilk karakteri kopyalamaya gittiğinde Haskell, o karakteri hesaplamak için gereken showve quicksortçağrılarının kesirini değerlendirir . Sonra bir sonraki karaktere geçer. Her üç functions- yürütülmesi Yani , ve - dönüşümlü olarak. artımlı olarak yürütülür ve nerede kaldığını hatırlamak için değerlendirilmemiş thunks grafiğini bırakır.putStrLnputStrLnshowquicksortquicksort

Bu, şimdiye kadarki herhangi bir programlama diline aşina iseniz, tahmin edebileceğinizden çılgınca farklıdır. quicksortHaskell'de bellek erişimleri ve hatta karşılaştırma sıralaması açısından gerçekte nasıl davrandığını görselleştirmek kolay değil . Sadece davranışı gözlemleyebilseydiniz, kaynak kodunu değil, hızlı bir sıralama olarak ne yaptığını anlayamazsınız. .

Örneğin, hızlı sınıflandırmanın C sürümü, ilk özyinelemeli aramadan önce tüm verileri bölümler. Haskell sürümünde, sonucun ilk öğesi hesaplanır (ve hatta ekranınızda görünebilir), ilk bölümün çalışması bitmeden önce - aslında üzerinde herhangi bir çalışma yapılmadan öncegreater .

PS Haskell kodu, hızlı sıralama ile aynı sayıda karşılaştırma yapsaydı, daha hızlı sıralama benzeri olurdu; Kod yazıldığı şekliyle iki kat daha fazla karşılaştırma yapar çünkü lesserve greaterbağımsız olarak hesaplanacak şekilde belirtilir, liste boyunca iki doğrusal tarama yapar. Elbette prensip olarak derleyicinin ekstra karşılaştırmaları ortadan kaldıracak kadar akıllı olması mümkündür; veya kod kullanmak için değiştirilebilirData.List.partition .

PPS Haskell algoritmalarının beklediğiniz şekilde davranmamaya dönüşen klasik örneği, hesaplama asalları için Eratosthenes'in eleği .


2
lpaste.net/108190 . - "ormansızlaştırılmış ağaç sıralaması" yapıyor, bununla ilgili eski bir reddit dizisi var. bakınız stackoverflow.com/questions/14786904/… ve ilgili.
Will Ness

1
görünüyor program aslında ne yaptığının çok iyi karakterizasyonu olduğunu, evet.
Jason Orendorff

elek sözler yeniden, eşdeğer olarak yazılmıştır primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..], onun en acil sorun belki daha net olacaktır. Ve bu , gerçek elek algoritmasına geçmeyi düşünmeden önce .
Will Ness

Kodun "göründüğü gibi" tanımına kafam karıştı. Bana Kodunuz "bakışlar" olarak adlandırdığı gibi putStrLnbir thunked uygulanması hangi showbir thunked uygulamasına quicksortbir liste değişmez değere --- ve bu kadar yaptığı tam olarak budur! (optimizasyondan önce --- ancak bazen C kodunu optimize edilmiş assembler ile karşılaştırın!). Belki de "tembel değerlendirme sayesinde, bir Haskell programı benzer görünümlü kodun diğer dillerde yaptığını yapmaz" demek istiyorsunuz?
Jonathan Cast

4
@jcast Bu konuda C ve Haskell arasında pratik bir fark olduğunu düşünüyorum. Bu tür bir konu hakkında bir yorum başlığında hoş bir tartışma yürütmek gerçekten zor, gerçek hayatta bunu kahve içerek yapmak isterdim. Harcayacak bir saatiniz varsa Nashville'e gelirseniz haber verin!
Jason Orendorff

13

Çoğu insanın güzel Haskell Quicksort'un "gerçek" bir Quicksort olmadığını söylemesinin nedeninin yerinde olmaması gerçeği olduğuna inanıyorum - açıkça, değişmez veri türlerini kullanırken olamaz. Ancak bunun "hızlı" olmadığına dair bir itiraz da var: kısmen pahalı ++ nedeniyle ve ayrıca bir boşluk sızıntısı olduğu için - daha küçük elemanlarda özyinelemeli çağrıyı yaparken girdi listesine bağlı kalıyorsunuz ve bazı durumlarda - örneğin liste azaldığında - bu ikinci dereceden alan kullanımına neden olur. (Doğrusal uzayda çalışmasını sağlamanın, değişmez verileri kullanarak "yerinde" yapabileceğiniz en yakın yöntem olduğunu söyleyebilirsiniz.) Her iki soruna da toplama parametreleri, tupling ve füzyon kullanarak temiz çözümler vardır; Richard Bird S7.6.1'e bakınız


4

Tamamen işlevsel ortamlarda, öğelerin yerinde değiştirilmesi fikri değildir. Değişken dizilerle bu iş parçacığındaki alternatif yöntemler saflığın ruhunu kaybetti.

Hızlı sıralamanın temel sürümünü (en etkileyici sürüm olan) optimize etmek için en az iki adım vardır.

  1. Doğrusal bir işlem olan birleştirmeyi (++) akümülatörler ile optimize edin:

    qsort xs = qsort' xs []
    
    qsort' [] r = r
    qsort' [x] r = x:r
    qsort' (x:xs) r = qpart xs [] [] r where
        qpart [] as bs r = qsort' as (x:qsort' bs r)
        qpart (x':xs') as bs r | x' <= x = qpart xs' (x':as) bs r
                               | x' >  x = qpart xs' as (x':bs) r
  2. Yinelenen öğeleri işlemek için üçlü hızlı sıralamaya (Bentley ve Sedgewick tarafından bahsedilen 3 yollu bölüm) optimize edin:

    tsort :: (Ord a) => [a] -> [a]
    tsort [] = []
    tsort (x:xs) = tsort [a | a<-xs, a<x] ++ x:[b | b<-xs, b==x] ++ tsort [c | c<-xs, c>x]
  3. 2 ve 3'ü birleştirin, Richard Bird'ün kitabına bakın:

    psort xs = concat $ pass xs []
    
    pass [] xss = xss
    pass (x:xs) xss = step xs [] [x] [] xss where
        step [] as bs cs xss = pass as (bs:pass cs xss)
        step (x':xs') as bs cs xss | x' <  x = step xs' (x':as) bs cs xss
                                   | x' == x = step xs' as (x':bs) cs xss
                                   | x' >  x = step xs' as bs (x':cs) xss

Veya alternatif olarak, çoğaltılan öğeler çoğunluk değilse:

    tqsort xs = tqsort' xs []

    tqsort' []     r = r
    tqsort' (x:xs) r = qpart xs [] [x] [] r where
        qpart [] as bs cs r = tqsort' as (bs ++ tqsort' cs r)
        qpart (x':xs') as bs cs r | x' <  x = qpart xs' (x':as) bs cs r
                                  | x' == x = qpart xs' as (x':bs) cs r
                                  | x' >  x = qpart xs' as bs (x':cs) r

Maalesef ortanca üç aynı etkiyle uygulanamaz, örneğin:

    qsort [] = []
    qsort [x] = [x]
    qsort [x, y] = [min x y, max x y]
    qsort (x:y:z:rest) = qsort (filter (< m) (s:rest)) ++ [m] ++ qsort (filter (>= m) (l:rest)) where
        xs = [x, y, z]
        [s, m, l] = [minimum xs, median xs, maximum xs] 

çünkü aşağıdaki 4 durumda hala kötü performans gösteriyor:

  1. [1, 2, 3, 4, ...., n]

  2. [n, n-1, n-2, ..., 1]

  3. [m-1, m-2, ... 3, 2, 1, m + 1, m + 2, ..., n]

  4. [n, 1, n-1, 2, ...]

Tüm bu 4 vaka, zorunlu ortanca üç yaklaşımı ile iyi bir şekilde ele alınmaktadır.

Aslında, tamamen işlevsel bir ayar için en uygun sıralama algoritması yine de birleştirme-sıralama, ancak hızlı sıralama değil.

Ayrıntılı bilgi için lütfen devam eden yazımı https://sites.google.com/site/algoxy/dcsort adresinde ziyaret edin.


Kaçırdığınız başka bir optimizasyon daha var: alt listeleri oluşturmak için 2 filtre yerine bölüm kullanın (veya 3 alt liste oluşturmak için benzer bir iç işlevde foldr).
Jeremy Liste

3

Neyin gerçek hızlı sıralama olup olmadığı konusunda net bir tanım yoktur.

Buna gerçek bir hızlı sıralama değil diyorlar çünkü yerinde sıralamıyor:

Yerinde C sıralarıyla gerçek hızlı sıralama


-1

Çünkü listeden ilk elemanın alınması çok kötü çalışma süresine neden olur. Ortanca 3 kullanın: ilk, orta, son.


2
Liste rastgele ise ilk öğeyi almak tamamdır.
Keith Thompson

2
Ancak sıralı veya neredeyse sıralı bir listeyi sıralamak yaygındır.
Joshua

7
Ama qsort IS O(n^2)
Thomas Eding

8
qsort ortalama n log n, en kötü n ^ 2'dir.
Joshua

3
Teknik olarak, girdi zaten sıralanmadıkça veya neredeyse sıralanmadıkça rastgele bir değer seçmekten daha kötü değildir. Kötü pivotlar, medyandan uzak olan pivotlardır; ilk öğe, minimum veya maksimuma yakınsa yalnızca kötü bir pivottur.
Platinum Azure

-1

Haskell'de herhangi birinden hızlı sıralama yazmasını isteyin ve esasen aynı programı alacaksınız - bu kesinlikle hızlı sıralama. İşte bazı avantajlar ve dezavantajlar:

Pro: Kararlı olarak "gerçek" hızlı sıralamayı geliştirir, yani eşit öğeler arasında sıra sırasını korur.

Pro: O (n) kez meydana gelen bazı değerler nedeniyle ikinci dereceden davranıştan kaçınan üç yollu bölünmeye (<=>) genellemek önemsizdir.

Pro: Okuması daha kolay - filtre tanımının eklenmesi gerekse bile.

Con: Daha fazla bellek kullanıyor.

Eksileri: Pivot seçimini, belirli düşük entropi sıralamalarında ikinci dereceden davranışı önleyebilecek daha fazla örnekleme yoluyla genelleştirmek maliyetlidir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.