Почему XOR является способом объединения хэшей по умолчанию?

скажем, у вас есть два хэши H(A) и H(B) и вы хотите объединить их. Я читал, что хороший способ объединить два хэша-это XOR, например XOR( H(A), H(B) ).

лучшее объяснение, которое я нашел, кратко затронуто здесь на этих рекомендации по хэш-функции:

XORing двух чисел с примерно случайным распределением приводит к другому числу, все еще с примерно случайным распределением*, но которое теперь зависит от двух ценности.
...
* В каждом бите из двух чисел для объединения выводится 0, если два бита равны, иначе-1. Другими словами, в 50% комбинаций будет выводиться 1. Поэтому, если два входных бита имеют примерно 50-50 шансов быть 0 или 1, то так же будет и выходной бит.

можете ли вы объяснить интуицию и / или математику, почему XOR должен быть операцией по умолчанию для объединения хэш-функций (а не ИЛИ ИЛИ и т. д.)?

8 ответов


предполагая равномерно случайные (1-битные) входы, распределение вероятности выхода и функции составляет 75% 0 и 25% 1. И наоборот, или 25% 0 и 75% 1.

функция XOR составляет 50% 0 и 50% 1, поэтому он хорош для объединения равномерных распределений вероятностей.

это можно увидеть, написав таблицы истины:

 a | b | a AND b
---+---+--------
 0 | 0 |    0
 0 | 1 |    0
 1 | 0 |    0
 1 | 1 |    1

 a | b | a OR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    1

 a | b | a XOR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    0

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


xor-опасная функция по умолчанию для использования при хешировании. Это лучше, чем " и " И "ИЛИ", но это мало о чем говорит.

xor симметричен, поэтому порядок элементов теряется. Так что "bad" хэш будет объединять то же самое, что и "dab".

xor сопоставляет одинаковые значения с нулем, и вы должны избегать сопоставления "общих" значений с нулем:

так (a,a) сопоставляется с 0 и (b,b) также сопоставляется с 0. Поскольку такие пары более распространены, чем случайность может означать, что вы в конечном итоге с гораздо большим количеством столкновений на нуле, чем вы должны.

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

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

так hash(a) + hash(b) лучше в том, что если a==b, в результате вместо hash(a)<<1 вместо 0.

это остается симметричным. Мы можем нарушить эту симметрию за скромную цену:

hash(a)<<1 + hash(a) + hash(b)

ака hash(a)*3 + hash(b). (вычисляя hash(a) один раз и хранение рекомендуется, если вы используете решение shift). Любая нечетная константа вместо 3 будет bijectively карте size_t (или K-разрядная беззнаковая константа) к себе, так как отображение на беззнаковых константах математика по модулю 2^k для некоторых k, и любая нечетная константа относительно проста до 2^k.

для еще более причудливой версии, мы можем рассмотреть boost::hash_combine, которая эффективно:

size_t hash_combine( size_t lhs, size_t rhs ) {
  lhs^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2);
  return lhs;
}

здесь мы добавляем вместе некоторые сдвинутые версии seed С константой (которая в основном случайна 0s и 1s -- в частности, это инверсия золотого сечения в виде 32-битной фиксированной точечной дроби) с некоторым добавлением и xor. Это нарушает симметрию, и вводит некоторый "шум", если входящие хэшированные значения плохие (т. е. Представьте, что каждый компонент хэширует до 0 -- выше обрабатывает его хорошо, генерируя мазок 1 и 0s после каждого комбайна. Мой просто выводит 0).

для тех, кто не знаком с C/C++, имеет size_t - это целое значение без знака, которое достаточно велико, чтобы описать размер любого объекта в памяти. В 64-разрядной системе это обычно 64-разрядное целое число без знака. В 32-разрядной системе 32 бит без знака целое число.


несмотря на свои сподручные бит-смешивая свойства, XOR не хороший способ объединить хэши из-за его коммутативности. Рассмотрим, что произойдет, если вы сохраните перестановки {1, 2,..., 10} в хэш-таблице из 10 кортежей.

гораздо лучший выбор m * H(A) + H(B), где m - большое нечетное число.

кредит: вышеупомянутый комбайнер был подсказкой от Боба Дженкинса.


Xor может быть" стандартным " способом объединения хэшей, но ответ Грега Хьюгилла также показывает, почему у него есть свои подводные камни: Xor двух идентичных хэш-значений равен нулю. В реальной жизни идентичные хэши встречаются чаще, чем можно было ожидать. Затем вы можете обнаружить, что в этих (не столь редких) угловых случаях результирующие объединенные хэши всегда одинаковы (ноль). Хэш-столкновения будут намного, намного чаще, чем вы ожидаете.

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


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

сначала я хочу определить две простые функции, которые я буду использовать для объяснения этого: Min () и Max ().

Min(A, B) вернет значение, меньшее между A и B, например: Min (1, 5) возвращает 1.

Max (A, B) вернет значение, которое больше между A и B, например: Max (1, 5) возвращает 5.

Если вам дают: C = A AND B

тогда вы можете найти это C <= Min(A, B) мы знаем это, потому что нет ничего, что вы можете и с 0 битами A или B, чтобы сделать их 1s. Таким образом, каждый нулевой бит остается нулевым битом, и каждый бит имеет шанс стать нулевым битом (и, следовательно, меньшим значением).

С: C = A OR B

верно и обратное: C >= Max(A, B) С этим, мы видим следствие и функции. Любой бит, который уже один не может быть назван нулем, поэтому он остается единицей, но каждый нулевой бит имеет шанс стать единицей, а следовательно, и большим числом.

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

для XOR нет подразумеваемого ограничения на основе входных данных. Есть особые случаи, когда вы можете найти это, если вы XOR байт с 255 чем вы получаете обратный, но любой возможный байт может быть выведен из этого. Каждый бит имеет шанс изменить состояние в зависимости от того же бита в другом операнде.


если вы XOR случайный вход со смещенным входом, выход является случайным. То же самое не верно для AND или OR. Пример:

00101001 XOR 00000000 = 00101001
00101001 AND 00000000 = 00000000
00101001 OR  11111111 = 11111111

как упоминает @Greg Hewgill, даже если и входные сигналы случайны, используя AND или OR приведет к смещенному выходу.

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


исходный код для различных версий hashCode() на java.утиль.Массивы является отличной ссылкой для твердых алгоритмов хэширования общего использования. Их легко понять и перевести на другие языки программирования.

грубо говоря, большинство многопараметрической hashCode() реализации следуют этому шаблону:

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

вы можете искать другие StackOverflow Q & As для получения дополнительной информации о магии позади 31, и почему Java-код использует его так часто. Она несовершенна, но имеет очень хорошие характеристики.


покройте левые 2 столбца и попробуйте выяснить, какие входы используют только выход.

 a | b | a AND b
---+---+--------
 0 | 0 |    0
 0 | 1 |    0
 1 | 0 |    0
 1 | 1 |    1

когда вы увидели 1-бит, вы должны были понять, что оба входа были 1.

теперь сделайте то же самое для XOR

 a | b | a XOR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    0

XOR ничего не выдает об этом входах.