Получение хэш-списка строк, независимо от того,


Я хотел бы написать функцию 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. Но это приводит к переполнению числа.

у кого какие мысли?

заранее спасибо за вашу помощь.

3 60

3 ответа:

здесь существуют различные подходы к двум основным категориям, каждая из которых обычно имеет свои преимущества и недостатки с точки зрения эффективности и производительности. Вероятно, лучше всего выбрать самый простой алгоритм для любого приложения и использовать только более сложные варианты, если это необходимо для любой ситуации.

обратите внимание, что в этих примерах используется EqualityComparer<T>.Default так как это будет иметь дело с нулевыми элементами чисто. При желании вы можете сделать лучше, чем ноль для null. Если T ограничено структурой, это также не нужно. Вы можете поднять EqualityComparer<T>.Default подстановки из функции, если это необходимо.

Операции Коммутативные

если вы используете операции над хэш-кодами отдельных записей, которые коммутативной то это приведет к тому же конечному результату независимо от того.

есть несколько очевидных вариантов по номерам:

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;
}

одним из недостатков этого является то, что хэш для {"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" становится бесполезной записью, ничего не вносящей, и любой нулевой элемент приводит к a нуль. Вы можете специальный случай ноль, чтобы не вызвать этот главный недостаток.

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;
}

порядок

другой основной подход заключается в том, чтобы сначала выполнить некоторый порядок, а затем использовать любую функцию хэш-комбинации, которая вам нравится. Сам порядок нематериален до тех пор, пока он последователен.

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 было бы похоже на то, что приведено в последнем примере в разделе сложения (например, любое постоянное число битовых сдвигов слева с последующим умножением на простое число - вы даже можете использовать последовательные простые числа на каждой итерации без дополнительных затрат, так как они должны быть созданы только один раз).

тем не менее, если вы имели дело со случаями, когда вы могли рассчитать и кэшировать хэш и амортизировать стоимость за многие вызовы GetHashCode этот подход может привести к превосходному поведению. Также последний подход является еще более гибким, так как он может избежать необходимости использовать GetHashCode на элементах, если он знает их тип и вместо этого использует операции на байт на них, чтобы получить еще лучший хэш распределение. Такой подход, вероятно, будет полезен только в тех случаях, когда производительность была определена как значительное узкое место.

наконец, если вы хотите достаточно полный и довольно нематематический обзор предмета хэш-кодов и их эффективности в целом,эти сообщения в блоге стоило бы читать, в частности реализация простого алгоритма хэширования (pt II) пост.

альтернативой сортировке списков строк было бы получить хэш-коды строк, а затем отсортировать хэш-коды. (Сравнение ints дешевле, чем сравнение строк.) Затем вы можете использовать алгоритм для объединения хэш-кодов, который (надеюсь) дает лучшее распределение.

пример:

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;
}
    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