← Назад к вопросам

Какие знаешь способы разрешения хеш коллизий?

1.7 Middle🔥 91 комментариев
#JVM и память#Коллекции и структуры данных

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы разрешения хеш-коллизий в контексте Android/Kotlin

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

1. Метод цепочек (Separate Chaining)

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

// Упрощённая реализация метода цепочек
class ChainingHashMap<K, V> {
    private val table: Array<LinkedList<Entry<K, V>>>
    
    fun put(key: K, value: V) {
        val index = key.hashCode() % table.size
        val bucket = table[index]
        
        // Поиск существующего ключа в цепочке
        for (entry in bucket) {
            if (entry.key == key) {
                entry.value = value
                return
            }
        }
        bucket.add(Entry(key, value))
    }
}

Преимущества:

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

Недостатки:

  • Дополнительная память на хранение ссылок в списках
  • При плохой хеш-функции деградация до O(n)

2. Открытая адресация (Open Addressing)

Все элементы хранятся непосредственно в массиве таблицы. При коллизии выполняется поиск следующей свободной ячейки по определённому алгоритму.

Основные стратегии открытой адресации:

  • Линейное пробирование - последовательная проверка ячеек:

    index = (hash(key) + i) % table.size // где i = 0, 1, 2...
    
  • Квадратичное пробирование - проверка с квадратичным шагом:

    index = (hash(key) + c1*i + c2*i*i) % table.size
    
  • Двойное хеширование - использование второй хеш-функции:

    index = (hash1(key) + i * hash2(key)) % table.size
    

Преимущества:

  • Лучшая локальность ссылок (все данные в одном массиве)
  • Нет дополнительных выделений памяти для связных списков
  • Предсказуемое время доступа при хорошей хеш-функции

Недостатки:

  • Максимальный размер таблицы ограничен
  • Сложнее удаление элементов (требуются специальные маркеры)
  • Сильная деградация при высокой заполненности

3. Robin Hood Hashing (Усовершенствованная открытая адресация)

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

// Концептуальный пример поиска места для вставки
fun findSlot(key: K): Int {
    var index = hash(key) % table.size
    var distance = 0
    
    while (table[index] != null) {
        val existingDistance = calculateDistance(table[index].key)
        if (distance > existingDistance) {
            // Нашли "более бедный" элемент - занимаем его место
            return index
        }
        index = (index + 1) % table.size
        distance++
    }
    return index
}

Преимущество: Более равномерное распределение, снижающее максимальное время поиска.

4. Cuckoo Hashing (Кукушечное хеширование)

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

class CuckooHashMap<K, V> {
    private val tables: Array<Array<Entry<K, V>?>>
    private val hashFunctions: Array<(K) -> Int>
    
    fun put(key: K, value: V) {
        var currentKey = key
        var currentValue = value
        
        for (i in 0..MAX_DISPLACEMENTS) {
            // Пробуем вставить в первую таблицу
            val hash1 = hashFunctions[0](currentKey) % tables[0].size
            if (tables[0][hash1] == null) {
                tables[0][hash1] = Entry(currentKey, currentValue)
                return
            }
            
            // Вытесняем существующий элемент и пытаемся его переразместить
            val displaced = tables[0][hash1]!!
            tables[0][hash1] = Entry(currentKey, currentValue)
            currentKey = displaced.key
            currentValue = displaced.value
            
            // Аналогично для второй таблицы...
        }
        // Если слишком много вытеснений - перехеширование
        rehash()
    }
}

Преимущества: Гарантированное O(1) время поиска в худшем случае (для двух таблиц). Недостатки: Сложная реализация вставки, возможны циклические вытеснения.

В контексте Android-разработки

  1. Стандартные коллекции Kotlin/Java используют метод цепочек с оптимизациями:

    • В Java 8+ при длине цепочки > 8 элементы преобразуются в сбалансированное дерево
    • Это предотвращает деградацию до O(n) при DoS-атаках с коллизиями
  2. SparseArray и ArrayMap в Android SDK используют открытую адресацию для примитивных ключей, что даёт преимущество в памяти и производительности для специфичных случаев.

  3. Выбор метода зависит от:

    • Ожидаемого размера данных
    • Требований к памяти
    • Критичности времени доступа
    • Качества доступных хеш-функций

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

Какие знаешь способы разрешения хеш коллизий? | PrepBro