web-dev-qa-db-ja.com

順序に関係なく文字列のリストのハッシュを取得する

順序に関係なく文字列のリストのハッシュコードを返す関数GetHashCodeOfList()を書きたいのですが。同じ文字列の2つのリストを指定すると、同じハッシュコードが返されます。

_ArrayList list1 = new ArrayList()    
list1.Add("String1");
list1.Add("String2");
list1.Add("String3");    

ArrayList list2 = new ArrayList()    
list2.Add("String3");    
list2.Add("String2"); 
list2.Add("String1");

GetHashCodeOfList(list1) = GetHashCodeOfList(list2) //this should be equal.
_

私はいくつかの考えを持っていました:

  1. 最初にリストを並べ替え、次に並べ替えたリストを1つの長い文字列に結合してから、GetHashCode()を呼び出します。ただし、ソートには時間がかかります。

  2. リスト内の個々の文字列のハッシュを(string.GetHashCode()を呼び出すことによって)取得し、すべてのハッシュを乗算してMod _UInt32.MaxValue_を呼び出すことができます。例:"String1".GetHashCode() * "String2".GetHashCode * … MOD UInt32.MaxValue。しかし、これにより数値がオーバーフローします。

誰もが何か考えを持っていますか?

よろしくお願いします。

62
MaxK

ここでは、2つの主要なカテゴリーの下にさまざまな異なるアプローチがあり、それぞれに有効性とパフォーマンスの点で、それぞれ独自の利点と欠点があります。どんなアプリケーションでも最も単純なアルゴリズムを選択し、どんな状況でも必要に応じてより複雑なバリアントのみを使用するのがおそらく最善です。

これらの例は_EqualityComparer<T>.Default_を使用していることに注意してください。これはnull要素を適切に処理するためです。必要に応じて、nullをゼロよりも高くすることができます。 Tがstructに制約されている場合、それも不要です。必要に応じて、関数から_EqualityComparer<T>.Default_ルックアップを引き上げることができます。

可換運用

commutative である個々のエントリのハッシュコードで演算を使用すると、順序に関係なく同じ最終結果が得られます。

数値にはいくつかの明白なオプションがあります。

XOR

_public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    int hash = 0;
    foreach (T element in source)
    {
        hash = hash ^ EqualityComparer<T>.Default.GetHashCode(element);
    }
    return hash;
}
_

その欠点の1つは、{"x"、 "x"}のハッシュが{"y"、 "y"}のハッシュと同じであることです。それがあなたの状況にとって問題でないなら、それはおそらく最も簡単な解決策です。

添加

_public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    int hash = 0;
    foreach (T element in source)
    {
        hash = unchecked (hash + 
            EqualityComparer<T>.Default.GetHashCode(element));
    }
    return hash;
}
_

ここではオーバーフローは問題ないため、明示的なuncheckedコンテキストです。

いくつかの厄介なケースがあります(例:{1、-1}および{2、-2})。ただし、特に文字列の場合は問題ない可能性が高くなります。このような整数を含む可能性のあるリストの場合、常にカスタムハッシュ関数(おそらく、特定の値の繰り返しのインデックスをパラメーターとして取り、それに応じて一意のハッシュコードを返す関数)。

以下は、前述の問題をかなり効率的に回避するアルゴリズムの例です。また、生成されるハッシュコードの分布を大幅に増やすという利点もあります(説明については、最後にリンクされている記事を参照してください)。このアルゴリズムが「より良い」ハッシュコードを正確に生成する方法の数学的/統計的分析は非常に高度ですが、広範囲の入力値にわたってテストし、結果をプロットすることで十分に検証できます。

_public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    int hash = 0;
    int curHash;
    int bitOffset = 0;
    // Stores number of occurences so far of each value.
    var valueCounts = new Dictionary<T, int>();

    foreach (T element in source)
    {
        curHash = EqualityComparer<T>.Default.GetHashCode(element);
        if (valueCounts.TryGetValue(element, out bitOffset))
            valueCounts[element] = bitOffset + 1;
        else
            valueCounts.Add(element, bitOffset);

        // The current hash code is shifted (with wrapping) one bit
        // further left on each successive recurrence of a certain
        // value to widen the distribution.
        // 37 is an arbitrary low prime number that helps the
        // algorithm to smooth out the distribution.
        hash = unchecked(hash + ((curHash << bitOffset) |
            (curHash >> (32 - bitOffset))) * 37);
    }

    return hash;
}
_

乗算

これは、追加よりもメリットが少ない場合は少なくなります。小さな数と正数と負数の混合により、ハッシュビットの分布が改善される場合があります。この「1」を相殺するための負の値は、何も寄与しない無用のエントリになり、ゼロ要素はゼロになります。この主要な欠陥を引き起こさないように、特別な場合はゼロにすることができます。

_public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    int hash = 17;
    foreach (T element in source)
    {
        int h = EqualityComparer<T>.Default.GetHashCode(element);
        if (h != 0)
            hash = unchecked (hash * h);
    }
    return hash;
}
_

最初に注文する

もう1つのコアアプローチは、最初にいくつかの順序付けを強制し、次に任意のハッシュ結合関数を使用することです。順序が一貫している限り、順序自体は重要ではありません。

_public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    int hash = 0;
    foreach (T element in source.OrderBy(x => x, Comparer<T>.Default))
    {
        // f is any function/code you like returning int
        hash = f(hash, element);
    }
    return hash;
}
_

