Как рассчитать хороший хеш-код для списка строк?

Фон:

  • У меня есть короткий список строк.
  • Количество строк не всегда одно и то же, но почти всегда порядка "горстки"
  • В нашей базе данных будут храниться эти строки во 2-й нормализованной таблице
  • Эти строки заменяются никогда после их записи в базу данных.

Мы хотим иметь возможность быстро сопоставлять эти строки в запросе без удара производительности при выполнении большого количества объединений.

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

Итак, как мне получить хороший хэш-код? Я мог:

  • Xor хэш-коды всей строки вместе
  • Xor с умножением результата после каждой строки (скажем, на 31)
  • Соедините всю строку вместе, затем получите hashcode
  • Другой способ

Так что думают люди?


В конце концов я просто конкатенирую строки и вычисляю хэш-код для конкатенации, поскольку он прост и достаточно хорошо работает.

(Если вам интересно, мы используем .NET и SqlServer)


Ошибка!, Ошибка!

Цитата из правил и правил для GetHashCode Эрика Липперта

Документация для Заметки System.String.GetHashCode в частности, что два идентичных строки могут иметь разные хэш-коды в разных версиях CLR и на самом деле они это делают. Не хранить строку хэши в базах данных и быть одинаковыми навсегда, потому что они не будет.

Так что String.GetHashcode() не должен использоваться для этого.

Ответ 1

Стандартная практика Java - просто написать

final int prime = 31;
int result = 1;
for( String s : strings )
{
    result = result * prime + s.hashCode();
}
// result is the hashcode.

Ответ 2

Ваш первый вариант имеет единственное неудобство (String1, String2), создающее тот же хэш-код (String2, String1). Если это не проблема (например, потому что у вас есть заказ на исправление), это нормально.

"Кошка вся строка вместе, а затем получить хэш-код" кажется более естественной и безопасной для меня.

Обновление. Как отмечается в комментарии, у этого есть недостаток, что список ( "x", "yz" ) и ( "xy", "z" ) даст тот же хеш. Чтобы этого избежать, вы можете присоединиться к строкам с разделителем строк, который не может появляться внутри строк.

Если строки большие, вы можете предпочесть хэш-выбор каждого из них, cat-хэш-коды и перефразировать результат. Больше ЦП, меньше памяти.

Ответ 3

Я не вижу причин не конкатенировать строки и вычислить хэш-код для конкатенации.

Как аналогия, скажем, что я хотел вычислить контрольную сумму MD5 для блока памяти, я бы не разбил блок на меньшие части и вычислил отдельные контрольные суммы MD5 для них, а затем объединил их с каким-то специальным методом.

Ответ 4

Другой способ, который появляется в моей голове, цепочка xors с повернутыми хэшами на основе индекса:

int shift = 0;
int result = 1;
for(String s : strings)
{
    result ^= (s.hashCode() << shift) | (s.hashCode() >> (32-shift)) & (1 << shift - 1);
    shift = (shift+1)%32;
}

edit: прочитав объяснение, данное в эффективной java, я думаю, что код geoff будет намного более эффективным.

Ответ 5

Решение на основе SQL может основываться на функциях контрольной суммы и checksum_agg. Если я следую за ним правильно, у вас есть что-то вроде:

MyTable
  MyTableId
  HashCode

MyChildTable
  MyTableId  (foreign key into MyTable)
  String

с различными строками для данного элемента (MyTableId), хранящегося в MyChildTable. Чтобы вычислить и сохранить контрольную сумму, отражающую эти строки (никогда не изменившиеся), должно работать что-то вроде этого:

UPDATE MyTable
 set HashCode = checksum_agg(checksum(string))
 from MyTable mt
  inner join MyChildTable ct
   on ct.MyTableId = mt.MyTableId
 where mt.MyTableId = @OnlyForThisOne

Я считаю, что это не зависит от порядка, поэтому строки "Быстро коричневый" будут производить ту же контрольную сумму, что и "коричневый Быстро".

Ответ 6

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

Равномерность Hashcode!= равенство значений

Будут множество наборов строк, которые дают одинаковый хэш-код, но не всегда будут равны.

Ответ 7

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

