快速排序正常的分布式双打

时间:2012-02-06 05:39:27

标签: c# algorithm sorting

我们有一个对象数组。每个对象都有双倍的值。该数组应按此值排序。数组参数是:

  1. 范围是1 - 10 000个元素。 100 - 5000大部分时间。 > 10 000真的不太可能
  2. 值的分布接近正常
  3. 值只插入一次,之后不再更改(不重新排序几乎排序的数组)
  4. 有很多这样的数据样本
  5. 现在我们使用OrderBy并执行以下操作:

    public class Item
    {
        double Value;
        //... some else data fields
    }
    
    List<Item> items;           // fill items
    items.Sort(p=>p.Value);  // sort
    

    众所周知:

    1. List.Sort (与 Array.Sort 相同)是快速排序算法的implementation
    2. Quick sort is最适合均匀分配双打
    3. OrderBy为我们的案例实现排序looks worse而不是List.Sort。
    4. 但基准测试表明,这种排序占据了我们软件处理时间的95%。

      对于这种特殊情况,是否有更快的排序实施?

3 个答案:

答案 0 :(得分:3)

评估排序算法的问题在于有许多因素会对结果产生影响。我不相信你给了很多的网站,因为它可能比javascript引擎,可视化和javascript实现更基准,而不是实际的排序算法。

Heapsort在理论上具有很好的属性,但是无法充分利用现代CPU优化。

QuickSort在理论上更糟糕,但常见的技巧,如3的中位数和9个中位数的枢轴元素使得坏情况真的不太可能,并且通过线性处理数组,它可以被CPU很好地优化。 p>

当您不需要就地排序时,MergeSort很好。要对它进行原位使用并对预分类和几乎预分类的数据进行优化并不是一件容易的事,但是你可能会看一下Python和Java7使用的Tim sort。

没有一般的答案,例如“QuickSort对高斯分布式数据不利”。理论与实践之间存在着巨大的差距。理论上,Insertion Sort和QuickSort比HeapSort更糟糕。实际上,在大多数情况下,经过优化的QuickSort很难被击败,特别是因为它优化并从CPU缓存中受益。 Tim Sort不是简单的合并。它实际上是与InsertionSort混合使用来优化已经排序的对象运行的常见情况。

其次,这应该是相当明显的,没有提到的排序算法实际上计算两个对象的差异。因此,任何不会产生大量重复项的分发对它们看起来都是一样的。他们所看到的只是“小于,等于,大于”。因此,分布之间的唯一区别是有多少对象是相等的!事实上,只有桶排序算法(例如基数排序)应该关心对象分布,因为它们使用超出<=>比较的实际值。

对于您的特定情况,您需要检查列表的组织方式。链表非常不适合排序。事实上,如果我没记错,Java会将任何集合转换为本机数组进行排序,然后重建集合。 其次,p=>p.Value的概念很漂亮,但也可能付出相当大的代价。这可能会导致创建,管理和垃圾收集等各种其他对象。

你应该尝试的第一件事是检查是否完整的比较器比lambda语法概念更快。 查看内存管理。最有可能的是,这是实际工作发生的地方,不必要地复制和转换双打。

例如,C#可能会为您的数据集“double - &gt; index”构建逆映射,然后对此数组进行排序,然后使用映射对数据进行排序。这很好,如果你的lambda函数非常昂贵,并且只能计算一次。

答案 1 :(得分:3)

