哈希表

哈希表(Hashtable),也称为散列表,是一种常见的数据结构,用于实现关联数组。它通过将关键字(key)映射到哈希表中的一个位置来实现快速的数据查找、插入和删除操作。哈希表通常由一个数组和一组哈希函数组成。

在哈希表中,每个关键字都经过哈希函数处理后得到一个索引值,该索引值指示了关键字在数组中的位置。理想情况下,哈希函数能够将关键字均匀地分布在数组中的不同位置,从而实现较好的性能。然而,在实际情况中,由于哈希函数的限制和数据分布的不均匀性,可能会出现哈希冲突,即多个关键字映射到了同一个位置。解决哈希冲突的方法包括链地址法(Chaining)、开放寻址法(Open Addressing)等。

注意:

要求:数组长度是 2 的 n 次方
hash % 数组长度 等价于 hash & (数组长度-1)
推荐使用求模运算替换为位运算,性能更好

1.基本框架

哈希表(HashTable)类,内部类 Entry 作为节点类。

package com.dreams.hashtable;

import com.google.common.hash.Hashing;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;


public class HashTable {

    // 节点类
    static class Entry {
        int hash; // 哈希码
        Object key; // 键
        Object value; // 值
        Entry next;

        public Entry(int hash, Object key, Object value) {
            this.hash = hash;
            this.key = key;
            this.value = value;
        }
    }
}

 

哈希表(HashTable)类也有自己的属性。

package com.dreams.hashtable;

import com.google.common.hash.Hashing;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class HashTable {

    // 摘要算法
    // 散列算法

    // 节点类
    static class Entry {
        //......
    }

    Entry[] table = new Entry[16];
    int size = 0; // 元素个数
    //哈希表的负载因子
    float loadFactor = 0.75f; // 12 是16阈值
    //保存阈值,不必重复计算
    int threshold = (int) (loadFactor * table.length);

}

 

2.get方法

通过哈希码确定键值对的存储位置,然后在对应位置的链表中查找目标键。

// 根据 hash 码获取 value
Object get(int hash, Object key) {
    int idx = hash & (table.length - 1);
    if (table[idx] == null) {
        return null;
    }
    Entry p = table[idx];
    while (p != null) {
        if (p.key.equals(key)) {
            return p.value;
        }
        p = p.next;
    }
    return null;
}

 

3.put方法

代码:

// 向 hash 表存入新 key value,如果 key 重复,则更新 value
void put(int hash, Object key, Object value) {
    int idx = hash & (table.length - 1);
    if (table[idx] == null) {
        // 1. idx 处有空位, 直接新增
        table[idx] = new Entry(hash, key, value);
    } else {
        // 2. idx 处无空位, 沿链表查找 有重复key更新,否则新增
        Entry p = table[idx];
        while (true) {
            if (p.key.equals(key)) {
                p.value = value; // 更新
                return;
            }
            if (p.next == null) {
                break;
            }
            p = p.next;
        }
        p.next = new Entry(hash, key, value); // 新增
    }
    size++;
    if (size > threshold) {
        resize();
    }
}

代码解释:

1.根据哈希码计算出在哈希表数组中的索引 idx。
2.然后,检查哈希表数组中该索引处是否有节点。如果没有,说明该位置没有键值对,直接在该位置新增一个节点存储新的键值对。
3.如果该索引处有节点,则遍历该链表,逐个比较节点的键与目标键是否相等:

  • 如果找到了相同的键,则更新对应节点的值为新值,并返回。
  • 如果遍历完链表仍未找到相同的键,则在链表末尾新增一个节点存储新的键值对。

4.最后,更新哈希表的大小 size,并检查是否需要进行扩容操作。

 

 

4.扩容

哈希表的负载因子(Load Factor)是一个重要的衡量指标,它定义为哈希表中已存储的元素数量与哈希表大小的比值。当负载因子超过一定阈值时,就需要进行负载均衡操作。

