从更大的列表中筛选出无序int列表

时间:2018-07-11 14:35:25

标签: c# list hash

我得到了一个无序列表。在80到140个项目之间,每个项目的值在0到175之间。

我正在生成该列表的列表,其中大约有5到1000万。

我需要尽快处理所有唯一有序的序列(不包括重复项)。

我现在的操作方式是创建列表所有值的哈希,然后将其插入到哈希集中。

分析时的两个热点是ToArray() HOTSPOT1 和Array.Sort() HOTSPOT2

是否有更好的方法来完成该任务,或者有更好的替代方法来修复这两个热点?速度很重要。

小型演示,我尝试尽可能多地复制

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{

    class Example
    {
        //some other properties

        public int Id { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var checkedUnlock = new HashSet<int>();
            var data = FakeData();

            foreach (List<Example> subList in data)
            {
                var hash = CalcHash(subList.Select(x => x.Id).ToArray());  // HOTPSOT1

                var newHash = checkedUnlock.Add(hash);

                if (newHash)
                {
                    //do something
                }
            }
        }

        static int CalcHash(int[] value)
        {
            Array.Sort(value); // HOTPSOT2

            int hash;
            unchecked // https://stackoverflow.com/a/263416/40868
            {
                hash = (int)2166136261;
                var i = value.Length;
                while (i-- > 0)
                    hash = (hash * 16777619) ^ value[i];
            }

            return hash;
        }

        //don't look at this, this is just to fake data
        static List<List<Example>> FakeData()
        {
            var data = new List<List<Example>>();

            var jMax = 10; //normally between 80 and 140
            var idMax = 25; //normally between 0 and 175
            var rnd = new Random(42);
            var ids = Enumerable.Range(0, idMax).ToArray();

            for (int i = 0; i < 500000; ++i)
            {
                //force duplicate
                if(i % 50000 == 0)
                {
                    ids = Enumerable.Range(0, idMax).ToArray();
                    rnd = new Random(42);
                }

                for (int r = 0; r < idMax; ++r)
                {
                    int randomIndex = rnd.Next(idMax);
                    int temp = ids[randomIndex];
                    ids[randomIndex] = ids[r];
                    ids[r] = temp;
                }

                var subList = new List<Example>();
                data.Add(subList);

                for (int j = 0; j < jMax; ++j)
                {
                    subList.Add(new Example() { Id = ids[j] });                    
                }
            }

            return data;
        }
    }
}

4 个答案:

答案 0 :(得分:3)

因此,您有一个最多可以包含140个项目的数组,并且所有值都在0到175之间。数组中的所有值都是唯一的,顺序无关紧要。也就是说,数组[20, 90, 16]将被视为与[16, 20, 90]相同。

鉴于此,您可以将一个数组表示为一组175位。更好的是,您可以创建集合而不必对输入数组进行排序。

您将C#中的集合表示为BitArray。要计算数组的哈希码,请创建集合,然后遍历集合以获取哈希码。看起来像这样:

private BitArray HashCalcSet = new BitArray(175);
int CalcHash(int[] a, int startIndex)
{
    // construct the set
    HashCalcSet.SetAll(false);

    for (var i = startIndex; i < a.Length; ++i)
    {
        HashCalcSet[a[i]] = true;
    }

    // compute the hash
    hash = (int)2166136261;
    for (var i = 174; i >= 0; --i)
    {
        if (HashCalcSet[i])
        {
            hash = (hash * 16777619) ^ value[i];
        }
    }
    return hash;
}

这消除了排序和ToArray。您必须在BitArray上循环几次,但是在BitArray上进行三遍传递可能比排序更快。

我对您的解决方案看到的一个问题是您如何使用HashSet。您有以下代码:

var hash = CalcHash(subList.Select(x => x.Id).ToArray());  // HOTPSOT1

var newHash = checkedUnlock.Add(hash);

if (newHash)
{
    //do something
}

该代码错误地假设,如果两个数组的哈希码相等,则数组相等。您正在生成一个175位数量的32位哈希码。肯定会有哈希冲突。您最终会说,两个数组不同时,它们是相同的。

如果您对此感到担心,请告诉我,我可以编辑答案以提供解决方案。

允许比较

如果您希望能够比较项目是否相等,而不仅仅是检查其哈希码是否相同,则需要创建一个具有EqualsGetHashCode方法的对象。您将把该对象插入HashSet中。这些对象中最简单的对象将包含上文所述的BitArray以及对其进行操作的方法。像这样:

class ArrayObject
{
    private BitArray theBits;
    private int hashCode;
    public override bool Equals(object obj)
    {
        if (object == null || GetType() != obj.GetType())
        {
            return false;
        }
        ArrayObject other = (ArrayObject)obj;
        // compare two BitArray objects
        for (var i = 0; i < theBits.Length; ++i)
        {
            if (theBits[i] != other.theBits[i])
                return false;
        }
        return true;
    }

    public override int GetHashCode()
    {
        return hashCode;
    }

    public ArrayObject(int hash, BitArray bits)
    {
        theBits = bits;
        hashCode = hash;
    }
}

这样的想法是,您按照上述方法构造BitArray和哈希码(尽管您必须为每个调用分配一个新的BitArray),然后创建并返回这些ArrayObject实例之一。