由于O(n ^ 2)努力和5000元素向量,值得尝试

  1. 将您的列表转换为普通双数组,
  2. 使用一些专门的排序库对数组进行排序
  3. 从排序和
  4. 中获取排序顺序索引
  5. 根据索引
  6. 重新排列列表中的项目

    这是可行的,因为它只增加了O(2n),因此如果由于比较快得多,开销小于增益,它可能是可忽略的并且有回报。

    如果我找到时间,我会在这里发布一个例子。

    @Edit:我已经测试了这个用于演示(以及我自己的兴趣)。以下测试将List.Sort()与复制 - 排序 - 复制返回方法进行比较。后期完成ILNumerics快速排序。两个版本都以相同的长度运行(意味着:交错)。

    免责声明:这只是为了得到一个粗略的概述,如果该方法将支付。在我的计算机(Core i5,2.4 GHz,3MB L3数据缓存)上。但收支平衡点很难确定。此外,正如一如既往的快速和肮脏的性能指标 - 一大堆影响被遗漏了。可能是最重要的:由于在实际生产环境中可能不需要多个副本而导致的缓存问题。

    代码:

    namespace ConsoleApplication1 {
    unsafe class Program : ILNumerics.ILMath {
    
    static void Main(string[] args) {
        int numItems = 0, repet = 20000; 
    
        Stopwatch sw01 = new Stopwatch();
        // results are collected in a dictionary: 
        // key: list length
        // value: tuple with times taken by ILNumerics and List.Sort()
        var results = new Dictionary<int, Tuple<float,float>>();
        // the comparer used for List.Sort() see below 
        ItemComparer comparer = new ItemComparer();
    
        // run test for copy-sort-copy back via ILNumerics
        for (numItems = 500; numItems < 50000; numItems = (int)(numItems * 1.3)) {
    
            Console.Write("\r measuring: {0}", numItems);  
            long ms = 0;
            List<Item> a = makeData(numItems);
            for (int i = 0; i < repet; i++) {
                sw01.Restart();
                List<Item> b1 = fastSort(a);
                sw01.Stop();
                ms += sw01.ElapsedMilliseconds;
            }
            results.Add(numItems,new Tuple<float,float>((float)ms / repet, 0f)); 
        }
    
        // run test using the straightforward approach, List.Sort(IComparer)
        for (numItems = 500; numItems < 50000; numItems = (int)(numItems * 1.3)) {
    
            Console.Write("\r measuring: {0}", numItems);  
            List<Item> a = makeData(numItems);
            long ms = 0;
            for (int i = 0; i < repet; i++) {
                List<Item> copyList = new List<Item>(a);
                sw01.Restart();
                copyList.Sort(comparer);
                sw01.Stop();
                ms += sw01.ElapsedMilliseconds;
            }
            results[numItems] = new Tuple<float, float>(results[numItems].Item1, (float)ms / repet); 
        }
    
        // Print results
        Console.Clear(); 
        foreach (var v in results) 
            Console.WriteLine("Length: {0} | ILNumerics/CLR: {1} / {2} ms", v.Key, v.Value.Item1, v.Value.Item2);
        Console.ReadKey(); 
    }
    public class Item {
        public double Value;
        //... some else data fields
    }
    
    public class ItemComparer : Comparer<Item> {
        public override int Compare(Item x, Item y) {
            return (x.Value > y.Value)  ? 1 
                 : (x.Value == y.Value) ? 0 : -1;
        }
    }
    
    
    public static List<Item> makeData(int n) {
        List<Item> ret = new List<Item>(n); 
        using (ILScope.Enter()) {
            ILArray<double> A = rand(1,n);
            double[] values = A.GetArrayForRead(); 
            for (int i = 0; i < n; i++) {
                ret.Add(new Item() { Value = values[i] }); 
            }
        }
        return ret; 
    }
    
    public static List<Item> fastSort(List<Item> unsorted) {
        //double [] values = unsorted.ConvertAll<double>(item => item.Value).ToArray(); 
    
        //// maybe more efficient? safes O(n) run 
        //double[] values = new double[unsorted.Count];
        //for (int i = 0; i < values.Length; i++) {
        //    values[i] = unsorted[i].Value;
        //}
        using (ILScope.Enter()) {
            // convert incoming
            ILArray<double> doubles = zeros(unsorted.Count);
            double[] doublesArr = doubles.GetArrayForWrite();
            for (int i = 0; i < doubles.Length; i++) {
                doublesArr[i] = unsorted[i].Value;
            }
    
            // do fast sort 
            ILArray<double> indices = empty();
            doubles = sort(doubles, Indices: indices);
    
            // convert outgoing
            List<Item> ret = new List<Item>(unsorted.Count); 
            foreach (double i in indices) ret.Add(unsorted[(int)i]); 
            return ret; 
        }
    }
    }
    }
    

    这给出了以下输出:

    Length: 500 | ILNumerics / List.Sort: 0,00395 / 0,0001 ms
    Length: 650 | ILNumerics / List.Sort: 0,0003 / 0,0001 ms
    Length: 845 | ILNumerics / List.Sort: 0,00035 / 0,0003 ms
    Length: 1098 | ILNumerics / List.Sort: 0,0003 / 0,00015 ms
    Length: 1427 | ILNumerics / List.Sort: 0,0005 / 0,00055 ms
    Length: 1855 | ILNumerics / List.Sort: 0,00195 / 0,00055 ms
    Length: 2000 | ILNumerics / List.Sort: 0,00535 / 0,0006 ms
    Length: 2600 | ILNumerics / List.Sort: 0,0037 / 0,00295 ms
    Length: 3380 | ILNumerics / List.Sort: 0,00515 / 0,0364 ms
    Length: 4394 | ILNumerics / List.Sort: 0,0051 / 1,0015 ms
    Length: 4500 | ILNumerics / List.Sort: 0,1136 / 1,0057 ms
    Length: 5850 | ILNumerics / List.Sort: 0,2871 / 1,0047 ms
    Length: 7605 | ILNumerics / List.Sort: 0,5015 / 2,0049 ms
    Length: 9886 | ILNumerics / List.Sort: 1,1164 / 2,0793 ms
    Length: 12851 | ILNumerics / List.Sort: 1,4236 / 3,6335 ms
    Length: 16706 | ILNumerics / List.Sort: 1,6202 / 4,9506 ms
    Length: 21717 | ILNumerics / List.Sort: 2,3417 / 6,1871 ms
    Length: 28232 | ILNumerics / List.Sort: 3,4038 / 8,7888 ms
    Length: 36701 | ILNumerics / List.Sort: 4,4406 / 12,1311 ms
    Length: 47711 | ILNumerics / List.Sort: 5,7884 / 16,1002 ms
    

    在这里,收支平衡看起来大约有4000个元素。较大的数组总是通过复制 - 排序 - 复制方法更快地排序大约3倍。我认为对于较小的数组,它可以支付 - 或者可能不支付。这里收集的数字是不可靠的。我假设,对于小型列表,排序时间会受到内存管理(GC)等其他一些问题的影响。也许有人在这里有更多想法如何解释这一点。

    同样奇怪的是List.Sort的执行时间超过了4000项。不知道List.Sort在这里切换到另一个(更糟糕的)实现吗?

    关于这个问题,似乎需要付费以复制项目,按明确数组排序并在需要时将其复制回来。根据您的硬件,收支平衡可能会上下移动。一如既往:描述您的实施!

答案 2 :(得分:2)

一种解决方案可能是以0.001(或任意值p)的步长创建从0到1的区间。注意每个区间中的预期数字是p * N)。现在,对于数组中的每个数字,计算累积概率(-infinity的累积概率为0,0为0.5,无穷大为1.0)并将该数字放入相应的bin中。使用您喜欢的排序算法分别对每个bin进行排序并合并结果。如果选择p使得p * n = k(k是常数),则该算法在最佳和平均情况下为O(Nlogk)。