接受随机比较器的排序算法


22

通用排序算法通常将一组数据进行排序,并使用一个比较器函数来比较两个单独的元素。如果比较器是顺序关系¹,则算法的输出是排序的列表/数组。

我想知道哪种排序算法实际上可以与不是顺序关系的比较器一起使用(特别是在每次比较时都返回随机结果的比较器)。所谓“工作”,是指他们继续返回其输入的排列并以其通常引用的时间复杂度运行(而不是总是降级为最坏的情况,或者陷入无限循环,或者缺少元素)。结果的顺序将是不确定的。更好的是,当比较器是投币游戏时,所得排序将是均匀分布。

从我粗略的脑力计算来看,似乎可以使用合并排序并保持相同的运行时成本并产生合理的随机排序。我认为,诸如快速排序之类的东西会退化,可能无法完成,也不公平。

如随机比较器所述,还有哪些其他排序算法(合并排序除外)可以工作?


  1. 作为参考,比较器是一个顺序关系,如果它是一个适当的函数(确定性)并且满足顺序关系的公理:

    • 它是确定性的:compare(a,b)对于特定的对象ab总是返回相同的结果。
    • 它是可传递的: compare(a,b) and compare(b,c) implies compare( a,c )
    • 它是反对称的 compare(a,b) and compare(b,a) implies a == b

(假设所有输入元素都是不同的,那么自反性就不是问题。)

随机比较器违反所有这些规则。但是,有些比较器不是顺序关系而是随机的(例如,它们可能只违反一个规则,并且仅针对集合中的特定元素)。


(1)比较功能稳定意味着什么?(2)“不稳定”和“随机”是同义词吗?
伊藤刚(Tsuyoshi Ito)2012年

“以它们通常引用的时间复杂度运行(而不是降级为最坏的情况”)-通常引用的时间复杂度最坏的情况!“排序将是公平的随机排序” –“公平”是指统一吗?您是否也认为比较器是统一的?
Raphael

也许不是在形式理论中,而是在实践中(编程语言),许多事情都是在摊销时间内引用的。例如,快速排序通常显示为但实际上是O n 2O(logn)O(n2)
edA-qa mort-ora-y 2012年

4
@ edA-qamort-ora-y:(1)您的意思是,而不是O log n 。(2)这不是“ 摊销时间 ”的意思;您的意思是“ 预期时间 ”,或更不正式地是“典型时间”。O(nlogn)O(logn)
JeffE 2012年

1
没有人解决上面提出的(对我而言)一个更有趣的问题:哪种排序算法(如果有)具有以下性质:如果比较器是硬币翻转,则结果是统一排列。

Answers:


13

因此,基本上,您想知道是否有任何排序算法,如果给定类似于以下内容的比较函数,则该算法不会降低其平均情况:

int Compare(object a, object b) { return Random.Next(-1,1); }

...其中Random.Next()是将在指定的下限和上限之间生成随机生成的整数的某些方法。

答案实际上是,大多数基本排序算法将根据其平均情况执行,因为它们至少满足以下两个条件之一:

  1. 两个唯一元素之间的比较不会在排序中进行两次,和/或
  2. 在排序的每个迭代中,确定至少一个元素的正确位置,以便不再对元素进行比较。

例如,SelectionSort遍历未排序元素的子列表,找到“最小”和/或“最大”元素(通过将每个元素与目前为止最大的元素进行比较),将其放置在正确的位置并重复。结果,即使使用不确定的比较器,算法在每次迭代结束时也会发现它认为是最小或最大的值,将其与要确定的位置的元素交换,而从不考虑再次该元素,因此它符合条件2。但是,在此过程中,可以多次比较A和B(作为最极端的示例,请考虑对以相反顺序排序的数组进行多次SelectionSort传递),因此它违反了条件1 。

