Основная мысль
Почему люди используют комоды для хранения своей одежды? Помимо того, что они выглядят модно и стильно, у них есть то преимущество, что у каждого предмета одежды есть место, где он должен быть. Если вы ищете пару носков, просто загляните в ящик для носков. Если вы ищете рубашку, проверьте ящик, в котором лежат ваши рубашки. Когда вы ищете носки, не имеет значения, сколько у вас рубашек или пар брюк, потому что вам не нужно на них смотреть. Вы просто заглядываете в ящик для носков и ожидаете найти там носки.
На высоком уровне хеш-таблица - это способ хранения вещей, который (вроде как) похож на комод для одежды. Основная идея заключается в следующем:
- Вы получаете некоторое количество мест (ящиков), где можно хранить предметы.
- Вы придумываете какое-то правило, которое сообщает вам, к какому месту (ящику) принадлежит каждый предмет.
- Когда вам нужно что-то найти, вы используете это правило, чтобы определить, в какой ящик искать.
Преимущество такой системы в том, что, если ваше правило не слишком сложное и у вас есть соответствующее количество ящиков, вы можете довольно быстро найти то, что ищете, просто заглянув в нужное место.
Когда вы складываете одежду, вы можете использовать правило, например, что-то вроде носков в левый верхний ящик, рубашек в большой средний ящик и т. Д. Однако, когда вы храните более абстрактные данные, мы используем что-то вызвали хеш-функцию, чтобы сделать это за нас.
Разумный способ думать о хэш-функции - это черный ящик. Вы помещаете данные в одну сторону, а число, называемое хеш-кодом, выходит из другой. Схематично это выглядит примерно так:
+---------+
|\| hash |/| --> hash code
data --> |/| function|\|
+---------+
Все хэш-функции являются детерминированными: если вы поместите одни и те же данные в функцию несколько раз, вы всегда получите одно и то же значение на другой стороне. И хорошая хеш-функция должна выглядеть более или менее случайной: небольшие изменения входных данных должны давать совершенно разные хеш-коды. Например, хэш-коды для строки pudu и для строки kudu, скорее всего, будут сильно отличаться друг от друга. (Опять же, возможно, что они такие же. В конце концов, если выходные данные хеш-функции должны выглядеть более или менее случайными, есть шанс, что мы получим один и тот же хеш-код дважды.)
Как именно построить хеш-функцию? А пока пойдем с порядочными людьми, не стоит об этом слишком много думать. Математики разрабатывали все лучшие и худшие способы разработки хэш-функций, но для наших целей нам действительно не нужно слишком беспокоиться о внутреннем устройстве. Достаточно хорошо думать о хэш-функции как о функции, которая
- детерминированный (равные входы дают равные выходы), но
- выглядит случайным (трудно предсказать один хэш-код по другому).
Когда у нас есть хеш-функция, мы можем построить очень простую хеш-таблицу. Мы сделаем ряд ведер, которые можно рассматривать как аналог ящиков в нашем комоде. Чтобы сохранить элемент в хэш-таблице, мы вычислим хэш-код объекта и будем использовать его в качестве индекса в таблице, что аналогично выбору, в какой ящик помещается этот элемент. Затем мы помещаем этот элемент данных в ведро с этим индексом. Если это ведро было пустым, отлично! Мы можем положить туда предмет. Если это ведро заполнено, у нас есть несколько вариантов того, что мы можем сделать. Простой подход (называемый цепное хеширование < / a>) заключается в том, чтобы рассматривать каждое ведро как список элементов, так же, как ваш ящик для носков может хранить несколько носков, а затем просто добавлять элемент в список по этому индексу.
Чтобы найти что-то в хеш-таблице, мы используем в основном ту же процедуру. Мы начинаем с вычисления хэш-кода для элемента, который нужно найти, который сообщает нам, в каком сегменте (ящике) искать. Если элемент находится в таблице, он должен быть в этом сегменте. Затем мы просто смотрим на все предметы в корзине и проверяем, есть ли там наш предмет.
В чем преимущество этого? Что ж, предполагая, что у нас есть большое количество корзин, мы ожидаем, что в большинстве корзин не будет слишком много вещей. В конце концов, наша хеш-функция вроде как выглядит так, будто имеет случайные выходы, поэтому элементы распределяются вроде как равномерно по всем сегментам. Фактически, если мы формализуем понятие нашей хэш-функции, которое выглядит как бы случайным, мы можем доказать, что ожидаемое количество элементов в каждом сегменте является отношением общего количества элементов к общему количеству сегментов. Таким образом, мы можем найти то, что ищем, без лишних усилий.
Детали
Объяснить, как работает хеш-таблица, немного сложно, потому что существует множество разновидностей хеш-таблиц. В следующем разделе рассказывается о некоторых общих деталях реализации, общих для всех хэш-таблиц, а также о некоторых особенностях работы различных стилей хэш-таблиц.
Первый вопрос, который возникает, - как превратить хэш-код в индекс слота таблицы. В приведенном выше обсуждении я только что сказал использовать хэш-код в качестве индекса, но на самом деле это не очень хорошая идея. В большинстве языков программирования хэш-коды работают с 32-битными или 64-битными целыми числами, и вы не сможете использовать их напрямую в качестве индексов корзины. Вместо этого общая стратегия состоит в том, чтобы создать массив сегментов некоторого размера m, вычислить (полные 32- или 64-битные) хэш-коды для ваших элементов, а затем изменить их по размеру таблицы, чтобы получить индекс от 0 до м-1 включительно. Использование модуля здесь хорошо работает, потому что оно достаточно быстрое и выполняет достойную работу, распределяя полный диапазон хэш-кодов на меньший диапазон.
(Иногда здесь используются побитовые операторы. Если размер вашей таблицы равен степени двойки, скажем, 2 k, то вычисляется побитовое И хэш-кода, а затем число 2 k - 1 эквивалентно вычислению модуля, и это значительно быстрее.)
Следующий вопрос - как правильно выбрать количество ведер. Если вы выберете слишком много ведер, то большинство ведер будет пустым или будет содержать мало элементов (хорошо для скорости - вам нужно всего лишь проверить несколько элементов на ведро), но вы будете использовать кучу места, просто храня ведра (не так отлично, хотя, может быть, вы можете себе это позволить). Верна и обратная сторона этого утверждения: если у вас слишком мало сегментов, то в среднем у вас будет больше элементов на каждый сегмент, поэтому поиск займет больше времени, но вы будете использовать меньше памяти.
Хорошим компромиссом является динамическое изменение количества сегментов в течение времени существования хеш-таблицы. Коэффициент загрузки хеш-таблицы, обычно обозначаемый α, представляет собой отношение количества элементов к количеству сегментов. Большинство хеш-таблиц выбирают некоторый максимальный коэффициент загрузки. Как только коэффициент загрузки превышает этот предел, хеш-таблица увеличивает количество слотов (скажем, удваивая), а затем перераспределяет элементы из старой таблицы в новую. Это называется перефразированием. Предполагая, что максимальный коэффициент загрузки в таблице является постоянным, это гарантирует, что при условии, что у вас есть хорошая хеш-функция, ожидаемые затраты на выполнение поиска останутся равными O (1). Теперь вставки имеют амортизированную ожидаемую стоимость O (1) из-за затрат на периодическое восстановление таблицы, как в случае с удалениями. (Удаление может аналогичным образом сжать таблицу, если коэффициент нагрузки станет слишком мал.)
Стратегии хеширования
До этого момента мы говорили о цепном хешировании, которое является одной из многих различных стратегий построения хеш-таблицы. Напоминаем, что цепное хеширование вроде как выглядит как шкафчик для одежды - каждое ведро (ящик) может содержать несколько элементов, и когда вы выполняете поиск, вы проверяете все эти элементы.
Однако это не единственный способ построить хеш-таблицу. Есть еще одно семейство хэш-таблиц, использующих стратегию под названием открытая адресация strong >. Основная идея открытой адресации заключается в хранении массива слотов, где каждый слот может быть пустым или содержать ровно один элемент.
При открытой адресации, когда вы выполняете вставку, как и раньше, вы переходите к некоторому слоту, индекс которого зависит от вычисленного хэш-кода. Если этот слот бесплатный - отлично! Вы кладете туда предмет, и все готово. Но что, если слот уже заполнен? В этом случае вы используете некоторую вторичную стратегию, чтобы найти другой свободный слот для хранения предмета. Наиболее распространенная стратегия для этого использует подход под названием линейное зондирование < / em>. При линейном зондировании, если желаемый слот уже заполнен, вы просто переходите к следующему слоту в таблице. Если этот слот пуст - отлично! Вы можете положить туда предмет. Но если этот слот заполнен, вы затем переходите к следующему слоту в таблице и т. Д. (Если вы попали в конец таблицы, просто вернитесь к началу).
Линейное зондирование - удивительно быстрый способ построения хеш-таблицы. Кеши ЦП оптимизированы для местоположения ссылки a>, поэтому поиск в памяти в соседних ячейках памяти обычно выполняется намного быстрее, чем поиск в памяти в разрозненных местах. Поскольку вставка или удаление с линейным зондированием работает путем попадания в какой-либо слот массива, а затем линейного движения вперед, это приводит к небольшому количеству промахов в кеше и в конечном итоге оказывается намного быстрее, чем обычно предсказывает теория. (И бывает так, что теория предсказывает, что это будет очень быстро!)
Еще одна стратегия, которая стала популярной в последнее время, - кукушечное хеширование < / а>. Мне нравится думать о хешировании с кукушкой как о замороженных хэш-таблицах. Вместо одной хеш-таблицы и одной хеш-функции у нас есть две хеш-таблицы и две хеш-функции. Каждый элемент может находиться ровно в одном из двух мест - либо в месте в первой таблице, заданном первой хеш-функцией, либо в месте во второй таблице, заданном второй хеш-функцией. Это означает, что поиск наихудший эффективен, поскольку вам нужно проверить только два места, чтобы увидеть, есть ли что-то в таблице.
Вставки при хешировании с кукушкой используют другую стратегию, чем раньше. Мы начинаем с того, что проверяем, свободны ли какие-либо из двух слотов, в которых может быть помещен предмет. Если да, то отлично! Мы просто кладем туда предмет. Но если это не сработает, мы выбираем один из слотов, помещаем туда предмет и выкидываем предмет, который там был раньше. Этот элемент должен куда-то пойти, поэтому мы пытаемся поместить его в другую таблицу в соответствующий слот. Если это сработает - отлично! Если нет, мы выкидываем элемент из этой таблицы и пытаемся вставить его в другую таблицу. Этот процесс продолжается до тех пор, пока все не остановится или мы не окажемся в ловушке цикла. (Последний случай встречается редко, и если это произойдет, у нас есть множество вариантов, например, поместить его во вторичную хеш-таблицу или выбрать новые хеш-функции и перестроить таблицы.)
Есть много улучшений, возможных для хеширования с кукушкой, таких как использование нескольких таблиц, позволяющая каждому слоту содержать несколько элементов и создание тайника, в котором хранятся предметы, которые не могут поместиться в другом месте, и это активная область исследований!
Тогда есть гибридные подходы. Хеширование Hopscotch представляет собой сочетание открытая адресация и цепное хеширование, которые можно представить как взятие связанной хеш-таблицы и сохранение каждого элемента в каждом сегменте в слоте рядом с тем, куда элемент хочет попасть. Эта стратегия хорошо работает с многопоточностью. Швейцарская таблица использует факт что некоторые процессоры могут выполнять несколько операций параллельно с одной инструкцией для ускорения таблицы линейного зондирования. Расширяемое хеширование предназначено для баз данных. и файловые системы и использует сочетание дерева и связанной хеш-таблицы для динамического увеличения размеров сегментов по мере загрузки отдельных сегментов. <сильный > Хеширование Робин Гуда - это вариант линейного исследования, при котором элементы можно перемещать после вставки, чтобы уменьшить разницу в том, как далеко от дома может находиться каждый элемент.
Дальнейшее чтение
Дополнительные сведения об основах хэш-таблиц см. На странице эти слайды с лекцией о цепном хешировании и эти последующие слайды о линейном зондировании и хешировании Робин Гуда a >. Вы можете узнать больше о здесь хеширование кукушки и примерно здесь теоретические свойства хеш-функций.
person
templatetypedef
schedule
11.09.2020