Псевдо-сложность быстрой сортировки время
Я знаю, что quicksort имеет O(n log n)
средняя сложность. Псевдо-quicksort (который является только quicksort, когда вы смотрите на него достаточно далеко, с соответствующим высоким уровнем абстракции), который часто используется для демонстрации краткости функциональных языков, выглядит следующим образом (приведено в Haskell):
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (p:xs) = quicksort [y | y<-xs, y<p] ++ [p] ++ quicksort [y | y<-xs, y>=p]
хорошо, я знаю, что у этой штуки проблемы. Самая большая проблема заключается в том, что он не сортируется на месте, что обычно является большим преимуществом quicksort. Даже если бы это не имело значения, это все равно заняло бы больше времени, чем типичный quicksort, потому что он должен сделать два прохода списка, когда он разбивает его, и он делает дорогостоящие операции добавления, чтобы соединить его обратно. Кроме того, выбор первого элемента в качестве оси вращения не является лучшим выбором.
но даже учитывая все это, не является средним времени сложность этой quicksort такая же, как стандартная quicksort? А именно:O(n log n)
? Потому что добавление и раздел по-прежнему имеют линейную временную сложность, даже если они неэффективны.
6 ответов
этот "quicksort" на самом деле обезлесенный вид дерева: http://www.reddit.com/r/programming/comments/2h0j2/real_quicksort_in_haskell
data Tree a = Leaf | Node a (Tree a) (Tree a)
mkTree [] = Leaf
mkTree (x:xs) = Node x (mkTree (filter (<= x) xs)) (mkTree (filter (x <) xs))
двоичное дерево несбалансировано, поэтому O(N^2) в худшем случае и O (N*Log N) средняя сложность для построения дерева поиска.
foldTree f g Leaf = g
foldTree f g (Node x l r) = f x (foldTree f g l) (foldTree f g r)
treeSort l = foldTree (\x lft rht -> lft++[x]++rht) [] (mkTree l)
алгоритм поиска имеет O (N^2) наихудший случай и O (N*Log N) средний случай сложность.
хорошо сбалансированный:
Prelude> let rnds = iterate step where step x = (75*x) `mod` 65537
Prelude> length . quicksort . take 4000 . rnds $ 1
4000
(0.08 secs, 10859016 bytes)
Prelude> length . quicksort . take 8000 . rnds $ 1
8000
(0.12 secs, 21183208 bytes)
Prelude> length . quicksort . take 16000 . rnds $ 1
16000
(0.25 secs, 42322744 bytes)
не-так-хорошо-сбалансированный:
Prelude> length . quicksort . map (`mod` 10) $ [1..4000]
4000
(0.62 secs, 65024528 bytes)
Prelude> length . quicksort . map (`mod` 10) $ [1..8000]
8000
(2.45 secs, 241906856 bytes)
Prelude> length . quicksort . map (`mod` 10) $ [1..16000]
16000
(9.52 secs, 941667704 bytes)
Я согласен с вашим предположением, что средняя Сложность время еще есть O(n log n)
. Я не специалист и на 100% уверен, но это мои мысли:
это псевдо-код на месте quicksort: (вызов quicksort с L=1 и R=длина массива)
Quicksort(l,r)
--------------
IF r-l>=1 THEN
choose pivot element x of {x_l,x_l+1,...,x_r-1,x_r}
order the array-segment x_l,...x_r in such a way that
all elements < x are on the left side of x // line 6
all elements > x are on the right side of x // line 7
let m be the position of x in the 'sorted' array (as said in the two lines above)
Quicksort(l,m-1);
Quicksort(m+1,r)
FI
анализ сложности среднего времени затем рассуждает, выбирая "
потому что добавления и раздел по-прежнему имеют линейную временную сложность, даже если они неэффективны.
для удобства предположим, что это добавляет "n" к нашим затратам на сложность времени, так что у нас есть "O(N log n+n)". Поскольку существует натуральное число o для этого N log n > n для всех натуральных чисел, больших o, вы можете оценить N log n +n сверху на 2 n log n и снизу на n log n, поэтому N log n+n = O (N log n).
далее выбор первого элемента в качестве опорного не лучший выбор.
Я думаю, что выбор элемента pivot здесь не имеет значения, потому что в анализе среднего случая вы предполагаете равномерное распределение элементов в массиве. Вы не можете знать, из какого места в массиве, вы должны выбрать его, и поэтому вы должны рассмотреть все эти случаи, в которых ваш стержень-элемент (независимо от место списка, которое вы берете) является i-м наименьшим элементом вашего списка, для i=1...r.
Я предлагаю вам выполнить тест время на Ideone.com, которые, кажется, показывают более или менее linearithmic время как (++) версии и методом аккумулятор от Landei это!--8-->, а также другой, используя один проход три-способ разметки. On приказал данные это становится квадратичным или хуже для всех из них.
-- random: 100k 200k 400k 800k
-- _O 0.35s-11MB 0.85s-29MB 1.80s-53MB 3.71s-87MB n^1.3 1.1 1.0
-- _P 0.36s-12MB 0.80s-20MB 1.66s-45MB 3.76s-67MB n^1.2 1.1 1.2
-- _A 0.31s-14MB 0.62s-20MB 1.58s-54MB 3.22s-95MB n^1.0 1.3 1.0
-- _3 0.20s- 9MB 0.41s-14MB 0.88s-24MB 1.92s-49MB n^1.0 1.1 1.1
-- ordered: 230 460 900 1800
-- _P 0.09s 0.33s 1.43s 6.89s n^1.9 2.1 2.3
-- _A 0.09s 0.33s 1.44s 6.90s n^1.9 2.1 2.3
-- _3 0.05s 0.15s 0.63s 3.14s n^1.6 2.1 2.3
quicksortO xs = go xs where
go [] = []
go (x:xs) = go [y | y<-xs, y<x] ++ [x] ++ go [y | y<-xs, y>=x]
quicksortP xs = go xs where
go [] = []
go (x:xs) = go [y | y<-xs, y<x] ++ (x : go [y | y<-xs, y>=x])
quicksortA xs = go xs [] where
go [] acc = acc
go (x:xs) acc = go [y | y<-xs, y<x] (x : go [y | y<-xs, y>=x] acc)
quicksort3 xs = go xs [] where
go (x:xs) zs = part x xs zs [] [] []
go [] zs = zs
part x [] zs a b c = go a ((x : b) ++ go c zs)
part x (y:ys) zs a b c =
case compare y x of
LT -> part x ys zs (y:a) b c
EQ -> part x ys zs a (y:b) c
GT -> part x ys zs a b (y:c)
на эмпирические сложности времени выполнения оцениваются вот как O(n^a)
здесь a = log( t2/t1 ) / log( n2/n1 )
. тайминги очень приблизительны, так как ideone не очень надежны со случайными дальними дальними, но для проверки сложности времени этого достаточно.
таким образом, эти данные, похоже, указывают на то, что однопереходный раздел is быстрее на 1.5 x-2x, чем двухпроходные схемы, и что с помощью (++)
никоим образом не замедляет работу - на всех. Т. е. "операции добавления" не являются "дорогостоящими" вообще. квадратичная поведение или (++) /append кажется городским мифом - в контексте Haskell, конечно (edit: ... т. е. в контексте охраняемой рекурсии/хвост рекурсии по модулю минусы; ср. ответ) (обновление: as пользователь:AndrewC объясняется, это действительно квадратично с левый складной; линейный, когда (++)
используется право складной; подробнее об этом здесь и здесь).
позже: чтобы быть стабильным, трехсторонняя версия quicksort для разбиения должна также строить свои части сверху вниз:
q3s xs = go xs [] where
go (x:xs) z = part x xs go (x:) (`go` z)
go [] z = z
part x [] a b c = a [] (b (c []))
part x (y:ys) a b c =
case compare y x of
LT -> part x ys (a . (y:)) b c
EQ -> part x ys a (b . (y:)) c
GT -> part x ys a b (c . (y:))
(работоспособность не проверял).
Я не знаю, насколько это повышает сложность выполнения, но и с помощью аккумулятора вы можете избежать дорогостоящего (++)
:
quicksort xs = go xs [] where
go [] acc = acc
go (x:xs) acc = go [y | y<-xs, y<x] (x : go [y | y<-xs, y>=x] acc)
найдите здесь true o (n log n) quicksort, который будет работать как с массивами, так и со списками : http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.23.4398&rep=rep1&type=pdf Его довольно легко реализовать в Common Lisp,и он превосходит реализацию многих коммерческих Lisp.
Да, эта версия имеет то же самое асимптотические сложность как классическая версия -- вы заменяете линейное время partition
with: два прохода (<
и >=
), и у вас есть дополнительное линейное время ++
(который включает линейное перераспределение / копирование). Таким образом, это здоровенный постоянный фактор хуже, чем разбиение на месте, но он все еще линейный. Все остальные аспекты алгоритма одинаковы, поэтому тот же анализ, который дает o (n log n) средний случай для " true "(т. е. на месте) quicksort все еще держится здесь.