伪快速排序时间复杂度

时间:2012-07-06 03:58:34

标签: haskell time-complexity quicksort

我知道快速排序的平均时间复杂度为O(n log n)。伪快速排序(当你从足够远的地方看它,具有适当高的抽象级别时只是一个快速排序),通常用于演示函数式语言的简洁性如下(在Haskell中给出):

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) = quicksort [y | y<-xs, y<p] ++ [p] ++ quicksort [y | y<-xs, y>=p]

好的,所以我知道这件事有问题。最大的问题是它没有排序,这通常是quicksort的一大优势。即使这没关系,它仍然需要比典型的快速排序更长的时间,因为它在分区时必须进行两次列表传递,然后它会进行昂贵的附加操作以将其拼接回来。此外,选择第一个元素作为枢轴并不是最佳选择。

即使考虑所有这些,这个快速排序的平均时间复杂度与标准快速排序相同吗?即O(n log n)?因为追加和分区仍然具有线性时间复杂度,即使它们效率低下。

6 个答案:

答案 0 :(得分:8)

这个“快速排序”实际上是砍伐森林的树种: 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)

答案 1 :(得分:4)

我同意您的假设,即平均时间复杂度仍为O(n log n)。我不是专家,100%肯定,但这些是我的想法:

这是就地快速排序的伪代码:(调用快速排序,其中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行中的“&lt;”比较作为该算法中的主导操作,并最终得出平均时间复杂度为O(n log n)的结论。由于线路的成本“以这样的方式对数组段x_l,... x_r进行排序”不被考虑(如果你想找到界限,只有主导操作在时间复杂度分析中很重要),我认为“因为它在分区时必须对列表进行两次传递”不是问题,因为在这一步中你的Haskell版本只需要大约两倍的时间。对于附录操作也是如此,我同意这一点,这对渐近成本没有任何影响:

  

因为追加和分区仍然具有线性时间复杂度,即使它们效率低下。

为了方便起见,我们假设这会将“n”加到我们的时间复杂度成本上,因此我们得到“O(n log n + n)”。因为存在n log n的自然数o>对于大于o的所有自然数的n,如果n为真,则可以将n log n + n估计到顶部2 n log n,并将n log n估计到底部,因此n log n + n = O(n log n)。

  

此外,选择第一个元素作为枢轴并不是最佳选择。

我认为枢轴元素的选择在这里是无关紧要的,因为在平均情况分析中,您假设元素在数组中的均匀分布。您无法知道阵列中的哪个位置应该选择它,因此您必须考虑所有这些情况,其中您的pivot元素(独立于列表的哪个位置)是i-st最小元素你的清单,对于i = 1 ... r。

答案 2 :(得分:4)

我可以在Ideone.com上为您提供运行时测试,它似乎显示基于(++)的版本或使用来自Landei's answer的累加器技术的或多或少的线性运行时间,以及另一个,使用one-pass three-way partitioning。在有序数据上,这对于所有这些都变为二次或更差。

-- 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)

此处empirical run-time complexities估算为O(n^a) a = log( t2/t1 ) / log( n2/n1 )时间是非常近似的,因为意外不是非常可靠,偶尔会有很多人,但是为了检查时间复杂性就足够了。

因此,这些数据似乎表明单通道分区 比双通道方案快1.5倍-2倍,并且使用(++)绝不会减慢速度 - at all 。即“追加行动”根本不是“代价高昂”。 二次行为或(++)/ append似乎是一个都市神话 - 当然在Haskell语境中编辑: ...即在受保护递归的上下文中tail recursion modulo cons; cf. this answer)(更新:user:AndrewC explains,它实际上是二次方 left 折叠; (++)折叠一起使用时的线性;有关此herehere的更多信息。


以后添加:为了保持稳定,三向分区快速排序版本也应该以自上而下的方式构建其部分:

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:))

(表现未经测试)。

答案 3 :(得分:1)

我不知道这会提高运行时间的复杂程度,但通过使用累加器可以避免昂贵的(++)

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)

答案 4 :(得分:0)

在这里查看适用于数组和列表的真正O(n log n)快速排序: http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.23.4398&rep=rep1&type=pdf 在Common Lisp中实现起来相当容易,并且它优于许多商业lisps的排序实现。

答案 5 :(得分:0)

是的,此版本与经典版本具有相同的渐近复杂度 - 您将线性时间partition替换为:两次传递(<和{{1你有额外的线性时间>=(包括线性重新分配/复制)。所以这是一个比原地分区更糟糕的常数因素,但它仍然是线性的。该算法的所有其他方面都是相同的,因此为“真实”(即就地)快速排序提供O(n log n)平均情况的相同分析仍然存在。