n 是哈希表中已存储的元素数量。

m 是哈希表数组的长度。

负载因子超过一定阈值(通常为 0.75)时,表示哈希表已经达到了扩容的条件,需要进行负载均衡操作。

private void resize() {
    //乘2相当于左乘一位
    Entry[] newTable = new Entry[table.length << 1];
    for (int i = 0; i < table.length; i++) {
        Entry p = table[i]; // 拿到每个链表头
        if (p != null) {
            Entry a = null;
            Entry b = null;
            Entry aHead = null;
            Entry bHead = null;
            while (p != null) {
                if ((p.hash & table.length) == 0) {
                    if (a != null) {
                        a.next = p;
                    } else {
                        aHead = p;
                    }
                    a = p; // 分配到a
                } else {
                    if (b != null) {
                        b.next = p;
                    } else {
                        bHead = p;
                    }
                    b = p; // 分配到b
                }
                p = p.next;
            }
            // 规律: a 链表保持索引位置不变,b 链表索引位置+table.length
            if (a != null) {
                a.next = null;
                newTable[i] = aHead;
            }
            if (b != null) {
                b.next = null;
                newTable[i + table.length] = bHead;
            }
        }
    }
    table = newTable;
    threshold = (int) (loadFactor * table.length);
}

代码解释:

1.首先,创建一个新的数组 newTable,其大小是原数组的两倍(table.length << 1)。

2.然后,遍历原数组 table 中的每个元素(每个元素是一个链表的头节点)。

3.对于每个链表头节点 p,根据其哈希值重新计算其在新数组中的位置,具体分为两组:

  • 如果 (p.hash & table.length) == 0,说明 p 应该留在原来的位置不变(记为组 a)。
  • 如果 (p.hash & table.length) != 0,说明 p 的位置应该在原来的基础上加上原数组的长度,即移到新数组的后半部分(记为组 b)。

4.将每个链表按照上述规则分别连接到新数组的对应位置上,并更新新数组中的链表头节点。

5.最后,更新哈希表的数组为新数组,并重新计算扩容后的阈值。

 

 

5.删除方法

代码

// 根据 hash 码删除,返回删除的 value
Object remove(int hash, Object key) {
    int idx = hash & (table.length - 1);
    if (table[idx] == null) {
        return null;
    }
    Entry p = table[idx];
    Entry prev = null;
    while (p != null) {
        if (p.key.equals(key)) {
            // 找到了, 删除
            if (prev == null) { // 链表头
                table[idx] = p.next;
            } else { // 非链表头
                prev.next = p.next;
            }
            size--;
            return p.value;
        }
        prev = p;
        p = p.next;
    }
    return null;
}

代码解释:

1.首先,根据哈希码计算出在哈希表数组中的索引 idx。
2.然后,检查哈希表数组中该索引处是否有节点。如果没有,说明该位置没有键值对,直接返回 null。
3.如果该索引处有节点,则遍历该链表,逐个比较节点的键与目标键是否相等:

  • 如果找到了相同的键,则删除对应节点,并返回被删除的值。
  • 如果未找到相同的键,则继续遍历链表。

4.如果遍历完链表仍未找到相同的键,则返回 null。

 

6.哈希算法

MurmurHash是一种广泛使用的非加密哈希函数。

加入依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>

代码:

public Object get(Object key) {
    int hash = hash(key);
    return get(hash, key);
}

public void put(Object key, Object value) {
    int hash = hash(key);
    put(hash, key, value);
}

public Object remove(Object key) {
    int hash = hash(key);
    return remove(hash, key);
}

private static int hash(Object key) {
    if (key instanceof String) {
        String k = (String) key;
        return Hashing.murmur3_32().hashString(k, StandardCharsets.UTF_8).asInt();
    }
    int hash = key.hashCode();
    return hash ^ (hash >>> 16);
}

 

参考

黑马数据结构

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