Могут ли хэш-таблицы действительно быть O (1)?

Похоже, что хэш-таблицы могут достигать O (1), но это никогда не имело для меня смысла. Может кто-нибудь объяснить это? Вот две ситуации, которые приходят на ум:

а. Значение int меньше размера хэш-таблицы. Поэтому значение представляет собой собственный хеш, поэтому нет хеш-таблицы. Но если бы это было, это было бы O (1) и все еще было бы неэффективным.

В. Вы должны вычислить хэш значения. В этой ситуации порядок O (n) для размера просматриваемых данных. Поиск может быть O (1) после того, как вы выполняете O (n), но это все еще выходит на O (n) в моих глазах.

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

Я считаю, что хеш-таблицы потрясающие, но я не получаю обозначение O (1), если только он не должен быть теоретическим.

Wikipedia статья для хэш-таблиц постоянно ссылается на постоянное время поиска и полностью игнорирует стоимость хэш-функции. Это действительно справедливая мера?


Изменить: Подводя итог тому, что я узнал:

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

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

Ответ 1

Здесь есть две переменные: m и n, где m - длина ввода, а n - количество элементов в хеше.

Требование производительности O (1) поиска составляет как минимум два предположения:

  • Ваши объекты могут быть равномерными по сравнению с O (1) временем.
  • Будут несколько столкновений хэшей.

Если ваши объекты являются переменными размерами, а проверка равенства требует просмотра всех битов, тогда производительность будет равна O (m). Однако хеш-функция не должна быть O (m) - она ​​может быть O (1). В отличие от криптографического хэша, хеш-функция для использования в словаре не должна смотреть на каждый бит во входе, чтобы вычислить хэш. Реализации могут смотреть только на фиксированное количество бит.

Для достаточно большого количества элементов количество элементов станет больше, чем количество возможных хэшей, а затем вы получите столкновение, вызывающее повышение производительности выше O (1), например O (n) для простого связанного обхода списка (или O (n * m), если оба предположения ложны).

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

Ответ 2

Вы должны вычислить хэш, поэтому порядок O (n) для размера просматриваемых данных. Поиск может быть O (1) после того, как вы выполняете O (n), но это все еще выходит на O (n) в моих глазах.

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

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

Не обязательно. Ведрам необязательно должны быть списки или массивы, они могут быть любыми типами контейнеров, такими как сбалансированный BST. Это означает O(log n) худший случай. Но именно поэтому важно выбрать хорошую хэширующую функцию, чтобы не помещать слишком много элементов в одно ведро. Как указывал KennyTM, в среднем вы все равно получите время O(1), даже если иногда вам приходится копать ковш.

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


Вы упоминаете использование строк как ключей в одном из ваших других комментариев. Вы обеспокоены тем, сколько времени требуется для вычисления хэша строки, потому что оно состоит из нескольких символов? Как заметил еще один человек, вам необязательно смотреть на все символы, чтобы вычислить хэш, хотя это могло бы привести к лучшему хешу, если бы вы это сделали. В этом случае, если в вашем ключе есть в среднем m символы, и вы использовали их все, чтобы вычислить свой хэш, тогда, полагаю, вы правы, что поиск будет принимать O(m). Если m >> n, то может возникнуть проблема. В этом случае вам, вероятно, будет лучше с BST. Или выберите более дешевую хэш-функцию.

Ответ 3

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

Вычисление хэша не должно быть особенно дорогостоящей операцией - здесь мы не говорим о криптографических хеш-функциях. Но это мимо. Сам расчет функции хеш не зависит от числа элементов n; в то время как это может зависеть от размера данных в элементе, это не относится к n. Таким образом, вычисление хеша не зависит от n и также O (1).

Ответ 4

Хеширование - это O (1), только если в таблице есть только постоянное количество ключей, и некоторые другие предположения сделаны. Но в таких случаях это имеет преимущество.