MergeSort符合条件1,但不符合条件2;当合并子数组时,同一子数组(左侧或右侧)中的元素不会相互比较,因为已经确定数组那侧的元素彼此之间是有序的;该算法仅将每个子数组中未合并最少的元素与另一个子数组进行比较,以确定哪个较小,应该在合并列表中排在下一个。这意味着任何两个唯一的对象A和B将最多相互比较一次,但是完整的集合中任何给定元素的“最终”索引在算法完成之前是未知的。

即使InsertionSort的总体策略和复杂性看起来更像SelectionSort,它也仅服从条件1。将每个未排序的元素与已排序的元素(从大到小)进行比较,直到找到一个小于被检查元素的元素。在该点插入元素,然后考虑下一个元素。结果是,任何A和B的相对顺序都是通过一次比较确定的,并且从未进行过A和B之间的进一步比较,但是只有考虑了所有元素之后才能知道任何元素的最终位置。

QuickSort服从两者条件。在每个级别上,均选择并布置枢轴,以使“左侧”包含小于枢轴的元素,而“右侧”包含大于枢轴的元素。该级别的结果是QuickSort(左)+枢轴+ QuickSort(右),这基本上意味着枢轴元素的位置是已知的(一个索引大于左侧的长度),该枢轴从未与任何其他元素进行比较在将其选择为枢轴之后(它可能已经与以前的枢轴元素进行了比较,但是这些元素也是已知的,并且不包含在任何子数组中),并且永远不会在枢轴的相对侧出现的任何A和B比较。在纯QuickSort的大多数实现中,基本情况是一个元素,此时其当前索引是其最终索引,因此不做进一步的比较。

(2/3)N1)。随着比较器结果的最大绝对值增加,任何一次比较返回负数或零的概率都降低到.5,从而使算法结束的可能性大大降低(99枚硬币翻转所有着陆头的可能性) ,这基本上可以归结为1.2 * 10 30中的1

长时间编辑:有一些专门设计为“不做”的示例的“排序”,其中包含一个随机比较器。也许最著名的是BogoSort。“给出列表,如果列表顺序不正确,请随机排序列表,然后再次检查”。从理论上讲,它将最终达到正确的值排列,就像上面的“非优化的BubbleSort”一样,但是平均情况是阶乘时间(N!/ 2),并且由于生日问题(在经过足够的随机排列后,变得比唯一排列更有可能遇到重复排列的排列),这种算法永远无法完成的非零可能性是,该算法没有时间限制。


条件2也可以涵盖快速分类吗?还是每个迭代都小于最后一个迭代会是第三个条件。
edA-qa mort-ora-y 2012年

在我看来,QuickSort可以同时满足这两个条件。在高效的QuickSorts中,您选择枢轴,然后将每个元素与其进行比较,并交换枢轴错误“一侧”上的元素。排列完元素后,该函数将返回QuickSort(左)+ Pivot + QuickSort(右),并且Pivot不会向下传递到较低级别。因此,两个条件都成立。您永远不会多次比较任何唯一的a和b,并且在完成其他元素的排列时已经确定了枢轴的索引。
KeithS 2012年

很好的答案,但我不同意BubbleSort。当使用一致的比较器时,在第i次迭代中,BubbleSort知道i-1个最后一个元素处于最终位置,并且任何合理的BubbleSort实现都将在每次迭代中使用较少的元素,因此它也应该在n次迭代后停止。
鲍里斯·特雷瓦斯

经过一番思考,我倾向于同意你的看法。在X传递之后,最大的X值就在适当的位置,因此您可以减少每次传递的问题空间,因此有效的算法将遵循条件2。我将编辑
KeithS 2012年

您必须小心使用Quicksort实施。可以假设当遇到枢轴或大于枢轴的元素时,搜索不小于枢轴的元素将结束。并非一定如此。
gnasher729 '18 -10-19

10

O(n2)

n


编辑:问题是我第一次想到的更有趣,所以这里有进一步的评论:

comparecompare(x,y)=true1/2false1/2

insert x [] = [x]
insert x y:ys = if x < y then x:y:ys
                else y:insert x ys

sort_aux l e = match l with
                 [] -> e
                 x:xs -> sort_aux xs (insert x ys)

