详细版:哈希表(Hash Table)哈希冲突及其解决方法

哈希冲突的来源

哈希冲突是指不同的键(Key)经过哈希函数计算后,映射到相同的索引(Index)位置。由于哈希表的大小是有限的,而键的集合通常是无限的,因此冲突是不可避免的。例如,如果哈希表的大小为10,而两个不同的键 key1 和 key2 经过哈希函数计算后都得到相同的哈希值 hash(key1) = hash(key2) = 3,那么它们都会被存储在索引为3的位置。

解决哈希冲突的方法

下面详细介绍两种主要的哈希冲突解决方法:链地址法(Chaining)和开放地址法(Open Addressing),并通过图表进行更直观的说明。

链地址法(Chaining)

在链地址法中,哈希表的每个索引位置(也称为“桶”或“槽”)存储一个链表(或其他数据结构,如数组、树),所有映射到该位置的键值对都被存储在这个链表中。这种方法简单直观,适合冲突较多的情况。

链地址法的示意图

+-----------------+
|      索引0      |
|    (key1, v1)   |
+-----------------+
|      索引1      |
|    (key2, v2)   |
+-----------------+
|      索引2      |
|    (key3, v3) ->|
|    (key4, v4)   |
+-----------------+
|      索引3      |
|    (key5, v5)   |
+-----------------+

链地址法的优点

  1. 实现简单:直接使用链表即可。
  2. 内存利用率高:只有在冲突时才需要额外的存储空间。
  3. 动态大小:链表的长度可以动态调整,不会因为冲突而影响性能。

链地址法的缺点

  1. 链表遍历开销:在最坏情况下(所有元素都映射到同一个索引),查找操作的时间复杂度会退化为 O(n)。
  2. 空间复杂度:每个链表节点需要额外的指针空间。

链地址法的实现示例(Java)

class HashNode<K, V> {
    K key;  // 键
    V value;  // 值
    HashNode<K, V> next;  // 指向下一个节点的指针

    // 构造函数
    public HashNode(K key, V value) {
        this.key = key;
        this.value = value;
        this.next = null;  // 初始时,下一个节点为空
    }
}

class HashTable<K, V> {
    private int size;  // 当前元素数量
    private int capacity;  // 哈希表容量
    private HashNode<K, V>[] buckets;  // 存储桶的数组

    // 构造函数,初始化哈希表
    public HashTable(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        this.buckets = new HashNode[capacity];  // 初始化桶数组
    }

    // 获取键的哈希值并计算对应的桶索引
    private int getBucketIndex(K key) {
        int hashCode = key.hashCode();  // 获取键的哈希值
        return Math.abs(hashCode) % capacity;  // 取绝对值并取模,得到桶索引
    }

    // 插入键值对
    public void insert(K key, V value) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        HashNode<K, V> head = buckets[index];  // 获取该桶的头节点
        while (head != null) {
            if (head.key.equals(key)) {
                head.value = value;  // 如果键已存在,更新值
                return;
            }
            head = head.next;  // 遍历链表
        }
        size++;  // 增加元素数量
        head = buckets[index];  // 重新获取头节点
        HashNode<K, V> newNode = new HashNode<>(key, value);  // 创建新节点
        newNode.next = head;  // 将新节点插入链表头部
        buckets[index] = newNode;  // 更新桶的头节点
    }

    // 获取键对应的值
    public V get(K key) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        HashNode<K, V> head = buckets[index];  // 获取该桶的头节点
        while (head != null) {
            if (head.key.equals(key)) {
                return head.value;  // 找到键,返回对应的值
            }
            head = head.next;  // 遍历链表
        }
        return null;  // 未找到键,返回null
    }

    // 删除键值对
    public void remove(K key) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        HashNode<K, V> head = buckets[index];  // 获取该桶的头节点
        HashNode<K, V> prev = null;  // 初始化前一个节点
        while (head != null) {
            if (head.key.equals(key)) {
                break;  // 找到键,跳出循环
            }
            prev = head;  // 更新前一个节点
            head = head.next;  // 遍历链表
        }
        if (head == null) return;  // 未找到键,直接返回
        size--;  // 减少元素数量
        if (prev != null) {
            prev.next = head.next;  // 前一个节点的指针指向后一个节点
        } else {
            buckets[index] = head.next;  // 如果删除的是头节点,直接更新桶的头节点
        }
    }
}
开放地址法(Open Addressing)