Если ваш ключ имеет n-разрядное представление, ваша хеш-функция может использовать 1, 2,... n этих бит. Думая о хэш-функции, которая использует 1 бит. Оценка O (1) наверняка. Но вы только разделяете ключевое пространство на 2. Таким образом, вы сопоставляете целых 2 ^ (n-1) ключей в один и тот же бит. используя поиск BST, это занимает до n-1 шагов, чтобы найти конкретный ключ, если он почти заполнен.

Вы можете расширить это, чтобы увидеть, что если ваша хеш-функция использует K-бит, размер вашего бина составляет 2 ^ (n-k).

поэтому хэш-функция K-бит == > не более 2 ^ K эффективных бинов == > до 2 ^ (n-K) n-разрядных ключей для каждого бина == > (n-K) шагов (BST) для разрешения конфликтов. На самом деле большинство хеш-функций намного менее "эффективны" и требуют/используют больше, чем K бит, чтобы создать 2 ^ k бункеров. Поэтому даже это оптимистично.

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

Однако это НЕ, как/когда вы используете хеш-таблицу!

Анализ сложности предполагает, что для n-разрядных ключей в таблице могут быть ключи O (2 ^ n) (например, 1/4 всех возможных ключей). Но большинство, если не все время мы используем хеш-таблицу, у нас есть только постоянное число n-разрядных ключей в таблице. Если вам нужно только постоянное количество ключей в таблице, скажите, что C - ваше максимальное число, тогда вы можете сформировать хеш-таблицу из O (C) бункеров, которая гарантирует ожидаемое постоянное столкновение (с хорошей хэш-функцией); и хэш-функцию с использованием ~ logC из n бит в ключе. Тогда каждый запрос равен O (logC) = O (1). Вот как люди утверждают, что доступ к хеш-таблице - O (1) "/

Здесь есть пара уловов - во-первых, заявив, что вам не нужны все биты, это может быть только биллинговый трюк. Сначала вы не можете передать значение ключа хеш-функции, потому что это будет перемещать n бит в памяти, которая является O (n). Так что вам нужно сделать, например. опорный проход. Но вы все равно должны хранить его где-то уже, что было операцией O (n); вы просто не выставляете это на хеширование; вы не можете избежать этой общей задачи вычисления. Во-вторых, вы делаете хэширование, находите бит и находите более 1 клавиши; ваша стоимость зависит от вашего метода разрешения - если вы используете сравнение (BST или List), у вас будет операция O (n) (клавиша возврата n-бит); если вы делаете второй хеш, ну, у вас такая же проблема, если у 2-го хэша есть столкновение. Таким образом, O (1) не гарантируется на 100%, если у вас нет столкновения (вы можете улучшить шанс, имея таблицу с большим количеством бункеров, чем ключи, но все же).

Рассмотрим альтернативу, например. BST, в этом случае. есть клавиши C, поэтому сбалансированный BST будет O (logC) по глубине, поэтому поиск выполняет шаги O (logC). Однако сравнение в этом случае было бы операцией O (n)... поэтому кажется, что хэширование является лучшим выбором в этом случае.

Ответ 5

TL; DR: Хеш-таблицы гарантируют O(1) ожидаемое время наихудшего случая, если вы выбираете свою хеш-функцию случайным образом из универсального семейства хеш-функций. Ожидаемый наихудший случай отличается от среднего.

Отказ от ответственности: я официально не доказываю, что хеш-таблицы являются O(1), для этого взгляните на это видео с Coursera [ 1 ]. Я также не обсуждаю амортизированные аспекты хеш-таблиц. Это ортогонально дискуссии о хешировании и столкновениях.

Я вижу удивительную путаницу вокруг этой темы в других ответах и комментариях, и постараюсь исправить некоторые из них в этом длинном ответе.

Рассуждая о худшем случае

Существуют разные виды анализа наихудшего случая. Анализ, который до сих пор дает большинство ответов, - это не наихудший случай, а средний случай [ 2 ]. Анализ среднего случая имеет тенденцию быть более практичным. Может быть, ваш алгоритм имеет один плохой входной сигнал в худшем случае, но на самом деле хорошо работает для всех других возможных входных данных. Суть в том, что ваше время выполнения зависит от набора данных, на котором вы работаете.

