Java 哈希函数 哈希表 动态容量 链地址法 简介+实现

2023-10-27

简介

实现哈希表有两个主要的问题, 一个是解决哈希函数的设计, 一个是哈希冲突的处理

哈希函数

键通过哈希函数可以得到一个索引, 通过索引可以在内存中找到这个键所包含的信息, 索引的分布越均匀冲突才越少

所有类型的数据, 包括浮点型, 字符型的都可以转化为整型, 然后用整型的哈希函数计算

哈希函数的设计要遵循一些原则:

  1. 一致性: 如果 a == b, 则 hash(a) == hash(b)
  2. 高效性: 计算高效简便
  3. 均匀性: 哈希值均匀分布

整型

如果是小范围正整数, 可以直接把键作为索引使用, 比如字母表的大小只有26, 1对应a, 2对应b…

如果是小范围的负整数, 可以加偏移, -10 ~ 10 偏移10的话就转为 0 ~ 20

如果是大整数, 如身份证号, 通常是取模, 比如身份证号码401625198906031289, 如果mod 1000000的话, 也就是取后6位, 会有一个问题, 那就是日期的范围不是00~99, 会导致分布不均匀,
最简单的解决方法是模一个素数, 可以根据你的数据范围, 到 这里 查找合适的素数 (模素数可以更均匀地分布)
在这里插入图片描述

浮点型

把存储浮点数的32位或者64位二进制当作整型处理

字符串型

比如 love = l * 26^3 + o * 26^2 + v * 26^1 + e * 26^0, 当作26进制的数字, 如果字符串不止有小写字母, 还有字符串, 大写字母的话, 也可以修改26, 下面用 B表示, 如果M表示素数的话, love的哈希值是

hash(love)
= (l * B^3 + o * B^2 + v * B^1 + e * B^0)%M
= ((((l * B) + o * B) + v * B) + e * B) % M
= ((((l % M) * B + o) % M * B + v) % M * B + e) % M. . . . . .每次都先模一次M可以防止整型溢出

代码如下

int hash = 0;
for(int i=0; i<s.length(); i++)
	hash = (hash * B + s.charAt(i)) % M

Java 中的hashCode()

将浮点型, 字符串型等非整型的转化为整型

int a = 35;
System.out.println(((Integer)a).hashCode());
运行结果: 35

int a2 = 35;
System.out.println(((Integer)a2).hashCode());
运行结果: 35  // 输入一样, 输出一样

int b = -35;
System.out.println(((Integer)b).hashCode());
运行结果: -35

double c = 3.14159265;
System.out.println(((Double)c).hashCode());
运行结果: 331478282

String d = "To freedom";
System.out.println(d.hashCode());
运行结果: 1240310481

因为Java并不知道我们的数据规模, 所以不知道要模多大的素数, hashCode()得到的不是索引, 得模完素数之后才得到索引

Object类默认都是有 hashCode() 这个方法的, 所有类都是Object类的子类, 这也是为什么上面的int, double要转化为Integer类, Double类.

如果是自己定义的类, 通常是要 重写hashCode() 的, 因为没有重写hashCode()的话, 那么用的就是Object类中的hashCode(), 这个方法把对象的地址映射成整型, 所以只要地址不同, hashCode()返回的值就会不同, 这通常都不是我们想要的
除此之外, 通常还要 重写equal() , 用于在哈希冲突的时候判断两个对象是不是一样
例子如下

public class Person{
    public String name;
    public int age;

    Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode(){
        int B = 31;

        int hash = 0;
        hash = hash * B + name.hashCode();
        hash = hash * B + age;

        return hash;
    }
	
	@Override
	public boolean equals(Object o){
		if(this == o)
			return true;

		if(o == null)
			return false;

		if(getClass() != o.getClass())
			return false;

		Person another = (Person)o;  // 强制类型转化
		return this.name == another.name && this.age == another.age			
	}
}

哈希冲突

