分组和选择整数列表的最快方法(最高出现次数)

时间:2014-05-01 06:11:58

标签: c# performance list

我有一个包含100万到1亿整数的整数列表。

我想根据其出现次数对其进行排名,然后选择热门K(此处K=10)结果。

我已经尝试了4种不同的方式来使Method1最快。并行化并没有在Method1中击败我自己的分组代码,并且由于线程竞争条件导致排名不准确。

Method1Method4结果准确无误,Method2Method3因种族条件而排名可能不准确。

现在,我正在寻找比Method1更快的代码,或修复并行化方法,使其准确然后比Method1更快。

class Benchmark
{
    static List<int> input = new List<int>();
    static void Main(string[] args)
    {
        int count = int.Parse(args[0]);

        Random rnd = new Random();

        for (int i = 0; i < count; i++)
            input.Add(rnd.Next(1, count));

        DoBench();

        Console.ReadKey();
    }

    private static void DoBench()
    {
        for (int i = 1; i <= 4; i++)
        {
            DateTime start = DateTime.Now;

            List<KeyValuePair<int, int>> results = null;

            switch (i)
            {
                case 1:
                    results = Method1();
                    break;
                case 2:
                    results = Method2();
                    break;
                case 3:
                    results = Method3();
                    break;
                case 4:
                    results = Method4();
                    break;
            }

            int resultsCount = 10;

            var topResults = results.Take(resultsCount).OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToArray();

            for (int j = 0; j < resultsCount; j++)
                Console.WriteLine("No {0,2}: {1,8}, Score {2,4}", j + 1, topResults[j].Key, topResults[j].Value);

            Console.WriteLine("Time of Method{0}: {1} ms", i, (long)DateTime.Now.Subtract(start).TotalMilliseconds);
            Console.WriteLine();
        }
    }

    private static List<KeyValuePair<int, int>> Method1()
    {
        Dictionary<int, int> dic = new Dictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            if (dic.ContainsKey(number))
                dic[number]++;
            else
                dic.Add(number, 1);
        }

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method2()
    {
        var sorted_results = input.AsParallel().GroupBy(x => x)
               .Select(g => new KeyValuePair<int, int>(g.Key, g.Count()))
               .OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method3()
    {
        ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>();

        input.AsParallel<int>().ForAll((number) =>
        {
            dic.AddOrUpdate(number, 1, new Func<int, int, int>((key, oldValue) => oldValue + 1));
        });

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method4()
    {
        var sorted_results = input.GroupBy(x => x)
               .Select(g => new KeyValuePair<int, int>(g.Key, g.Count()))
               .OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }
}

2 个答案:

答案 0 :(得分:1)

我认为你可以大大加快算法中使用选择排序找到10个最高计数的部分。

最快的方法是使用QuickSelect算法,但这对我来说有点太复杂了。

相反,我将演示使用Partial Selection Sort

这是一个使用它的Method5()。它与Method1()基本相同,只是它在最后阶段使用部分排序而不是完整排序:

private static List<KeyValuePair<int, int>> Method5()
{
    Dictionary<int, int> dic = new Dictionary<int, int>();

    for (int i = 0; i < input.Count; i++)
    {
        int number = input[i];

        if (dic.ContainsKey(number))
            dic[number]++;
        else
            dic.Add(number, 1);
    }

    var result = dic.ToList();
    partialSort(result, 10);

    return result;
}

private static void partialSort(List<KeyValuePair<int, int>> list, int k)
{
    for (int i = 0; i < k; ++i)
    {
        int maxIndex = i;
        int maxValue = list[i].Value;

        for (int j = i+1; j < list.Count; ++j)
        {
            if (list[j].Value > maxValue)
            {
                maxIndex = j;
                maxValue = list[j].Value;
            }
        }

        var temp = list[i];
        list[i] = list[maxIndex];
        list[maxIndex] = temp;
    }
}

我定时了一个RELEASE(而非调试)版本,K == 10时我的电脑上的速度几乎要快三倍。

较小的K相对于集合的大小将按比例更快。对于K的较大值(特别是K接近集合大小的地方),此算法可能比普通排序慢。

如果K预计很大,

请勿使用此算法。它的效率很大程度上取决于K相对于集合大小的小值。

答案 1 :(得分:1)

我找到了一个更快的字典实现,用于分组部分。我使用了Adam Horvath(FastDictionary)编写的MapReduce.NET项目中的link类。

使用FastDictionary我更新了我的Method1并写了一个更快的新Method6。然后我从@Mattew Watson添加快速排序并编写Method7。最后,我把它们完全填满了。

修改

我想出了一个很棒的主意,可以在Method8中更快地加速分组。我的想法是使用一个预定义的数组,其大小最大为输入数组,因此我可以用数组替换字典。当Method8接近Maximum-Element-of-Input/Size-Of-Input1整数的比例适合内存时,Maximum-Element-of-Input是有效的。

修改2

我写了另外3个名为Method9Method10Method11的方法。

我注意到收集分组结果并将它们添加到Method8中的列表需要大量不必要的内存,所以我稍微修改了partialSort并删除了内存开销。

同样,我注意到我在Int32的4个字节中持有相对较少的金额。为什么?!因此,假设输入列表中的元素不超过65535short.MaxValue),我只需将Int32数组替换为占用内存一半的short数组。

再一次,我注意到如果我们有一个内存关键场景并再次假设输入数组中的大多数元素都小于255byte.MaxValue),我们可以同时使用byte用于具有高发生率的元素的普通字典。由于在分组操作期间进行了多次检查,这种技术有点慢,并且还需要在结束时合并结果,但正如您在图表中所看到的,其内存使用量远远小于其他基于字典的初始方法。