Рассмотрим следующий псевдокод метода get хеш-таблицы. Здесь я предполагаю, что мы обрабатываем коллизии цепочкой, поэтому каждая запись таблицы представляет собой связанный список пар (key,value). Мы также предполагаем, что количество сегментов m фиксировано, но равно O(n), где n - количество элементов на входе.

function get(a: Table with m buckets, k: Key being looked up)
  bucket <- compute hash(k) modulo m
  for each (key,value) in a[bucket]
    return value if k == key
  return not_found

Как указывали другие ответы, это работает в среднем O(1) и наихудшем случае O(n). Мы можем сделать небольшой набросок доказательства путем вызова здесь. Задача состоит в следующем:

(1) Вы передаете свой алгоритм хэш-таблицы злоумышленнику.

(2) Противник может изучать это и готовиться столько, сколько он хочет.

(3) Наконец, противник дает вам ввод размера n вы можете вставить в свою таблицу.

Вопрос в том, как быстро ваша хеш-таблица на входе противника?

На шаге (1) злоумышленник знает вашу хэш-функцию; на этапе (2) злоумышленник может составить список из n элементов с одинаковым hash modulo m, например, путем случайного вычисления хеша группы элементов; и затем в (3) они могут дать вам этот список. Но, о чудо, поскольку все n элементов хешируются в одном и том же сегменте, вашему алгоритму потребуется O(n) время для обхода связанного списка в этом сегменте. Независимо от того, сколько раз мы повторим вызов, противник всегда выигрывает, и насколько плох ваш алгоритм, наихудший случай O(n).

Почему хеширование - это O (1)?

В предыдущем испытании нас оттолкнуло то, что злоумышленник очень хорошо знал нашу хэш-функцию и мог использовать эти знания для создания наихудшего возможного вклада. Что, если бы вместо того, чтобы всегда использовать одну фиксированную хеш-функцию, у нас фактически был набор хеш-функций H, который алгоритм может произвольно выбирать во время выполнения? Если вам интересно, H называется универсальным семейством хеш-функций [ 3 ]. Хорошо, давайте попробуем добавить немного случайности к этому.

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

function get(a: Table with m buckets and seed r, k: Key being looked up)
  rHash <- H[r]
  bucket <- compute rHash(k) modulo m
  for each (key,value) in a[bucket]
    return value if k == key
  return not_found

Если мы попробуем выполнить задачу еще раз: с шага (1) злоумышленник может узнать все хеш-функции, которые есть в H, но теперь конкретная хеш-функция, которую мы используем, зависит от r. Значение r является частным для нашей структуры, противник не может ни проверять его во время выполнения, ни прогнозировать его заранее, поэтому он не может составить список, который всегда плох для нас. Предположим, что на шаге (2) злоумышленник выбирает один hash функции в H случайным образом, затем он создает список из n столкновений под hash modulo m и отправляет его на шаг (3), скрещивая пальцы во время выполнения H[r] будет тот же hash они выбрали.

Это серьезная ставка для противника, список, который он создал, сталкивается с hash, но будет просто случайным вводом для любой другой хэш-функции в H Если он выиграет эту ставку, наше время выполнения будет в худшем случае O(n) как и раньше, но если он проиграет, то нам просто дадут случайный ввод, который занимает среднее время O(1). И действительно, в большинстве случаев противник проигрывает, он выигрывает только раз в каждом |H| вызовы, и мы можем сделать |H| быть очень большим.

Сравните этот результат с предыдущим алгоритмом, в котором противник всегда побеждал. Здесь немного помахали руками, но так как противник потерпел неудачу в большинстве случаев, и это справедливо для всех возможных стратегий, которые может попробовать противник, из этого следует, что, хотя наихудший случай - O(n), на самом деле ожидаемый худший случай - O(1)


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

Ответ 6

Есть две настройки, при которых вы можете получить O (1) худшее время.

  1. Если ваша установка статична, то хеширование FKS даст вам гарантии O (1) в худшем случае. Но, как вы указали, ваши настройки не являются статичными.
  2. Если вы используете хеширование Cuckoo, то запросы и удаления выполняются в наихудшем случае O (1), но ожидается вставка только O (1). Хеширование кукушки работает довольно хорошо, если у вас есть верхняя граница для общего количества вставок, и вы установите размер таблицы примерно на 25% больше.

