Сложная временная сложность

Я знаю, что quicksort имеет среднюю временную сложность O(n log n). Псевдо-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? А именно, O(n log n)? Поскольку добавление и раздел по-прежнему имеют линейную временную сложность, даже если они неэффективны.

Ответ 1

Эта "быстрая сортировка" - фактически обезлесенный вид дерева: 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)

Ответ 2

Я согласен с вашим предположением, что средняя временная сложность по-прежнему равна 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  

Затем анализируется средняя временная сложность, выбирая "<" - сравнения в строках 6 и 7 в качестве доминирующей операции в этом алгоритме и, наконец, приходит к выводу, что средняя временная сложность равна O (n log n). Поскольку стоимость строки "заказывает сегмент массива x_l,... x_r таким образом, что..." не учитывается (только важная операция важна при анализе временной сложности, если вы хотите найти границы), я думаю "потому что он должен сделать два прохода списка, когда он разбивает его", это не проблема, так же, как ваша версия Haskell займет примерно в два раза больше этого шага. То же самое справедливо и для приложения-операции, и я согласен с тем, что это ничего не добавляет к асимптотическим затратам:

Поскольку добавление и раздел по-прежнему имеют линейную временную сложность, даже если они неэффективны.

Для удобства предположим, что это добавляет "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).

Кроме того, выбор первого элемента в качестве стержня не лучший выбор.

Я думаю, что выбор элемента поворота не имеет значения здесь, потому что в среднем анализе случая вы предполагаете равномерное распределение элементов в массиве. Вы не можете знать, из какого места в массиве вы должны выбрать его, и поэтому вам необходимо рассмотреть все эти случаи, в которых ваш элемент-поворот (независимо от того, в каком месте списка вы его принимаете) является i-м наименьшим элементом вашего списка, для я = 1... r.

Ответ 3

Я могу предложить вам тест времени выполнения Ideone.com, который, по-видимому, показывает более или менее линейные временные интервалы для обоих (+ +) и тот, который использует технику аккумулятора из ответа Landei, а также другой, используя одноразовый три разделение диска. По упорядоченным данным это становится квадратичным или хуже для всех из них.

-- 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 ). Тайминги очень приблизительны, так как идеон не очень надежный, но иногда достаточно далеко, но для проверки его сложности достаточно.

Таким образом, эти данные показывают, что однопроходный раздел быстрее на 1,5x-2x, чем двухпроходные, и что использование (++) никоим образом не замедляет работу. То есть "операции добавления" не являются "дорогостоящими". Квадратичное поведение или (++)/append - это городской миф. в контексте Haskell (редактирование:... т.е. в контексте охраняемой рекурсии /хвостовая рекурсия по модулю cons, cf. этот ответ) (обновлено: как пользователь: AndrewC объясняет, он очень велик с левой складкой, линейный, когда (++) используется с правой складкой, больше об этом здесь и здесь).

Ответ 4

Я не знаю, насколько это улучшает сложность выполнения, но с помощью аккумулятора вы можете избежать дорогостоящего (++):

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)

Ответ 5

Посмотрите на настоящую операционную систему O (n log n), которая будет работать как с массивами, так и с списками: http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.23.4398&rep=rep1&type=pdf Его довольно легко реализовать в Common Lisp, и он превосходит реализацию сортировки многих коммерческих лиз.

Ответ 6

Да, эта версия имеет ту же асимптотическую сложность, что и классическая версия - вы заменяете линейное время partition на: два прохода (< и >=), и у вас есть дополнительное линейное время ++ (который включает в себя линейное перераспределение/копирование). Таким образом, он представляет собой здоровенный постоянный фактор хуже, чем локальный раздел, но он все еще линейный. Все остальные аспекты алгоритма одинаковы, поэтому здесь выполняется один и тот же анализ, который дает среднее значение O (n log n) для "истинной" (то есть на месте) быстрой сортировки.