<强>时序:

Timing of different methods

内存使用:

Memory usage of different methods

时间比较 Timing

内存比较 enter image description here

方法6:

    private static List<KeyValuePair<int, int>> Method6()
    {            
        FastDictionary<int, int> dic = new FastDictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            int pos = dic.InitOrGetPosition(number);
            int curr = dic.GetAtPosition(pos);
            dic.StoreAtPosition(pos, ++curr);
        }

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

方法7:

    private static List<KeyValuePair<int, int>> Method7()
    {            
        FastDictionary<int, int> dic = new FastDictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            int pos = dic.InitOrGetPosition(number);
            int curr = dic.GetAtPosition(pos);
            dic.StoreAtPosition(pos, ++curr);
        }

        var result = dic.ToList();
        partialSort(result, 10);

        return result;
    }

方法8

    private static List<KeyValuePair<int, int>> Method8()
    {
        int[] dic = new int[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>();
        for (int i = 0; i < dic.Length; i++)
        {
            if (dic[i] > 0)
                list.Add(new KeyValuePair<int, int>(i, dic[i]));
        }            

        partialSort(list, 10);

        return list;
    }

方法9

    private static List<KeyValuePair<int, int>> Method9()
    {
        int[] dic = new int[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = partialSort(dic, 10);

        return list;
    }

方法10

    private static List<KeyValuePair<int, int>> Method10()
    {
        short[] dic = new short[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = partialSort(dic, 10);

        return list;
    }

方法11

    private static List<KeyValuePair<int, int>> Method11()
    {
        byte[] dic = new byte[input.Max() + 1];

        Dictionary<int, int> largeDic = new Dictionary<int, int>();

        int index = 0;
        int val = 0;

        for (int i = 0; i < input.Count; i++)
        {
            index = input[i];
            val = dic[index];

            if (val < 255)
                dic[index] = (byte)(val + 1); // casting to byte
            else
            {
                if (largeDic.ContainsKey(index))
                {
                    largeDic[index]++;
                }
                else
                {
                    largeDic.Add(index, 255 + 1);
                }
            }
        }

        List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>();

        if (largeDic.Count == 0)
        {
            list = partialSort(dic, 10);
        }
        else
        {
            if (largeDic.Count < 10)
            {
                list.AddRange(largeDic.OrderByDescending(x => x.Value));

                var tempList = partialSort(dic, 50);

                list.AddRange(tempList.Except(list, new KeyValueComparer()).Take(10 - list.Count));
            }
            else
            {
                list = largeDic.OrderByDescending(x => x.Value).Take(10).ToList();
            }
        }

        return list;
    }

部分排序

不同的重载只有不同的参数类型(int[]short[]byte[]

    private static List<KeyValuePair<int, int>> partialSort(int[] list, int k)
    {
        int[] topIndexes = new int[k];

        for (int i = 0; i < k; ++i)
        {
            int maxIndex = i;
            int maxValue = list[i];

            for (int j = i + 1; j < list.Length; ++j)
            {
                if (list[j] > maxValue)
                {
                    maxIndex = j;
                    maxValue = list[j];
                }
            }

            var temp = list[i];
            list[i] = list[maxIndex];
            list[maxIndex] = temp;

            topIndexes[i] = maxIndex;
        }

        List<KeyValuePair<int, int>> top = new List<KeyValuePair<int, int>>();

        for (int i = 0; i < k; i++)
            top.Add(new KeyValuePair<int, int>(topIndexes[i], list[i]));

        return top;
    }