これには、fで可能な結合操作が非常に優れたハッシュプロパティ(ビットの分散など)を持つことができるという点でいくつかの重要な利点がありますが、これは大幅にコストが高くなります。並べ替えはO(n log n)であり、コレクションに必要なコピーはメモリ割り当てであり、元のファイルの変更を避けたい場合は避けられません。 GetHashCodeの実装では、通常、割り当てを完全に回避する必要があります。 fの可能な実装の1つは、加算セクションの最後の例で示したものと同様です(たとえば、ビットシフトの定数数が残って素数による乗算が続く-各反復で連続する素数を使用することもできます)追加の費用は不要です。生成する必要があるのは1回だけだからです)。

とは言え、ハッシュを計算してキャッシュし、GetHashCodeへの多くの呼び出しでコストを償却できるケースを扱っている場合、このアプローチは優れた動作をもたらす可能性があります。また、後者の方法は、要素の種類がわかっている場合は要素でGetHashCodeを使用する必要がなく、代わりにバイト単位の操作を使用してさらに優れたハッシュ分布を生成できるため、より柔軟です。このようなアプローチは、パフォーマンスが重大なボトルネックであると識別された場合にのみ役立つ可能性があります。

最後に、ハッシュコードの主題とその一般的な有効性について、かなり包括的でかなり非数学的な概要が必要な場合は、 これらのブログ投稿 、特にシンプルなハッシュアルゴリズムの実装(pt II)ポスト。

73
Jon Skeet

文字列リストをソートする代わりに、文字列のハッシュコードを取得してから、ハッシュコードをソートすることもできます。 (intの比較は、文字列の比較よりも安価です。)次に、アルゴリズムを使用して、ハッシュコードをマージして(うまくいけば)より良い分布を得ることができます。

例:

GetHashCodeOfList<T>(IEnumerable<T> list) {
   List<int> codes = new List<int>();
   foreach (T item in list) {
      codes.Add(item.GetHashCode());
   }
   codes.Sort();
   int hash = 0;
   foreach (int code in codes) {
      unchecked {
         hash *= 251; // multiply by a prime number
         hash += code; // add next hash code
      }
   }
   return hash;
}
21
Guffa

はるかに少ないコードですが、おそらくパフォーマンスは他の答えほど良くありません:

public static int GetOrderIndependentHashCode<T>(this IEnumerable<T> source)    
    => source == null ? 0 : HashSet<T>.CreateSetComparer().GetHashCode(new HashSet<T>(source));
0
Matthew Kane
    Dim list1 As ArrayList = New ArrayList()
    list1.Add("0")
    list1.Add("String1")
    list1.Add("String2")
    list1.Add("String3")
    list1.Add("abcdefghijklmnopqrstuvwxyz")

    Dim list2 As ArrayList = New ArrayList()
    list2.Add("0")
    list2.Add("String3")
    list2.Add("abcdefghijklmnopqrstuvwxyz")
    list2.Add("String2")
    list2.Add("String1")
    If GetHashCodeOfList(list1) = GetHashCodeOfList(list2) Then
        Stop
    Else
        Stop
    End If
    For x As Integer = list1.Count - 1 To 0 Step -1
        list1.RemoveAt(list1.Count - 1)
        list2.RemoveAt(list2.Count - 1)
        Debug.WriteLine(GetHashCodeOfList(list1).ToString)
        Debug.WriteLine(GetHashCodeOfList(list2).ToString)
        If list1.Count = 2 Then Stop
    Next


Private Function GetHashCodeOfList(ByVal aList As ArrayList) As UInt32
    Const mask As UInt16 = 32767, hashPrime As Integer = Integer.MaxValue
    Dim retval As UInt32
    Dim ch() As Char = New Char() {}
    For idx As Integer = 0 To aList.Count - 1
        ch = DirectCast(aList(idx), String).ToCharArray
        For idCH As Integer = 0 To ch.Length - 1
            retval = (retval And mask) + (Convert.ToUInt16(ch(idCH)) And mask)
        Next
    Next
    If retval > 0 Then retval = Convert.ToUInt32(hashPrime \ retval) 'Else ????
    Return retval
End Function
0
dbasnett

これがハイブリッドアプローチです。 3つの可換演算(XOR、加算、乗算)を組み合わせ、それぞれを32ビット数の異なる範囲に適用します。各操作のビット範囲は調整可能です。

public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
    var comparer = EqualityComparer<T>.Default;
    const int XOR_BITS = 10;
    const int ADD_BITS = 11;
    const int MUL_BITS = 11;
    Debug.Assert(XOR_BITS + ADD_BITS + MUL_BITS == 32);
    int xor_total = 0;
    int add_total = 0;
    int mul_total = 17;
    unchecked
    {
        foreach (T element in source)
        {
            var hashcode = comparer.GetHashCode(element);
            int xor_part = hashcode >> (32 - XOR_BITS);
            int add_part = hashcode << XOR_BITS >> (32 - ADD_BITS);
            int mul_part = hashcode << (32 - MUL_BITS) >> (32 - MUL_BITS);
            xor_total = xor_total ^ xor_part;
            add_total = add_total + add_part;
            if (mul_part != 0) mul_total = mul_total * mul_part;
        }
        xor_total = xor_total % (1 << XOR_BITS); // Compact
        add_total = add_total % (1 << ADD_BITS); // Compact
        mul_total = mul_total - 17; // Subtract initial value
        mul_total = mul_total % (1 << MUL_BITS); // Compact
        int result = (xor_total << (32 - XOR_BITS)) + (add_total << XOR_BITS) + mul_total;
        return result;
    }
}

各要素のGetHashCodeへの呼び出しがCPU需要を支配するため、パフォーマンスは単純なXORメソッドとほぼ同じです。

0
Theodor Zoulias