sort l = sort_aux l []

k=1nf(k)nlf(k)insertk:

compare

i=1ki2ii=1i2i=2

O(2n)O(n2)

给定这种统一的比较功能,计算出其他算法的平均运行时间将很有趣。


如果将同一元素多次选择为枢轴,则Quicksort可以重复比较(在列表中可能多次出现)。
拉斐尔

2
@Raphael:我的用词很可怜,我的意思之间反复比较,出现的元素,不发生一次以上的快速排序。
科迪2012年

1
@Gilles:我可能是错的,但是我不认为compare的可传递性对于大多数排序算法的运行时间至关重要。正确性是肯定的,但这不是问题的目的。
科迪2012年

@Gilles:OP并未询问实际排序的算法。他问的是当所有比较都被硬币翻转代替时,标准排序算法会发生什么情况。生成的算法不进行排序(除非可能性很小),但它们仍然是定义明确的算法。
JeffE 2012年

@JeffE我现在明白了。那不是我最初阅读问题的方式,但是考虑到问询者的评论,这就是我的意思。
吉尔(Gilles)'所以

2

具有公平随机比较器的Mergesort是不公平的。我没有证据,但是我有很强的经验证据。(公平是指均匀分布。)

module Main where

import Control.Monad
import Data.Map (Map)
import qualified Data.Map as Map
import System.Random (randomIO)

--------------------------------------------------------------------------------

main :: IO ()
main = do
  let xs = [0..9]
  xss <- replicateM 100000 (msortRand xs)
  print $ countFrequencies xss

msortRand :: [a] -> IO [a]
msortRand = msort (\_ _ -> randomIO)

countFrequencies :: (Ord a) => [[a]] -> [Map a Int]
countFrequencies [] = []
countFrequencies xss = foldr (\k m -> Map.insertWith (+) k 1 m) Map.empty ys : countFrequencies wss
  where
    ys = map head xss
    zss = map tail xss
    wss = if head zss == []
      then []
      else zss

--------------------------------------------------------------------------------

msort :: (Monad m) => (a -> a -> m Bool) -> [a] -> m [a]
msort (<) [] = return []
msort (<) [x] = return [x]
msort (<) xs = do
  ys' <- msort (<) ys
  zs' <- msort (<) zs
  merge (<) ys' zs'
  where
    (ys, zs) = split xs

merge :: (Monad m) => (a -> a -> m Bool) -> [a] -> [a] -> m [a]
merge (<) [] ys = return ys
merge (<) xs [] = return xs
merge (<) (x:xs) (y:ys) = do
  bool <- x < y
  if bool
    then liftM (x:) $ merge (<) xs (y:ys)
        else liftM (y:) $ merge (<) (x:xs) ys

split :: [a] -> ([a], [a])
split [] = ([], [])
split [x] = ([x], [])
split (x:y:zs) = (x:xs, y:ys)
  where
    (xs, ys) = split zs

Haskell或Caml现在流行吗?
Yai0Phah 2012年

我不知道。但是Haskell是我最喜欢的一种语言,因此我在其中进行了编程。模式匹配使此操作更加容易。
Thomas

0

一个非常相关的问题在克里斯蒂安森,达尼连科和迪鲁斯的《各种排列(功能性珍珠)》中得到了回答。他们在列表monad中运行排序算法,该算法本质上模拟了不确定性,并返回给定输入列表的所有排列。有趣的属性是每个排列仅返回一次。

引用摘要:

...

在本文中,我们从不同的角度看待非确定性和排序的组合:给定排序函数,我们将其应用于非确定性谓词,以获得枚举输入列表排列的函数。我们将深入研究排序算法和谓词的必要属性,并讨论建模非确定性的各种变化。

最重要的是,我们制定并证明了一个定理,指出无论我们使用哪种排序函数,相应的置换函数都会枚举输入列表的所有置换。我们使用自由定理来证明该陈述,这些定理仅从函数的类型派生。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.