动手实现一个java中的散列表(HashTable)(文末福利)(二)

简介: 动手实现一个java中的散列表(HashTable)(文末福利)(二)

4 手写HashTable底层实现

哈希表存储的是键值对,所以需要两个泛型来分别表示key和value的数据类型,key的数据类型必须实现hashcode()方法,不过由于java中所有的类都继承了Object类,所以默认所有的类都实现了hashcode()方法。


关键在于hashcode()是不是我们想要的。


另外我们也可以考虑哈希表的动态空间处理,当每个位置的平均哈希冲突超过上界时则扩容,小于下界时则缩容。


上面介绍链表法时,整个槽slot可以看做是一个TreeMap数组,数组的每一个位置存储了一个 链表 或者 红黑树,所以构造函数可以实现如下:


public class HashTable<K, V> {
    /**
    *    容忍度上界
    */ 
    private static final int upperTol = 10;
    /**
    *    容忍度下界
    */ 
    private static final int lowerTol = 2;
    /**
    *    初始容量
    */
    private static final int initCapacity = 7;
    private TreeMap<K, V>[] hashtable;
    // 长度(素数)
    private int M;
    // 已经存储了多少元素
    private int size;
    public HashTable(int M){
        this.M = M;
        size = 0;
        hashtable = new TreeMap[M];
        // 对每一个TreeMap进行初始化
        for (int i = 0; i < M; i ++)
            hashtable[i] = new TreeMap<>();
    }
    public HashTable(){
        this(initCapacity);
    }
}


在实现哈希表的增删改查之前,我们需要定义辅助方法hash用于计算要存储的位置,由于数组的索引是从0开始的,所以我们要避免负数的出现:


private int hash(K key){
    return key.hashCode() & 0x7fffffff % M; 
}
public int getSize(){ return size; }


向哈希表中添加键值对:


1.根据key计算要插入的位置(对应一个TreeMap)

2.如果TreeMap中已经存在这个key的话就进行value的更新

3.如果TreeMap中不存在这个key的话则进行数据添加

4.当元素过多时进行扩容操作


public void add(K key, V value){
        // 计算要插入的位置
        TreeMap<K, V> map = hashtable[hash(key)];
        // key已存在
        if (map.containsKey(key))
            map.put(key, value);
        else {
            // key不存在
            map.put(key, value);
            size ++;
            // 满足条件则扩容
            if (size >= upperTol * M)
                resize(2 * M);
        }
    }


删除操作:


1.根据key计算要删除key对应的位置(对应一个TreeMap)

2.判断是否包含这个key,存在的话则删除

3.当元素过少时进行扩容操作


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 && M / 2 >= initCapacity)
                resize(M / 2);
        }
        return ret;
    }


修改操作:


1.根据key计算要进行修改的位置(对应一个TreeMap)

2.存在该元素时则修改


public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if (!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");
        map.put(key, value);
    }


查询操作:


 

public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }
    public V get(K key){
        return hashtable[hash(key)].get(key);
    }


动态空间处理方法:


类似于动态数组,开辟一个新的TreeMap数组,将原始数据全都放入到新的TreeMap数组中即可,注意hash()函数计算时需要使用新的Map长度。


 

private void resize(int newM){
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for (int i = 0; i < newM; i ++)
            newHashTable[i] = new TreeMap<>();
        int oldM = M;
        // hash()函数需要新的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 static void main(String[] args) {
        System.out.println("Pride and Prejudice");
        ArrayList<String> words = new ArrayList<>();
        if(FileOperation.readFile("pride-and-prejudice.txt", words)){         
            // HashTable
            startTime = System.nanoTime();
            HashTable<String, Integer> ht = new HashTable<>(131071);
            for (String word : words) {
                if (ht.contains(word))
                    ht.set(word, ht.get(word) + 1);
                else
                    ht.add(word, 1);
            }
            for (String word: words)
                ht.contains(word);
            endTime = System.nanoTime();
            time = (endTime - startTime)/1000000000.0;
            System.out.println("HashTable: " + time);
        }
    }

执行用时:


80.png


5 *应用


有了哈希表我们可以应用于数据可以无序存储但需要快速查询的场景。比如:Word 文档中单词拼写检查功能。


常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间。所以我们可以用散列表来存储整个英文单词词典。