Скопировано отсюда

Ответ 7

Основываясь на обсуждении здесь, кажется, что если X - потолок (# элементов в таблице /# бинов), то лучшим ответом будет O (log (X)) при условии эффективной реализации поиска бина.

Ответ 8

О. Значение int меньше размера хеш-таблицы. Следовательно, значение является его собственным хешем, поэтому хеш-таблицы нет. Но если бы оно было, оно было бы O (1) и все равно было бы неэффективным.

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

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

Б. Вы должны вычислить хеш значения. В этой ситуации порядок O (n) для размера ищущихся данных. Поиск может быть O (1) после того, как вы сделаете O (n) работу, но это все еще выходит на O (n) в моих глазах.

Нам нужно различать размер ключа (например, в байтах) и размер количества ключей, хранящихся в хеш-таблице. Утверждения, что хеш-таблицы предоставляют операции O (1), означают, что операции (вставка/стирание/поиск) не имеют тенденцию к дальнейшему замедлению, так как количество ключей увеличивается от сотен до тысяч, от миллионов до миллиардов (по крайней мере, если не все данные доступ к нему/обновляется в столь же быстром хранилище, будь то ОЗУ или диск - эффекты кэша могут вступить в игру, но даже стоимость пропуска кэша в худшем случае имеет тенденцию быть постоянным кратным попаданию в лучший случай)

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

Мировой рекорд Гиннеса для самого длинного имени, которое когда-либо использовалось кем-либо, был установлен Адольфом Блейном Чарльзом Дэвидом Эрлом Фредериком Джеральдом Хьюбером Ирвином Джоном Кеннетом Ллойдом Мартином Неро Оливером Полом Куинси Рэндольфом Шерманом Томасом Ункасом Виктором Уильямом Ксерксом Янси Вулфешлегельсштайнхаузенбергердорф, старший

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

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

Также возможно создать хэш из объема ключевых данных фиксированного размера. Например, Microsoft Visual C++ поставляется с реализацией стандартной библиотеки std::hash<std::string> которая создает хэш, включающий всего десять байтов, равномерно распределенных по строке, поэтому, если строки изменяются только при других индексах, которые вы получаете столкновения (и, следовательно, на практике не O (1) поведения на стороне поиска после столкновения), но время для создания хеша имеет жесткую верхнюю границу.

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

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

Например, при коэффициенте загрузки 1,0 в среднем длина этих линейных поисков составляет ~ 1,58, независимо от количества ключей (см. Мой ответ здесь). Для закрытого хеширования это немного сложнее, но не намного хуже, когда коэффициент загрузки не слишком высок.

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

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

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

средняя длина ковша, обусловленная непустыми ковшами, является лучшим показателем эффективности. Это /(1-e ^ {-a}) [где a - коэффициент загрузки, e - 2,71828...]

Таким образом, один только коэффициент загрузки определяет среднее количество сталкивающихся ключей, которые вы должны искать во время операций вставки/стирания/поиска. Для раздельной цепочки он не просто приближается к постоянству при низком коэффициенте нагрузки - он всегда постоянен. Однако для открытой адресации ваше утверждение имеет некоторую обоснованность: некоторые сталкивающиеся элементы перенаправляются в альтернативные сегменты и могут затем мешать операциям с другими ключами, поэтому при более высоких коэффициентах нагрузки (особенно> 0,8 или 0,9) длина цепочки столкновений становится значительно хуже.

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

Что ж, размер таблицы должен приводить к нормальному коэффициенту загрузки, учитывая выбор хэширования или отдельного сцепления, но также, если хеш-функция немного слабая и ключи не очень случайны, использование простого числа сегментов часто помогает уменьшить тоже коллизии (hash-value % table-size затем оборачивается так, что изменения только одного или двух старших разрядов в хеш-значении все еще разрешаются, чтобы сегменты распределялись псевдослучайно по различным частям хеш-таблицы).