Как обрабатывать хеш-коллизии для словарей в Swift

TL; DR

Моя настраиваемая структура реализует протокол Hashable. . Однако, когда при вставке ключей в Dictionary возникают хеш-коллизии, они не обрабатываются автоматически. Как мне решить эту проблему?

Фон

Я ранее задавал этот вопрос Как реализовать протокол Hashable в Swift для массива Int (настраиваемой строковой структуры). Позже я добавил мой собственный ответ, который, похоже, работал.

Однако недавно я обнаружил небольшую проблему с hashValue коллизиями при использовании Dictionary.

Самый простой пример

Я максимально упростил код до следующего примера.

Собственная структура

struct MyStructure: Hashable {

    var id: Int

    init(id: Int) {
        self.id = id
    }

    var hashValue: Int {
        get {
            // contrived to produce a hashValue collision for id=1 and id=2
            if id == 1 {
                return 2 
            }
            return id
        }
    }
}

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

Обратите внимание на глобальную функцию для перегрузки оператора равенства (==), чтобы соответствовать Equatable Protocol, который требуется для протокола Hashable.

Тонкая ключевая проблема со словарем

Если я создам Dictionary с MyStructure в качестве ключа

var dictionary = [MyStructure : String]()

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

dictionary[ok] = "some text"
dictionary[collision1] = "other text"
dictionary[collision2] = "more text"

print(dictionary) // [MyStructure(id: 2): more text, MyStructure(id: 0): some text]
print(dictionary.count) // 2

одинаковые хеш-значения приводят к перезаписи ключа collision1 ключом collision2. Предупреждения нет. Если такое столкновение произошло только один раз в словаре со 100 ключами, то его легко можно было бы пропустить. (Мне потребовалось некоторое время, чтобы заметить эту проблему.)

Очевидная проблема со словарным литералом

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

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

let dictionaryLiteral = [
    ok : "some text",
    collision1 : "other text",
    collision2 : "more text"
]
// fatal error: Dictionary literal contains duplicate keys

Вопрос

У меня создалось впечатление, что hashValue не обязательно всегда возвращать уникальное значение. Например, Мэтт Томпсон говорит,

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

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

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

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

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

Этот комментарий заставляет меня думаю, что ответ заключается в том, как реализован протокол Equatable, но я не уверен, как мне его изменить.

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

Эта функция вызывается при каждом поиске ключа словаря или только при конфликте хешей? (Обновление: см. этот вопрос)

Что мне делать, чтобы определить уникальность, когда (и только когда) возникает хеш-коллизия?


person Suragch    schedule 27.07.2015    source источник
comment
Swift Comparison Protocols также является полезной ссылкой.   -  person Suragch    schedule 28.07.2015
comment
Вы возвращаете 1 & 2 для hashValues ​​только для демонстрации столкновения. правильно?   -  person Honey    schedule 18.11.2016
comment
Мое возвращение 2 и 2 для hashValues ​​является демонстрацией коллизии. (Идентификаторы: 1 и 2.) @Honey   -  person Suragch    schedule 19.11.2016


Ответы (4)


func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

Обратите внимание на глобальную функцию для перегрузки оператора равенства (==), чтобы соответствовать протоколу Equatable, который требуется для протокола Hashable.

Ваша проблема - неправильная реализация равенства.

Для хэш-таблицы (такой как Swift Dictionary или Set) требуются отдельные реализации равенства и hash.

hash приближает вас к объекту, который вы ищете; равенство дает вам именно тот объект, который вы ищете.

Ваш код использует ту же реализацию для hash и равенства, и это гарантирует коллизию.