在开放地址法中,所有元素都存储在哈希表的数组中,当发生冲突时,通过某种探测策略(如线性探测、二次探测、双重散列等)寻找下一个可用位置。

开放地址法的示意图

+-----------------+
|      索引0      |
|    (key1, v1)   |
+-----------------+
|      索引1      |
|    (key2, v2)   |
+-----------------+
|      索引2      |
|    (key3, v3)   |
+-----------------+
|      索引3      |
|    (key4, v4)   |
+-----------------+
|      索引4      |
|    (key5, v5)   |
+-----------------+

开放地址法的优点

  1. 内存连续:所有元素存储在连续的内存空间中,访问速度快。
  2. 无额外空间:不需要额外的链表或其他数据结构。

开放地址法的缺点

  1. 探测开销:当冲突较多时,需要多次探测才能找到空闲位置,性能下降。
  2. 删除操作复杂:需要标记删除位置或者移动元素,增加了操作的复杂性。

开放地址法的实现示例(Java)

class HashTable<K, V> {
    private int size;  // 当前元素数量
    private int capacity;  // 哈希表容量
    private K[] keys;  // 存储键的数组
    private V[] values;  // 存储值的数组

    // 构造函数,初始化哈希表
    public HashTable(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        this.keys = (K[]) new Object[capacity];  // 初始化键数组
        this.values = (V[]) new Object[capacity];  // 初始化值数组
    }

    // 获取键的哈希值并计算对应的桶索引
    private int getBucketIndex(K key) {
        int hashCode = key.hashCode();  // 获取键的哈希值
        return Math.abs(hashCode) % capacity;  // 取绝对值并取模,得到桶索引
    }

    // 线性探测函数
    private int linearProbe(int index, int i) {
        return (index + i) % capacity;  // 线性探测,返回下一个索引
    }

    // 插入键值对
    public void insert(K key, V value) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        for (int i = 0; i < capacity; i++) {
            int probeIndex = linearProbe(index, i);  // 获取探测索引
            if (keys[probeIndex] == null || keys[probeIndex].equals(key)) {
                if (keys[probeIndex] == null) {
                    size++;  // 如果该位置为空,增加元素数量
                }
                keys[probeIndex] = key;  // 插入键
                values[probeIndex] = value;  // 插入值
                return;
            }
        }
        throw new RuntimeException("HashTable is full");  // 哈希表已满,抛出异常
    }

    // 获取键对应的值
    public V get(K key) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        for (int i = 0; i < capacity; i++) {
            int probeIndex = linearProbe(index, i);  // 获取探测索引
            if (keys[probeIndex] == null) {
                return null;  // 如果该位置为空,未找到键,返回null
            }
            if (keys[probeIndex].equals(key)) {
                return values[probeIndex];  // 找到键,返回对应的值
            }
        }
        return null;  // 未找到键,返回null
    }

    // 删除键值对
    public void remove(K key) {
        int index = getBucketIndex(key);  // 获取键对应的桶索引
        for (int i = 0; i < capacity; i++) {
            int probeIndex = linearProbe(index, i);  // 获取探测索引
            if (keys[probeIndex] == null) {
                return;  // 如果该位置为空,未找到键,直接返回
            }
            if (keys[probeIndex].equals(key)) {
                keys[probeIndex] = null;  // 删除键
                values[probeIndex] = null;  // 删除值
                size--;  // 减少元素数量
                return;
            }
        }
    }
}
其他解决哈希冲突的方法

除了链地址法和开放地址法,还有其他一些解决哈希冲突的方法,如:

  1. 再哈希(Rehashing):当哈希表负载因子超过一定阈值时,重新调整哈希表的大小并重新计算所有元素的位置。
  2. 布谷鸟哈希(Cuckoo Hashing):使用多个哈希函数,将冲突的元素放置到另一个哈希表中。
总结

哈希冲突是哈希表使用中不可避免的问题,但通过合理的解决方法,可以有效地管理和缓解冲突的影响。链地址法和开放地址法是两种主要的冲突解决策略,各有优缺点,大家需要根据具体应用场景和需求选择合适的方法。

希望你喜欢这篇文章!请点关注和收藏吧。祝关注和收藏的帅哥美女们今年都能暴富。如果有更多问题,欢迎随时提问

猜你喜欢

转载自blog.csdn.net/m0_64974617/article/details/143230366