您的HashSet成为HashSet<ArrayObject>

以上方法有效,但它占用了大量内存。您可以通过创建仅包含三个long整数的类来减少内存需求。您无需直接使用BitArray,而直接操作这些位。您映射这些位,以便数字0到63修改第一个数字中的位0到63。数字64到127对应于第二个数字的位0到63,以此类推。因此,您不必保存单独的哈希码,因为从三个long中进行计算很容易,并且相等比较变得容易得多也是如此。

该类看起来像这样。理解,我还没有测试代码,但是这个想法应该是正确的。

class ArrayObject2
{
    private long l1;
    private long l2;
    private long l3;

    public ArrayObject2(int[] theArray)
    {
        for (int i = 0; i < theArray.Length; ++i)
        {
            var rem = theArray[i] % 63;
            int bitVal = 1 << rem;
            if (rem < 64) l1 |= bitVal;
            else if (rem < 128) l2 |= bitVal;
            else l3 |= bitVal;
        }
    }

    public override bool Equals(object obj)
    {
        var other = obj as ArrayObject2;
        if (other == null) return false;
        return l1 == other.l1 && l2 == other.l2 && l3 == other.l3;
    }

    public override int GetHashCode()
    {
        // very simple, and not very good hash function.
        return (int)l1;
    }
}

正如我在代码中评论的那样,哈希函数并不是很好。它将起作用,但是通过一些研究您可以做得更好。

此方法的优点是使用的内存少于BitArrayBoolean数组。它可能会比bool的数组慢。它可能BitArray代码快。但是无论如何,它都可以使您避免错误地假设相同的哈希码等于相同的数组。

答案 1 :(得分:1)

我认为您可以通过重用一个更大的数组来节省一些时间,而不必每次都分配新的数组而导致额外的内存通信和垃圾回收。

这将需要自定义排序实现,该实现知道即使数组可以有1000个项目,但对于当前运行,仅需要对前80个项目进行排序(散列也是如此)。在id的子范围上运行的quicksort看起来应该可以正常工作。快速的想法样本(尚未经过详细测试)

int[] buffer = new int[1000];
foreach (List<Example> subList in data)
{
    for (int i = 0; i < subList.Count; i++)
    {
        buffer[i] = subList[i].Id;
    }
    var hash = CalcHashEx(buffer, 0, subList.Count - 1);

    var newHash = checkedUnlock.Add(hash);

    if (newHash)
    {
        //do something
    }
}


public static void QuickSort(int[] elements, int left, int right)
{
    int i = left, j = right;
    int pivot = elements[(left + right) / 2];
    while (i <= j)
    {
        while (elements[i] < pivot)
        {
            i++;
        }
        while (elements[j] > pivot)
        {
            j--;
        }
        if (i <= j)
        {
            // Swap
            int tmp = elements[i];
            elements[i] = elements[j];
            elements[j] = tmp;
            i++;
            j--;
        }
    }

    if (left < j)
    {
        QuickSort(elements, left, j);
    }
    if (i < right)
    {
        QuickSort(elements, i, right);
    }
}

static int CalcHashEx(int[] value, int startIndex, int endIndex)
{
    QuickSort(value, startIndex, endIndex);

    int hash;
    unchecked // https://stackoverflow.com/a/263416/40868
    {
        hash = (int)2166136261;
        var i = endIndex + 1;
        while (i-- > 0)
            hash = (hash * 16777619) ^ value[i];
    }

    return hash;
}

答案 2 :(得分:1)

此版本的CalcHash()可让您删除.ToArray()并用可以对序列起作用的不同东西替换Array.Sort(),而不需要整个集合...所以两个热点。

static int CalcHash(IEnumerable<int> value)
{
    value = value.OrderByDescending(i => i);

    int hash;
    unchecked // https://stackoverflow.com/a/263416/40868
    {
        hash = (int)2166136261;
        foreach(var item in value)
        {
            hash = (hash * 16777619) ^ item;
        }
    }

    return hash;
}

我不确定OrderByDescending()的表现如何。我怀疑它会比Array.Sort()慢一些,但由于消除了ToArray()而仍然是一个全面的胜利...但是您需要再次运行探查器才能确定。

通过.GroupBy()消除或减少分支,并在每个组的.First()项目上运行代码,您可能还会得到一些改进:

var groups = data.GroupBy(sub => CalcHash(sub.Select(x => x.Id)));
foreach(List<Example> subList in groups.Select(g => g.First()))
{
    //do something
}

答案 3 :(得分:0)

将其放在此处,因为将其放在注释中没有意义

到目前为止,我所做的是创建一个布尔数组并将存在时将该项目的索引设置为true,然后我将CalcHash替换为;

        unchecked
        {
            hash = (int)2166136261;
            var i = theMaxLength;
            while (i-- > 0)
                if(testing[i]) //the array of boolean
                {
                    hash = (hash  * 16777619) ^ i;
                    testing[i] = false;
                }
        }

这样做是为了彻底删除ToArray()和Array.Sort(),此解决方案是根据dlxeon / jim / joel答案构建的

我将运行时减少了大约20-25%,这很好