Чтобы решить эту проблему, реализуйте равенство для соответствия точным значениям объекта (однако ваша модель определяет равенство). Например.:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
}
person Darren    schedule 28.07.2015
comment
Я очень запутался. Из 2 фрагментов кода, которые у вас есть, 1-я реализация Hashable и другая реализация equatable. Нужно ли мне реализовать их оба, чтобы соответствовать хешируемому? Если да, то как компилятор узнает, какой из них выбрать, если предположить, что они оба имеют одинаковую сигнатуру метода ?! - person Honey; 18.11.2016
comment
Я только что попытался написать еще == для равенства и получил недействительное повторное объявление == @Suragch - person Honey; 18.11.2016
comment
@ Дорогой, первым фрагментом кода был Даррен, цитирующий мой неправильный код. Второй фрагмент кода - правильная версия Даррена. Протокол Hashable требует, чтобы протокол Equitable разрешал любые ситуации, в которых возникают конфликты хеш-функции (т. Е. Каждый раз, когда два разных объекта имеют одинаковое значение хеш-функции). Я вижу из вашего собственного ответа, что вы уже догадались об этом. - person Suragch; 19.11.2016
comment
@ Дорогой, я полагаю, вы тоже видели этот ответ. Это то, что я написал после того, как проработал все это. В разделе Big Picture показаны все части, которые необходимы для протокола Hashable. - person Suragch; 19.11.2016
comment
@ Дорогой, мое предыдущее утверждение о цели протокола Equatable кажется неверным. См. этот вопрос. - person Suragch; 09.12.2016

Я думаю, у вас есть все необходимые кусочки головоломки - вам просто нужно собрать их воедино. У вас есть множество отличных источников.

Коллизии хэшей в порядке. Если возникает конфликт хешей, вместо этого объекты будут проверяться на равенство (только по отношению к объектам с совпадающими хэшами). По этой причине соответствие объектов Equatable должно основываться на чем-то другом, кроме hashValue, если вы не уверены, что хеши не могут конфликтовать.

Это точная причина того, что объекты, соответствующие Hashable, также должны соответствовать Equatable. Swift нужен более специфичный для предметной области метод сравнения, когда хеширование не помогает.

В той же статье NSHipster вы можете увидеть, как Мэтт реализует isEqual: по сравнению с hash в своем примере Person класса. В частности, у него есть isEqualToPerson: метод, который проверяет другие свойства человека (дата рождения, полное имя) для определения равенства.