Если это так, это не имеет особого значения, если используемая вами схема дает уникальные номера для разных строк/комбинаций строк. Я бы начал с просто конкатенирования строк и вычисления String.hashCode() и просмотра, если вы закончите с уникальными номерами. Если вы этого не сделаете, вы можете попробовать:

  • вместо конкатенации строк, объединить хеш-коды строк компонентов и попробовать разные множители (например, если вы хотите идентифицировать сочетания двухстрочных последовательностей, попробуйте HC1 + 17 * HC2, если это не дает уникальных номеров, попробуйте HC1 + 31 * HC2, затем попробуйте 19, затем попробуйте 37 и т.д. - по сути, любое небольшое количество нечетных чисел будет хорошо).
  • если вы не получите уникальные номера таким образом - или если вам нужно справиться с множеством возможностей расширения - тогда рассмотрите более сильный хэш-код. 64-битный хэш-код является хорошим компромиссом между легкостью сравнения и вероятностью уникальности хешей.

Возможная схема для 64-битного хэш-кода выглядит следующим образом:

  • сгенерируйте массив из 256 64-битных случайных чисел с использованием довольно сильной схемы (вы можете использовать SecureRandom, но XORShift будет работать нормально)
  • выберите "m", другое "случайное" 64-битное, нечетное число с более или менее половиной его битов, установленных
  • для генерации хэш-кода, пройти через каждое значение байта, b, составить строку и взять b-й номер из вашего массива случайных чисел; затем XOR или добавьте его с текущим значением хэша, умноженным на "m"

Таким образом, реализация, основанная на значениях, предлагаемых в Numerical Recipes, будет:

  private static final long[] byteTable;
  private static final long HSTART = 0xBB40E64DA205B064L;
  private static final long HMULT = 7664345821815920749L;

  static {
    byteTable = new long[256];
    long h = 0x544B2FBACAAF1684L;
    for (int i = 0; i < 256; i++) {
      for (int j = 0; j < 31; j++) {
        h = (h >>> 7) ^ h;
        h = (h << 11) ^ h;
        h = (h >>> 10) ^ h;
      }
      byteTable[i] = h;
    }
  }

Вышеуказанное инициализирует наш массив случайных чисел. Мы используем генератор XORShift, но мы могли бы использовать любой довольно качественный генератор случайных чисел (создавая SecureRandom() с определенным семенем, тогда вызов nextLong() будет прекрасен). Затем для генерации хеш-кода:

  public static long hashCode(String cs) {
    if (cs == null) return 1L;
    long h = HSTART;
    final long hmult = HMULT;
    final long[] ht = byteTable;
    for (int i = cs.length()-1; i >= 0; i--) {
      char ch = cs.charAt(i);
      h = (h * hmult) ^ ht[ch & 0xff];
      h = (h * hmult) ^ ht[(ch >>> 8) & 0xff];
    }
    return h;
  }

Руководство для рассмотрения состоит в том, что, учитывая хэш-код из n бит, вы обычно должны генерировать хэши в порядке строк 2 ^ (n/2), прежде чем вы получите столкновение. Или по-другому, с 64-битным хешем, вы ожидаете столкновения после примерно 4 миллиардов строк (так что если вы имеете дело до, скажем, нескольких миллионов строк, шансы на столкновение довольно незначительны).

Другим вариантом будет MD5, который является очень сильным хешем (практически безопасным), но это 128-битный хеш, поэтому у вас есть небольшой недостаток иметь дело со 128-битными значениями. Я бы сказал, что MD5 является излишним для этих целей - как я уже сказал, с 64-битным хешем вы можете справиться достаточно безопасно с порядком нескольких миллионов строк.

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

Ответ 8

Использование GetHashCode() не идеально подходит для объединения нескольких значений. Проблема в том, что для строк хеш-код является просто контрольной суммой. Это оставляет мало энтропии для подобных значений. например добавление хэш-кодов для ( "abc", "bbc" ) будет таким же, как ( "abd", "abc" ), вызывая столкновение.

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

public static class Fnv1 {
    public const uint OffsetBasis32 = 2166136261;
    public const uint FnvPrime32 = 16777619;

    public static int ComputeHash32(byte[] buffer) {
        uint hash = OffsetBasis32;

        foreach (byte b in buffer) {
            hash *= FnvPrime32;
            hash ^= b;
        }

        return (int)hash;
    }
}

Ответ 10

Если вы используете Java, вы можете создать массив строк (или преобразовать коллекцию в массив), а затем использовать Arrays.hashCode(), как описано здесь.

Ответ 11

Позвольте решить вашу проблему с корнем.

Не используйте хэш-код. Просто добавьте целочисленный первичный ключ для каждой строки