Ваш хеш-метод имеет фиксированный диапазон. Это означает, что один элемент может привести к созданию 214748 сегментов (если его хэш-код перефразирован в 214747). Более часто используемый (и почти всегда лучший подход) состоит в том, чтобы начинать с начального размера, который либо известен (благодаря знанию предметной области), чтобы он был достаточно большим для всех значений, либо начинал с малого, и размер хэш-таблицы изменялся бы соответствующим образом. С повторным исследованием очевидной мерой необходимости изменения размера является то, сколько повторной проверки было необходимо. Если вы экспериментируете здесь с цепочкой, вам нужно уменьшить средний и максимальный размеры цепей. Это сокращает время поиска в худшем случае и, следовательно, среднее время поиска ближе к O (1) в лучшем случае.
Два наиболее распространенных подхода к такому хешированию (и, следовательно, к исходному размеру таблицы) - это использовать простые числа или степени двойки. Считается, что первый вариант (хотя по этому поводу имеется определенное противоречие) предлагает лучшее распределение ключей, в то время как второй обеспечивает более быстрое вычисление (оба случая выполняют по модулю входной хеш-код, но с числом, известным как степень 2). По модулю можно быстро выполнить как двоичные операции, так и операции. Еще одно преимущество использования степени двойки при объединении в цепочку состоит в том, что можно проверить цепочку, чтобы увидеть, действительно ли изменение размера хеша приведет к разделению цепочки или нет (если у вас есть таблица с 8 значениями и есть цепочка все хэши которых равны 17, 1 или 33, тогда удвоение размера таблицы все равно оставит их в одной цепочке, но увеличение в четыре раза приведет к их перераспределению).
У вас нет метода, предлагающего семантику замены, которая обычно используется в типах словарей .NET (при добавлении произойдет ошибка, если элемент с таким ключом уже есть, но присвоение индекса не будет).
Ваша ошибка при поиске, которая будет пытаться выйти за пределы количества сегментов, не будет иметь смысла для пользователя, которому все равно, существует ли этот сегмент или нет, только для ключа (им не нужно знать, как работает ваша реализация совсем). В обоих случаях, когда ключ не найден, следует выдавать одну и ту же ошибку (System.Collections.Generic.KeyNotFoundException
имеет точно правильную семантику, поэтому вы можете использовать ее повторно.).
Использование List
довольно тяжело в этом случае. В общем, я бы нахмурился, если бы кто-то сказал, что коллекция BCL была слишком тяжелой, но когда дело доходит до проката ваших собственных коллекций, это обычно либо потому, что (1) вы хотите извлечь уроки из упражнения, либо (2) коллекции BCL не подходят ваши цели. В случае (1) вы должны узнать, как выполнить начатое задание, а в случае (2) вы должны быть уверены, что у List
нет ошибок, которые вы обнаружили с помощью Dictionary
.
Ваше удаление выдает бессмысленную ошибку для того, кто не знает о деталях реализации, и несовместимую ошибку (если в этом контейнере есть что-то еще, это не то, о чем им следует заботиться). Поскольку удаление несуществующего элемента не является вредным, чаще всего просто возвращать логическое значение, указывающее, присутствовал элемент или нет, и позволить пользователю решить, указывает ли это на ошибку или нет. Также расточительно продолжать поиск по всему ведру после того, как предмет был удален.
Ваша реализация теперь допускает нулевые ключи, что является достаточно разумным (действительно, документация для IDictionary<TKey, TValue>
говорит, что реализации могут или не могут сделать это). Однако вы отклоняете их путем возврата NullReferenceException
, вызванного попыткой вызова GetHashCode()
при нулевом значении, вместо проверки и выдачи ArgumentNullException
. Для пользователя получить NullReferenceException
предполагает, что сама коллекция была нулевой. Следовательно, это явная ошибка.