处理方法: 链地址法(Separate Chaining), 开发地址法, 再哈希法, Coalesced Hashing(综合 Separate Chaining 和 开发地址法)

这里只介绍链地址法, 有需要再填坑
有哈希冲突时候, 可以用链表把不同的键值挂在同一索引上, 也可以用树
在这里插入图片描述
在Java8之前, 每一个索引位置对应一个链表
在Java8之后, 一开始每一个索引位置依然对应一个链表, 但是当哈希冲突达到一定程度后(比如每一个位置的存储的元素数超过一定数量), Java就会把链表转为红黑树, 也就是TreeMap(底层实现就是红黑树), 因为冲突小的时候, 链表更快, 但是! 转为红黑树有一个条件, 就是哈希表的键要可比较, 因为红黑树是有序的, 可比较才可以排序

时间复杂度

N个元素放入有M个地址的哈希表, 平均每个地址存N/M个元素
如果用的是链表存储, 时间复杂度是 O(N/M)
如果用的是平衡树, 时间复杂度是 O(log(N/M)

动态空间处理

当N无线增大时, 时间复杂度就会很大
所以要达到 O(1), 就要使用动态的哈希表, M要随着N的增大而增大

当每个地址承载的元素多到一定程度时, 扩容: N/M >= upperTol (上界)

if(size >= upperTol * M)  // 用除法会有误差, size是存储的元素个数, M是哈希表容量
    resize(2 * M);  // 扩大为原来的两倍

当每个地址承载的元素少到一定程度时, 缩容: N/M < lowerTol (下界)

if(size < lowerTol * M && M / 2 >= initCapacity)  // 要防止缩太小, 不能小于初始容量
    resize(M / 2);  // 变为原来的一半

设置了两个界限, 可以避免震荡, 如果只有一个界限, 在边界反复添加删除就会导致M反复变化

扩容的时候是扩大两倍, 有一点不好, 就是素数乘2之后会变成偶数, 模一个偶数会容易导致分布不均匀, 所以做一些改进, 那就是使用上面的素数表

上面两个条件判断的修改如下

// 上面那个素数表
private final int[] capacity = {53, 97, 193, 389, 769, 1543, 3079, 6151, 
  12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 
  6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 
  805306457, 1610612741};  // 最大不能超过int的表示范围
private int capacityIndex = 0;  // 初始容量是53

扩容:
if(size >= upperTol * M && capcityIndex+1 < capacity.length){  // 用除法会有误差
    capcityIndex++;
    resize(capacity[capcityIndex]);
}

缩容:
if(size < lowerTol * M && capacityIndex-1 >= 0) {  // 要防止缩太小
    capacityIndex--;
    resize(capacity[capacityIndex]);
}

哈希函数和扩容缩容函数如下

// 哈希函数
private int hash(K key){
    return (key.hashCode() & 0x7fffffff) % M;  // 去掉符号, 再转为索引
}

private void resize(int newM) {
    TreeMap<K, V>[] newHashTable = new TreeMap[newM];
    // 初始化newM个索引, 每个索引是一个TreeMap
    for(int i=0; i<newM; i++)
        newHashTable[i] = new TreeMap<>();

    int oldM = M;  // 保存旧的M
    this.M = newM;  // 更新M, 因为hash()要用到M
    for(int i=0; i<oldM; i++){  // 遍历旧的所有索引
        TreeMap<K, V> map = hashtable[i];
        for(K key: map.keySet()){  // 遍历索引上的TreeMap的每个键值
            newHashTable[hash(key)].put(key, map.get(key));
        }
    }

    this.hashtable = newHashTable;
}

适用范围

Java标准库中
有序集合, 有序映射用的是平衡树
无序集合, 无序映射用的是哈希表

实现

总的代码如下
HashTable.java

import java.util.TreeMap;

public class HashTable<K, V> {

    private final int[] capacity
            = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
            12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};  // 最大不能超过int的表示范围

    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private int capacityIndex = 0;  // 初始容量是53

    private TreeMap<K, V>[] hashtable;
    private int M;  // hashtable的长度
    private int size;  // 存储的元素个数

    public HashTable(){
        this.M = capacity[capacityIndex];
        size = 0;
        hashtable = new TreeMap[M];
        for(int i=0; i<M; i++)
            hashtable[i] = new TreeMap<>();  // 每个索引位置都连一个TreeMap
    }

    // 哈希函数
    private int hash(K key){
        return (key.hashCode() & 0x7fffffff) % M;  // 去掉符号, 再转为索引
    }

    public int getSize(){
        return size;
    }

    // 添加元素
    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)]; // 找到索引位置
        // 如果已经存在, 就更新
        if(map.containsKey(key)){  // 看那个索引位置连的树中有没有我们要的键
            map.put(key, value);
        }
        else{
            map.put(key, value);
            size++;

            if(size >= upperTol * M && capacityIndex+1 < capacity.length){  // 用除法会有误差
                capacityIndex++;
                resize(capacity[capacityIndex]);
            }
        }
    }

    // 删除元素
    public V remove(K key){
        TreeMap<K, V> map = hashtable[hash(key)]; // 找到索引位置
        V ret = null;
        if(map.containsKey(key)){
            ret = map.remove(key);
            size--;

            if(size < lowerTol * M && capacityIndex-1 >= 0) {  // 要防止缩太小
                capacityIndex--;
                resize(capacity[capacityIndex]);
            }
        }
        return ret;
    }

    private void resize(int newM) {
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        // 初始化newM个索引, 每个索引是一个TreeMap
        for(int i=0; i<newM; i++)
            newHashTable[i] = new TreeMap<>();

        int oldM = M;
        this.M = newM;
        for(int i=0; i<oldM; i++){
            TreeMap<K, V> map = hashtable[i];
            for(K key: map.keySet()){
                newHashTable[hash(key)].put(key, map.get(key));
            }
        }

        this.hashtable = newHashTable;
    }

    // 修改
    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];V ret = null;
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + "does not exist!");

        map.put(key, value);
    }

    // key是否存在
    public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }

    // 通过key获取value
    public V get(K key){
        return hashtable[hash(key)].get(key);
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 哈希函数 哈希表 动态容量 链地址法 简介+实现 的相关文章

  • Maven 2:如何将当前项目版本打包在WAR文件中?

    我正在使用 Maven 2 构建我的 Java 项目 并且正在寻找一种向用户呈现 pom xml 当前版本号的方法 例如使用 Servlet 或 JSP 据我所知 最好的方法是 Maven 将版本号作为文本文件打包到 WAR 中 这使我能够
  • Java - 如何将特殊字符放入字符串中

    Java 似乎有很好的字符串处理能力 尽管如此 我还是遇到了最简单的问题 我需要动态字符串 它们在运行时更改 因此字符串类型不是一个好的选择 因为它们是不可变的 所以我使用字符数组 设置起来有点痛苦 但至少它们是可以修改的 我想创建一个字符
  • JavaFX 图像未在舞台中显示

    我尝试了很多次 尝试了很多方法 但都无法让自己的形象在舞台上如我所愿 我认为这可能与java寻找资源的路径有关 但我不确定 因为我刚刚开始使用视觉库 在本例中为JavaFX 这是我的目录结构 MyProject assets img myI
  • 防止 Spring Boot 注册 Spring Security 过滤器之一

    我想禁用安全链中的 Spring Security 过滤器之一 我已经看到了防止 Spring Boot 注册 servlet 过滤器 https stackoverflow com questions 28421966 prevent s
  • URL.setURLStreamHandlerFactory

    我正在使用带有嵌入式 Jetty 的可执行 jar 开发一个 Web 应用程序 我的jar包含一个依赖jar jar in jar 我参考了JarRsrcLoader and RsrcURLStreamHandlerFactory由 Ecl
  • 无法使用 json 架构验证器根据预定义的 yaml 文件验证查询参数

    我需要根据预定义的 yaml 文件架构验证查询参数的架构 因此我使用 json 架构验证器 验证如何失败 我正在执行以下步骤 填充参数和相应的架构 final List
  • 使用 ChannelExec 的命令未执行 - Jsch

    我正在使用 Jsch 在服务器中创建一个文件并执行一些命令 对于文件创建 它工作正常 但是对于命令执行 则不然 它保持状态 1 仍在处理它 并永远保持该状态 这种情况发生在 shell 执行或我尝试成为 root 时 请按照以下方法操作 p
  • 如何比较 Struts 2 中 url 请求参数中的单个字符

    我正在读取具有单个字符的 url 参数 它将是Y or N 我必须写一个条件来检查它是否Y or N并做相应的事情 这是我写的 但似乎不起作用 总是转到其他地方 网址是
  • Java 正则表达式 - 字母数字,最多一个连字符,句点或下划线,七个字符长

    我是 Java 正则表达式工具的新手 尽管它们潜力巨大 但我很难完成这项任务 我想编写一个正则表达式来验证遵循以下语法的输入字符串 小写字母和数字的任意组合 仅一个下划线 一个破折号或一个句号 无其他特殊字符 最小长度为 5 我想出了以下解
  • RMI 中的引用传递问题? [复制]

    这个问题在这里已经有答案了 有人可以告诉我我错在哪里 为什么这个 RMI 聊天应用程序不起作用 目标是通过远程对象或序列化对象实现客户端 服务器和逻辑之间的解耦 import javax swing import java awt even
  • 字符串池可以包含两个具有相同值的字符串吗? [复制]

    这个问题在这里已经有答案了 字符串池可以包含两个具有相同值的字符串吗 String str abc String str1 new String abc Will the second statement with new operator
  • 定期更新 SWT 会导致 GUI 冻结

    Problem 当 GUI 字段定期更新时 SWT 会冻结 我想要一个基于 SWT 的 GUI 其中文本字段的值会定期递增 最初我从单独的线程访问 textField 导致抛出异常 线程 Thread 0 org eclipse swt S
  • 如何导入 org.apache.commons.lang3.ArrayUtils;进入 Eclipse [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我如何导入 org apache commons lang3 ArrayUtils 将库添加到 Ecl
  • 容器中的 JVM 计算处理器错误?

    最近我又做了一些研究 偶然发现了这一点 在向 OpenJDK 团队抱怨之前 我想看看是否有其他人观察到这一点 或者不同意我的结论 因此 众所周知 JVM 长期以来忽略了应用于 cgroup 的内存限制 众所周知 现在从 Java 8 更新某
  • 如何将 Observable>> 转换为 Observable>

    我陷入了如何将以下可观察类型转换 转换为我的目标类型的困境 我有以下类型的可观察值 Observable
  • 当您在数组列表上调用remove(object o)时,它如何比较对象?

    当您在 java 中的数组列表上调用remove object o 时 它如何比较对象以找到要删除的正确对象 它使用指针吗 或者它使用 Comparable 接口来比较对象吗 ArrayList remove 依赖于对象的实现Equal方法
  • 如何找到被点击的JLabel并从中显示ImageIcon?

    这是我的代码 我想知道哪个l单击 然后在新框架中显示该 ImageIcon e getSource 不起作用 final JFrame shirts new JFrame T shirts JPanel panel new JPanel n
  • 使用 JAD 反编译 java - 限制

    我正在尝试使用 Java 中的 JAD 反编译几个 jar 文件 我也尝试过 JD GUI 但运气更差 但出现了很多错误 一种类型 易于修复 似乎是内部类 但我也发现了这段代码 static int SWITCH TABLE atp com
  • 春季 CORS。在允许的来源中添加模式

    查看CORS的弹簧指南 以下代码启用所有允许的来源 public class MyWebMVCConfigurer extends WebMvcConfigurerAdapter Override public void addCorsMa
  • 防止Java实例化的正确方法[关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi

随机推荐