- (BOOL)isEqualToPerson:(Person *)person {
  if (!person) {
    return NO;
  }

  BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
  BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

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

Точно так же Swift не позволяет вам просто использовать объект Hashable в качестве словарного ключа - неявно, посредством наследования протокола - ключи также должны соответствовать Equatable. Для стандартных библиотечных типов Swift об этом уже позаботились, но для ваших пользовательских типов и классов вы должны создать свою собственную == реализацию. Вот почему Swift не обрабатывает автоматически конфликты словаря с пользовательскими типами - вы должны реализовать Equatable самостоятельно!

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

if person1 === person2 {
    // ...
}

Здесь нет гарантии, что person1 и person2 имеют разные свойства, просто они занимают отдельное место в памяти. И наоборот, в более раннем методе isEqualToPerson: нет гарантии, что два человека с одинаковыми именами и датами рождения на самом деле являются одними и теми же людьми. Таким образом, вы должны подумать о том, что имеет смысл для конкретного типа объекта. Опять же, еще одна причина, по которой Swift не реализует Equatable за вас на пользовательских типах.

person justinpawela    schedule 27.07.2015
comment
Вы сказали, что если происходит коллизия хешей, вместо этого объекты будут проверяться на равенство (только с объектами с совпадающими хэшами). Пока я тестировал это, я заметил какое-то странное поведение, которое, похоже, идет вразрез с тем, что я ожидал. См. этот вопрос, который я только что задал. - person Suragch; 28.07.2015
comment
@Suragch Теоретически объект должен быть протестирован только на предмет столкновения с объектами, но разработчики Swift могут реализовать свой тип Dictionary, как им нравится, при условии, что он функционирует должным образом для конечного пользователя. Они не дают никаких гарантий относительно того, сколько раз ключи будут проверяться на равенство, и, таким образом, они могут оптимизировать тип словаря, как они считают нужным. У них могут быть сумасшедшие оптимизации, которые действительно улучшают производительность Dictionary, но внутренне работают иначе, чем базовая модель хэш-карты. Единственное, что имеет значение, - это то, что API-интерфейсы типа работают, как задокументировано. - person justinpawela; 28.07.2015

равные значения хэша приводят к перезаписи ключа collision1 ключом collision2. Предупреждения нет. Если такое столкновение произошло только один раз в словаре со 100 ключами, то его легко можно было бы пропустить.

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

Dictionary операции работают над равенством (==) ключей. Словари не содержат повторяющихся ключей (т. Е. Одинаковых ключей). Когда вы устанавливаете значение с помощью ключа, он перезаписывает любую запись, содержащую одинаковый ключ. Когда вы получаете запись с нижним индексом, она находит значение с ключом, который равен, но не обязательно такой же, как то, что вы указали. И так далее.

collision1 и collision2 равны (==) в зависимости от того, как вы определили оператор ==. Следовательно, установка записи с ключом collision2 должна перезаписать любую запись с ключом collision1.

P.S. То же самое и со словарями на других языках. Например, в Какао NSDictionary не позволяет дублировать ключи, то есть ключи, которые имеют isEqual:. В Java Maps не допускают дублирования ключей, т. Е. Ключей .equals().

person user102008    schedule 28.07.2015

Вы можете увидеть мои комментарии к ответам на этой странице и этот ответ. Я думаю, что все ответы по-прежнему написаны ОЧЕНЬ запутанно.

tl; dr 0) вам не нужно писать реализацию isEqual, т.е. == между hashValues. 1) Только предоставить / вернуть hashValue. 2) просто реализуйте Equatable, как обычно

0) Чтобы соответствовать hashable, вы должны иметь вычисленное значение с именем hashValue и присвоить ему соответствующее значение. В отличие от equatable протокола, сравнение hashValues ​​уже существует. Вам НЕ нужно писать:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
    // Snippet A
}

1) Затем он использует hashValue, чтобы проверить, существует ли для этого индекса hashValue (рассчитанного по модулю против счетчика массива) просматриваемый ключ или нет. Он просматривает массив пар ключ / значение этого индекса.

2) Однако в качестве отказоустойчивого, т. е. в случае совпадения хешей, вы возвращаетесь к обычной функции ==. (Логически он вам нужен из-за отказоустойчивости. Но он вам также нужен, потому что протокол Hashable соответствует Equatable, и поэтому вы должны написать реализацию для ==. В противном случае вы получите ошибку компилятора)

func == (lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
    //Snippet B
}

Вывод:

Вы должны включить фрагмент B, исключить фрагмент A, но при этом иметь свойство hashValue

person Honey    schedule 18.11.2016
comment
Пункт 1 не имеет смысла. hashValue НЕ используется для проверки равенства двух объектов. Совершенно допустимо, чтобы два неравных объекта имели одинаковое хеш-значение. Требуется только то, что два равных объекта должны иметь одинаковое хеш-значение. - person rmaddy; 01.05.2018
comment
@rmaddy Спасибо за отзыв. Я действительно недавно освежил свои навыки структур данных немного. Сейчас лучше? - person Honey; 01.05.2018
comment
О каком массиве вы говорите? - person rmaddy; 01.05.2018
comment
Из того, что я узнал, здесь. Словари хранятся в двухмерном массиве. Где каждый ключ добавляется к внутреннему массиву по index внешнего массива key's hashValue % array.count. Я предполагаю, что на самом деле он не хранится в массиве, но это то, что вас просят сделать на собеседовании ... - person Honey; 01.05.2018
comment
Это деталь реализации. Нет никакой гарантии, что Swift Dictionary реализован именно так. И это не имеет отношения к этому вопросу и не требует ответа. - person rmaddy; 01.05.2018