Haskell의 웹 사이트는 아래와 같이 매우 매력적인 5 줄 퀵 정렬 기능을 소개 합니다.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
또한 “C의 True quicksort” 도 포함됩니다 .
// 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 버전 아래의 링크는 ‘소개에 인용 된 빠른 정렬이 “실제”빠른 정렬이 아니며 C 코드처럼 긴 목록에 맞게 확장되지 않는다는 내용의 페이지로 연결됩니다.
위의 Haskell 함수가 진정한 퀵 정렬이 아닌 이유는 무엇입니까? 더 긴 목록으로 확장하는 데 어떻게 실패합니까?
답변
진정한 퀵소트에는 두 가지 아름다운 측면이 있습니다.
- 나누고 정복하십시오 : 문제를 두 개의 작은 문제로 나누십시오.
- 요소를 제자리에서 분할합니다.
짧은 Haskell 예제는 (1)을 보여 주지만 (2)는 보여주지 않습니다. 기술을 아직 모르면 (2)가 어떻게 수행되는지 명확하지 않을 수 있습니다!
답변
Haskell의 진정한 인플레 이스 퀵소트 :
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
답변
다음은 “진정한”quicksort C 코드를 Haskell로 음역 한 것입니다. 자신을 보호하십시오.
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
재미 있지 않았나요? 나는 실제로 let
처음과 where
함수의 끝 에서이 큰 부분을 잘라내어 모든 도우미를 정의하여 앞의 코드를 다소 예쁘게 만듭니다.
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
그리고 여기에서 그것이 작동하는지 확인하는 멍청한 테스트입니다.
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에서 명령형 코드를 자주 작성하지 않기 때문에이 코드를 정리할 수있는 많은 방법이 있다고 확신합니다.
그래서 뭐?
위의 코드가 매우 길다는 것을 알 수 있습니다. 핵심은 C 코드만큼 길지만, 각 줄은 종종 좀 더 장황합니다. 이것은 C가 당연한 것으로 생각할 수있는 많은 불쾌한 일을 비밀리에 수행하기 때문입니다. 예 : a[l] = a[h];
. 이것은 변경 가능한 변수 l
및 h
에 액세스 a
한 다음 변경 가능한 배열에 액세스 한 다음 변경 가능한 배열을 변경합니다 a
. 이런 돌연변이, 배트맨! Haskell에서 변경 및 변경 가능한 변수에 액세스하는 것은 명시 적입니다. “가짜”qsort는 여러 가지 이유로 매력적이지만 그중 가장 중요한 점은 돌연변이를 사용하지 않는다는 것입니다. 이 자체적으로 부과 된 제한은 한눈에 이해하기 훨씬 쉽게 만듭니다.
답변
제 생각에는 그것이 “진정한 퀵 정렬이 아닙니다”라고 말하는 것은 그 사건을 과장합니다. 저는 이것이 Quicksort 알고리즘 의 유효한 구현이라고 생각합니다 . 특히 효율적인 것은 아닙니다.
답변
이 주장이 시도하는 경우는 quicksort가 일반적으로 사용되는 이유는 그것이 제자리에 있고 결과적으로 상당히 캐시 친화적이기 때문이라고 생각합니다. Haskell 목록에는 이러한 이점이 없기 때문에 주요 이유 d’ être가 사라지고 병합 정렬을 사용하여 O (n log n) 을 보장 하는 반면 퀵 정렬을 사용하면 무작위 화 또는 복잡함을 사용해야합니다. 최악의 경우 O (n 2 ) 런타임 을 피하기위한 분할 스키마 .
답변
게으른 평가 덕분에 Haskell 프로그램은 보이는대로 수행 하지 않습니다 (거의 할 수 없습니다 ).
이 프로그램을 고려하십시오.
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
열성적인 언어에서는 먼저 quicksort
실행 한 다음 show
, 그런 다음 putStrLn
. 함수의 인수는 해당 함수가 실행되기 전에 계산됩니다.
Haskell에서는 그 반대입니다. 함수가 먼저 실행되기 시작합니다. 인수는 함수가 실제로 사용할 때만 계산됩니다. 그리고 목록과 같은 복합 인수는 각 부분이 사용됨에 따라 한 번에 하나씩 계산됩니다.
그래서이 프로그램에서 가장 먼저 일어나는 일은 putStrLn
실행 을 시작하는 것입니다.
GHC의 작업 구현은putStrLn
String 인수의 문자를 출력 버퍼에 복사합니다. 그러나이 루프에 들어가면 show
아직 실행되지 않았습니다. 따라서 문자열에서 첫 번째 문자를 복사 할 때 Haskell은 해당 문자 를 계산 하는 데 필요한 show
및 quicksort
호출 의 일부를 평가합니다 . 그런 다음 다음 문자로 이동합니다. 따라서 세 가지 함수 인 ,, 및 모두의 실행 이 인터리브됩니다. 점진적으로 실행되어 평가되지 않은 썽크 그래프를 남겨두고 중단 된 위치를 기억합니다.putStrLn
putStrLn
show
quicksort
quicksort
이제 이것은 다른 프로그래밍 언어에 익숙하다면 예상 할 수있는 것과는 크게 다릅니다. quicksort
메모리 액세스 또는 비교 순서 측면에서 Haskell에서 실제로 어떻게 작동하는지 시각화하는 것은 쉽지 않습니다 . 소스 코드가 아닌 동작 만 관찰 할 수 있다면 Quicksort로 무엇을하는지 인식하지 못할 것 입니다.
예를 들어 C 버전의 quicksort는 첫 번째 재귀 호출 전에 모든 데이터를 분할합니다. Haskell 버전에서는 첫 번째 파티션 실행이 완료 되기 전에 결과의 첫 번째 요소가 계산되고 화면에 나타날 수도 있습니다. 실제로에서 작업이 완료 되기 전에 말입니다 greater
.
추신 : Haskell 코드는 Quicksort와 같은 수의 비교를했다면 더 quicksort와 비슷할 것입니다. 작성된 코드는 많아 비교 두번 수행 lesser
하고 greater
목록을 통해 2 개의 선형 스캔하고, 독립적으로 계산하도록 동작한다. 물론 원칙적으로 컴파일러가 추가 비교를 제거 할만큼 똑똑 할 수 있습니다. 또는을 사용하도록 코드를 변경할 수 있습니다 Data.List.partition
.
PPS 하스켈 알고리즘의 고전적인 예가 예상대로 작동하지 않는 것으로 밝혀진 것은 소수 계산을위한 에라토스테네스 의 체입니다 .
답변
대부분의 사람들이 예쁜 Haskell Quicksort가 “진정한”Quicksort가 아니라고 말하는 이유는 그것이 제자리에 있지 않다는 사실입니다. 분명히 불변 데이터 유형을 사용할 때는 불가능합니다. 그러나 “빠르지 않다”는 이의도 있습니다. 부분적으로는 값 비싼 ++ 때문이고 공간 누수가 있기 때문입니다. 더 적은 요소에 대해 재귀 호출을 수행하는 동안 입력 목록에 매달리고 있습니다. 경우에 따라-예를 들어 목록이 감소하는 경우-이로 인해 2 차 공간이 사용됩니다. (선형 공간에서 실행하는 것이 불변 데이터를 사용하여 “인플레 이스”에 가장 가깝다고 말할 수 있습니다.) 누적 매개 변수, tupling 및 융합을 사용하여 두 문제에 대한 깔끔한 솔루션이 있습니다. Richard Bird의 S7.6.1 참조