当用户输入某个英文单词时,我们拿用户输入的单词去散列表中查找。如果查到,则说明拼写正确;如果没有查到,则说明拼写可能有误,给予提示。


6 来自阿里师傅的面试题


HashMap的初始容量是16,并且容量都维持在n次方原因:性能考虑。只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞(hash值 & length-1)


负载因子越小,内存占用越多,查询时间越快;内存吃紧的话可以调大负载因子,冲突概率会增加。


HashMap对key的要求:不可变的,比如String被final修饰的。


HashMap存在线程安全问题,1.8之前使用头插法会产生环。我们可以使用ConcurrentHashMap来避免线程安全问题。


1.7之前:采用了分段锁机制来储存数据。在集合对象中储存一个segment数组,把集合的元素分为16个分段,每个分段上储存一个HashEntry数组及对应的链表,每个HashEntry数组就相当于一个HashMap。

81.png


当触发锁时每一个segment中都会有一个独立的锁,而不会将整个对象锁住,所以多线程操作时每个segment中的数据不会相互影响,从而保证了效率,当然,这样储存的版本还有着不少的问题,如最多并发只有16个,结构过于臃肿等等,随着版本的迭代,在jdk1.8时更改了ConcurrentHashMap的结构,使之更加完善。


1.8之后:利用CAS原理(比较交换原理)解决并发问题,其中有三个数:所存的值V,预期的值A,要变换成为的新值B。当A和V相同时,V会修改成B,否则不进行操作。这样就能较好的解决线程安全的问题。


相关文章
71.【Java.哈希表(散列表)】
71.【Java.哈希表(散列表)】
60 1
|
8月前
|
存储 安全 算法
详解Java中HashMap、HashTable、ConcurrentHashMap常见问题
详解Java中HashMap、HashTable、ConcurrentHashMap常见问题
73 0
|
6天前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
28 3
|
5月前
|
存储 安全 Java
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
这篇文章是关于Java面试的第二天笔记,涵盖了HashMap与HashTable的区别、ConcurrentHashMap的实现原理、IOC容器的实现方法、字节码的概念和作用、Java类加载器的类型、双亲委派模型、Java异常体系、GC如何判断对象可回收、线程的生命周期及状态,以及sleep、wait、join、yield的区别等十道面试题。
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
|
5月前
|
安全 Java
【Java集合类面试十五】、说一说HashMap和HashTable的区别
HashMap和Hashtable的主要区别在于Hashtable是线程安全的,不允许null键和值,而HashMap是非线程安全的,允许null键和值。
|
8月前
|
存储 安全 Java
【JAVA】concurrentHashMap和HashTable有什么区别
【JAVA】concurrentHashMap和HashTable有什么区别
|
8月前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
56 0
|
存储 安全 Java
java学会这些,我就入门啦!(基础篇六)HashMap、Hashtable、ConcurrentHashMap的原理与区别
java学会这些,我就入门啦!(基础篇六)HashMap、Hashtable、ConcurrentHashMap的原理与区别
|
8月前
|
存储 Java 索引
【亮剑】Java中的并发容器ConcurrentHashMap,它在JDK1.5中引入,用于替换HashTable和SynchronizedMap
【4月更文挑战第30天】本文介绍了Java中的并发容器ConcurrentHashMap,它在JDK1.5中引入,用于替换HashTable和SynchronizedMap。文章展示了创建、添加、获取、删除和遍历元素的基本用法。ConcurrentHashMap的内部实现基于分段锁,每个段是一个独立的Hash表,通过分段锁实现并发控制。每个段内部采用数组+链表/红黑树的数据结构,当冲突过多时转为红黑树优化查询。此外,它有扩容机制,当元素超过阈值时,会逐段扩容并翻倍Segment数量,以保持高性能的并发访问。
67 0
|
8月前
|
存储 并行计算 安全
【Java编程进阶之路 01】深入探索:HashMap、ConcurrentHashMap与HashTable的演进之路
HashMap、ConcurrentHashMap与HashTable均为Java中的哈希表实现。HashMap非线程安全但性能高,适用于单线程;HashTable线程安全但性能较低,已少用;ConcurrentHashMap线程安全且高性能,是并发环境下的首选。三者在线程安全性与性能间各有侧重。
78 1