zoukankan      html  css  js  c++  java
  • 哈希表的实现

    哈希表的实现

    1、TreeMap分析

    • 时间复杂度(平均)
      • 添加、删除、搜索:O(logn)
    • 特点
      • Key必须具备可比较性
      • 元素的分布是有顺序的
    • 在实际应用中,很多时候的需求
      • Map中存储的元素不需要讲究顺序
      • Map中的Key不需要具备可比较性
    • 不考虑顺序、不考虑Key的可比较性,Map有更好的实现方案,平均时间复杂度可以达到O(1)
      • 那就是采取哈希表来实现Map

    2、哈希表(Hash Table)

    • 哈希表也叫作散列表
    • 它是如何实现高效处理数据的?
      • put("Jack",666)
      • put("Rose",777)
      • put("Kate",888)
    • 添加、搜索、删除的流程都是类似的
      • 利用哈希函数生成Key对应的index【O(1)】
      • 根据index操作定位数组元素【O(1)】
    • 哈希表是【空间换时间】的典型应用
    • 哈希函数,也叫做散列函数
    • 哈希表内部的数组元素,很多地方也叫Bucket(桶),整个数组叫Buckets或者Bucket Array
    image-20210418155004226

    3、哈希冲突(Hash Collision)

    • 哈希冲突也叫做哈希碰撞
      • 2个不同的Key,经过哈希函数计算出相同的结果
      • Key1 != key2,hash(Key1) = hash(key2)
    • 解决哈希冲突的常见方式
      • 开放定址法(Open Addressing)
        • 按照一定规则向其他地址探测,知道遇到空桶
      • 再哈希法(Re-Hashing)
        • 设计多个哈希函数
      • 链地址法(Separate Chaining)
        • 比如通过链表将同一index的元素串起来

    Snipaste_2021-04-18_16-01-39

    4、JDK1.8的哈希冲突解决方案

    • 默认使用单向链表将元素串起来
    • 在添加元素时,可能会由单向链表转为红黑树来存储元素
      • 比如当哈希表容量>=64且单向链表的节点数量大于8时
    • 当红黑树节点数量少到一定程度时,又会转为单向链表
    • JDK1.8中的哈希表是使用链表+红黑树解决哈希冲突
    image-20210418160514494

    5、哈希函数

    • 哈希表中哈希函数的实现步骤大概如下

      • 1.先生成Key的哈希值(必须是整数)
      • 2.再让Key的哈希值跟数组的大小进行相关运算,生成一个索引值
      Public int hash(Object key){
      	return hash_code(key) % table.length;
      }
      
    • 为了提高效率,可以使用&位运算取代%运算【前提:将数组的长度设计为2的幂(2^n)】

      Public int hash(Object key){
      	return hash_code(key) & (table.length - 1);
      }
      
    • 良好的哈希函数

      • 让哈希表更加均匀分布-->减少哈希冲突次数-->提升哈希表的性能

    6、如何生成Key的哈希值

    • key的常见类型种类可能有
      • 整数、浮点数、字符串、自定义对象
      • 不同种类的Key,哈希值的生成方式不一样,但目标是一致的
        • 尽量让每个key的哈希值是唯一的
        • 尽量让key的所有信息参与运算
    • 在Java中,HashMap的key必须实现hashCode、equals方法,也允许key为null
    • 整数
      • 整数值当做哈希值
      • 比如10的哈希值就是10
    • 浮点数
      • 将存储的二进制格式转为整数值
    public static int hashCode(int value){
    	return value;
    }
    
    public static int hashCode(float value){
    	return floatToIntBits(value);
    }
    

    7、Long和Double的哈希值

    public static int hashCode(long value){
    	return (int)(value ^ (value >>> 32));
    }
    
    public static int hashCode(double value){
    	long bits = doubleToLongBits(value);
    	return (int)(bits ^ (bits >>> 32));
    }
    

    >>>和^的作用是?

    • 高32bit和低32bit混合计算出32bit的哈希值
    • 充分利用所有信息计算出哈希值

    image-20210418162539920

    8、字符串的哈希表

    • 整数5489是如何计算出来的
      • 5 ∗ 103 + 4 ∗ 102 + 8 ∗ 101 + 9 ∗ 100
    • 字符串是由若干个字符组成的
      • 比如字符串 jack,由 j、a、c、k 四个字符组成(字符的本质就是一个整数)
      • 因此,jack 的哈希值可以表示为 j ∗ n 3 + a ∗ n 2 + c ∗ n 1 + k ∗ n 0,等价于 [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k
      • 在JDK中,乘数 n 为 31,为什么使用 31?
      • 31 是一个奇素数,JVM会将 31 * i 优化成 (i << 5) – i
    String string = "jack";
    int hashCode = 0;
    int len = string.length;
    for(int i = 0; i < len; i++){
    	char c = string.charAt(i);
    	hashCode = 31 * hashCode + c;
    }
    
    String string = "jack";
    int hashCode = 0;
    int len = string.length;
    for(int i = 0; i < len; i++){
    	char c = string.charAt(i);
    	hashCode = (hashCode << 5) - hasCode + c;
    }
    

    9、关于31的探讨

    • 31 * i = (2^5 – 1) * i = i * 2^5 – i = (i << 5) – i
    • 31不仅仅是符合2^n – 1,它是个奇素数(既是奇数,又是素数,也就是质数)
      • 素数和其他数相乘的结果比其他方式更容易产成唯一性,减少哈希冲突
      • 最终选择31是经过观测分布结果后的选择

    10、自定义对象的哈希值

    public class Person implements Comparable<Person> {
    	private int age;   // 10  20
    	private float height; // 1.55 1.67
    	private String name; // "jack" "rose"
    	
    	public Person(int age, float height, String name) {
    		this.age = age;
    		this.height = height;
    		this.name = name;
    	}
    	
    	@Override
    	/**
    	 * 用来比较2个对象是否相等
    	 */
    	public boolean equals(Object obj) {
    		// 内存地址
    		if (this == obj) return true;
    		if (obj == null || obj.getClass() != getClass()) return false;
    		// if (obj == null || !(obj instanceof Person)) return false;
    		
    		// 比较成员变量
    		Person person = (Person) obj;
    		return person.age == age
    				&& person.height == height
    				&& (person.name == null ? name == null : person.name.equals(name));
    	}
    	
    	@Override
    	public int hashCode() {
    		int hashCode = Integer.hashCode(age);
    		hashCode = hashCode * 31 + Float.hashCode(height);
    		hashCode = hashCode * 31 + (name != null ? name.hashCode() : 0);
    		return hashCode;
    	}
    
    	@Override
    	public int compareTo(Person o) {
    		return age - o.age;
    	}
    }
    

    11、自定义对象作为Key

    • 自定义对象作为 key,最好同时重写 hashCode 、equals 方法
      • equals :用以判断 2 个 key 是否为同一个 key
        • 自反性:对于任何非 null 的 x,x.equals(x)必须返回true
        • 对称性:对于任何非 null 的 x、y,如果 y.equals(x) 返回 true,x.equals(y) 必须返回 true
        • 传递性:对于任何非 null 的 x、y、z,如果 x.equals(y)、y.equals(z) 返回 true,那么x.equals(z) 必须 返回 true
        • 一致性:对于任何非 null 的 x、y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true,或者一致地返回 false
        • 对于任何非 null 的 x,x.equals(null) 必须返回 false
      • hashCode :必须保证 equals 为 true 的 2 个 key 的哈希值一样
      • 反过来 hashCode 相等的 key,不一定 equals 为 true
    • 不重写 hashCode 方法只重写 equals 会有什么后果?
      • 可能会导致 2 个 equals 为 true 的 key 同时存在哈希表中

    12、哈希值的进一步处理”扰动计算

    private int hash(K key){
    	if(key == null){
    		return 0;
    	}
    	int h = key.hashCode();
    	return (h ^ (h >>> 16)) & (table.length - 1);
    }
    

    13、装填因子

    • 装填因子(Load Factor):节点总数量 / 哈希表桶数组长度,也叫做负载因子
    • 在JDK1.8的HashMap中,如果装填因子超过0.75,就扩容为原来的2倍

    14、TreeMap vs HashMap

    • 何时选择TreeMap?
      • 元素具备可比较性且要求升序遍历(按照元素从小到大)
    • 何时选择HashMap?
      • 无序遍历

    15、LinkedHashMap

    • 在HashMap的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的
    • 删除度为2的节点node时
      • 需要注意更换 node 与 前驱后继节点 的连接位置
    • 假设添加顺序是
      • 37、21、31、41、97、95、52、42、83
    image-20210418164848346

    LinkedHashMap – 删除注意点

    • 删除度为2的节点node时(比如删除31)
      • 需要注意更换 node 与 前驱后继节点 的连接位置

    image-20210418164932222

    16、LinkedHashMap – 更换节点的连接位置

    image-20210418164955599

    // 交换prev
    LinkedNode<K, V> tmp = node1.prev;
    node1.prev = node2.prev;
    node2.prev = tmp;
    if (node1.prev == null) {
    	first = node1;
    } else {
    	node1.prev.next = node1;
    }
    if (node2.prev == null) {
    	first = node2;
    } else {
    	node2.prev.next = node2;
    }
    
    // 交换next
    tmp = node1.next;
    node1.next = node2.next;
    node2.next = tmp;
    if (node1.next == null) {
    	last = node1;
    } else {
    	node1.next.prev = node1;
    }
    if (node2.next == null) {
    	last = node2;
    } else {
    	node2.next.prev = node2;
    }
    

    17、关于使用%来计算索引

    • 如果使用%来计算索引
      • 建议把哈希表的长度设计为素数(质数)
      • 可以大大减小哈希冲突
    • 右边表格列出了不同数据规模对应的最佳素数,特点如下
      • 每个素数略小于前一个素数的2倍
      • 每个素数尽可能接近2的幂(2 n)
    image-20210418165252235
  • 相关阅读:
    7年.NET面试Java的尴尬历程
    服务挂后Dump日志
    并发中如何保证缓存DB双写一致性(JAVA栗子)
    如何通过Visual Studio来管理我们的数据库项目
    无需Get更多技能,快速打造一个可持久化的任务调度
    Dapper Use For Net
    2014年——新的开始,新的人生
    途牛网站无线架构变迁实践
    windows 下解决 Time_Wait 和 CLOSE_WAIT 方法
    System.Data.DbType 与其它DbType的映射关系
  • 原文地址:https://www.cnblogs.com/coderD/p/14673913.html
Copyright © 2011-2022 走看看