Почему метод get HashMap имеет цикл FOR?

Я рассматриваю исходный код для 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;
    }

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

Ответы

Ответ 1

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

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

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

Ответ 2

Если вы видите внутреннюю работу метода 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)
  • Если совпадение не найдено, оно возвращает null.

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

Возьмем пример: enter image description here

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

шаги:

  1. Вычислить хэш-код ключа {"vaibhav"}. Он будет сгенерирован как 118.

  2. Вычислить индекс с помощью индексного метода будет 6.

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

  4. В нашем случае он не найден как первый элемент, а следующий узел - не нуль.

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

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

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

Ответ 3

Для записи в 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), повторите весь контейнер до тех пор, пока не найдете нужную нам запись.

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

Ответ 4

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

Концепции

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

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

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

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

пример

Ниже приведен простой пример, который я продемонстрировал для вас:

public class Person {
    private int id;

    Person(int _id){
        id = _id;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

Класс испытания:

import java.util.Map;

public class HashMapHashingTest {
    public static void main(String[] args) {
        Person p1 = new Person(129);
        Person p2 = new Person(133);

        Map<Person, String> hashMap = new MyHashMap<>(2);
        hashMap.put(p1, "p1");
        hashMap.put(p2, "p2");
        System.out.println(hashMap);
    }
}

Отладочный снимок экрана (пожалуйста, нажмите и увеличьте, потому что он выглядит небольшим):

enter image description here

Обратите внимание, что в приведенном выше примере оба объекта Person дают другое значение хэша (соответственно 136 и 140), но дает тот же самый индекс 0, поэтому оба объекта идут в одном и том же ведре. На скриншоте вы можете видеть, что оба объекта находятся в индексе 0 и там у вас есть и next который в основном указывает на второй объект.


Обновление: Еще один простой способ увидеть, что более одного ключа входит в одно и то же ведро, - это создать класс и переопределить метод hashCode чтобы всегда возвращать одно и то же значение int, теперь произойдет то, что все объекты этого класса будут давать то же самое положение index/bucket, но поскольку вы не переопределили метод equals чтобы они не считались одинаковыми и, следовательно, сформировали бы список в этом месте индекс/ведро.

Еще один поворот в этом случае предполагает, что вы также переопределите метод equals и сравните все объекты, равные, тогда только один объект будет присутствовать в местоположении index/bucket, потому что все объекты равны.

Ответ 5

В то время как другие ответы объясняют, что происходит, ответы 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); Скажем, что хэш-значение для "А" равно 5. Теперь хэш-карта может делать bucket[5] = new Entry("A", value);

  2. map.put("B", value); Предположим, что hash ("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 ввода разных ключей. Нам нужен ведро, которое может содержать как "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-loop повторяется с несколькими элементами в вашей отладке, это означает, что ваш HashMap может быть слишком маленьким. Если у вас есть разумная догадка о том, сколько вещей вы можете вложить в нее, попробуйте установить размер, который будет больше этого. Обратите внимание, в моем примере выше, что я вставлял 10 строк, но имел хэш-карту с 20 ведрами. При хорошей хэш-функции это приведет к очень небольшим столкновениям.

Замечания:

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

Ответ 6

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

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

Ответ 7

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

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

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

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