Я просматриваю исходный код HashMap в Java 7 и вижу, что метод put проверит, присутствует ли какая-либо запись, и если она присутствует, то она заменит старое значение на новое значение

    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

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

Теперь, поскольку для данного ключа есть только одна запись, почему метод get имеет цикл FOR, поскольку он мог просто вернуть значение напрямую?

    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }

Я чувствую, что вышеуказанный цикл не нужен. Пожалуйста, помогите мне понять, если я ошибаюсь.

46
pjj 26 Фев 2018 в 15:14

6 ответов

Лучший ответ

table[indexFor(hash, table.length)] - это сегмент HashMap, который может содержать ключ, который мы ищем (если он присутствует в Map).

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

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

62
Eran 26 Фев 2018 в 12:16

Хеш-таблицы имеют сегменты, потому что хеши объектов не обязательно должны быть уникальными. Если хеши объектов равны, значит, объекты, возможно, равны. Если хеши объектов разные, то объекты совершенно разные. Поэтому объекты с одинаковыми хэшами группируются в сегменты. Цикл for используется для итерации объектов, содержащихся в таком сегменте.

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

1
Nikolay Lebedev 26 Фев 2018 в 15:15

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

Что происходит, когда вы делаете put пару key-value в хэш-карту:

  1. Таким образом, для каждого key, который вы передаете HashMap, он вычисляет для него хеш-код.
  2. Так много keys могут попасть под одно и то же hashCode ведро. Теперь HashMap проверит, присутствует ли тот же key или нет в том же сегменте.
  3. В Java 7 HashMap поддерживает все ключи одного и того же блока в списке. Поэтому, прежде чем вставить ключ, он пройдет по списку, чтобы проверить, присутствует ли тот же ключ или нет. Вот почему есть цикл FOR.

Таким образом, в среднем случае его временная сложность: O(1), а в худшем случае его временная сложность равна O(N).

0
vkrishna17 1 Мар 2018 в 17:14

В то время как другие ответы объясняют, что происходит, комментарии OP на эти ответы заставляют меня думать, что требуется другой угол объяснения.

Упрощенный пример

Допустим, вы собираетесь добавить 10 строк в хэш-карту: «A», «B», «C», «Hi», «Bye», «Yo», «Yo-yo», «Z», «1». "," 2 "

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

HashMap волшебным образом не знает, что вы добавите в него 10 строк, а также не знает, какие строки вы добавите в него позже. Он должен предоставить места для размещения всего, что вы могли бы дать ему ... потому что он знает, что вы собираетесь поместить в него 100 000 строк - возможно, каждое слово в словаре.

Предположим, что из-за аргумента конструктора, который вы выбрали при создании new HashMap(n), ваша хэш-карта содержит 20 сегментов . Мы будем называть их bucket[0] через bucket[19].

  1. map.put("A", value); Допустим, что значение хеш-функции для «A» равно 5. Теперь хеш-карта может выполнять bucket[5] = new Entry("A", value);

  2. map.put("B", value); Предположим, что хэш ("B") = 3. Итак, bucket[3] = new Entry("B", value);

  3. map.put("C"), value); - хеш ("C") = 19 - bucket[19] = new Entry("C", value);

  4. map.put("Hi", value); Теперь вот где это становится интересным. Допустим, ваша хеш-функция такова, что hash ("Hi") = 3. Итак, теперь хэш-карта хочет выполнить bucket[3] = new Entry("Hi", value); У нас проблема! bucket[3] - это то, где мы поставить ключ «B», и «Hi» определенно отличается от ключа «B» ... но они имеют одинаковое значение хеша . У нас столкновение !

Из-за этой возможности HashMap фактически не реализован таким образом. В хэш-карте должны быть сегменты, которые могут содержать более 1 записи. ПРИМЕЧАНИЕ. я не сказал более 1 записи с тем же ключом , поскольку у нас этого не может быть , но для этого нужно есть корзины, которые могут содержать более 1 записи разных ключей . Нам нужен контейнер, который может содержать как «B» , так и «Hi».

Так что давайте не будем делать bucket[n] = new Entry(key, value);, но вместо этого давайте bucket будем иметь тип Bucket[] вместо Entry[]. Так что теперь мы делаем bucket[n].add( new Entry(key, value) );

Итак, давайте перейдем к ...

bucket[3].add("B", value);

И

bucket[3].add("Hi", value);

Как видите, теперь у нас есть записи для «B» и «Hi» в одном и том же сегменте . Теперь, когда мы хотим вернуть их обратно, нам нужно перебрать все содержимое корзины, например , с помощью цикла for .

Таким образом, цикл присутствует из-за столкновений . Не коллизии key , а коллизии hash(key) .

Почему мы используем такую сумасшедшую структуру данных?

В этот момент вы можете спросить: «Подождите, ЧТО!?! Почему мы делаем такие странные вещи?» Почему мы используем такую искусственную и запутанную структуру данных ??? » Ответ на этот вопрос будет ...

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

Ваш HashMap может быть слишком маленьким

Поскольку вы говорите, что часто видите, что этот цикл for повторяется с несколькими элементами в вашей отладке, это означает, что ваш HashMap может быть слишком маленьким. Если у вас есть разумное предположение относительно того, сколько вещей вы можете вложить в него, попробуйте установить размер больше этого. Обратите внимание, что в моем примере выше я вставлял 10 строк, но имел хэш-карту с 20 сегментами. С хорошей хэш-функцией это даст очень мало коллизий.

Примечание:

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

2
Loduwijk 26 Фев 2018 в 18:02

Для записи в java-8 это также присутствует (вроде как, поскольку TreeNode также есть):

if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }

В основном (для случая, когда корзина не является Tree), итерируйте всю корзину, пока не найдете запись, которую мы ищем.

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

5
Eugene 26 Фев 2018 в 12:30

Если вы видите внутреннюю работу метода get из HashMap.

public V get(Object key)  {
        if (key == null)
           return getForNullKey();
         int hash = hash(key.hashCode());
         for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) 
         {
             Object k;
             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                 return e.value;
         }
             return null;
}
  • Сначала он получает хеш-код ключевого объекта, который передается, и находит местоположение сегмента.
  • Если правильный контейнер найден, он возвращает значение (e.value)
  • Если совпадений не найдено, возвращается ноль.

Иногда возможны конфликты Hashcode, и для решения этого конфликта Hashmap использует equals (), а затем сохраняет этот элемент в LinkedList в том же сегменте.

Давайте рассмотрим пример: введите описание изображения здесь

Получить данные для ключа vaibahv: map.get (новый ключ ("vaibhav"));

Шаги:

  1. Рассчитайте хеш-код Key {"vaibhav"}. Он будет сгенерирован как 118.

  2. Рассчитайте индекс, используя метод индекса, это будет 6.

  3. Перейти к индексу 6 массива и сравнить ключ первого элемента с заданным ключ . Если оба равны, то вернуть значение, в противном случае проверьте Следующий элемент, если он существует.

  4. В нашем случае он не найден в качестве первого элемента и следующего объекта узла не является нулевым

  5. Если следующий узел имеет значение null, вернуть null.

  6. Если следующий узел не является нулевым, переходите ко второму элементу и повторяйте процесс 3, пока ключ не будет найден или следующий не будет нулевым.

Для этого процесса поиска будет использоваться цикл. Для получения дополнительной ссылки вы можете обратиться этот

18
Prashant Patil 26 Фев 2018 в 13:01