第9讲 对比Hashtable、 HashMap、 TreeMap有什么不同?

对比Hashtable、 HashMap、 TreeMap有什么不同? 谈谈你对HashMap的掌握。

  • Hashtable、 HashMap、 TreeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。

  • Hashtable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
    自我补充:遗留的类,不建议使用。

  • HashMap是应用更加广泛的哈希表实现,行为上大致上与HashTable一致,主要区别在于HashMap不是同步的,支持null键和值等。通常情况下, HashMap进行put或者get操
    作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户ID和用户信息对应的运行时存储结构。

  • TreeMap则是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的get、 put、 remove之类操作都是O(log(n))的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。

类结构图(不包括并发包)

在这里插入图片描述

HashMap分析

回顾散列(hash)数据结构

散列表用的是数组支持按照下标随机访问的数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。

由于数组下标随机访问时间复杂度是O(1)的,hash表的时间复杂度也是O(1)的。

数组的随机访问特性:

我们知道数组创建是一段连续的内存空间,并且存储相同的数据类型。
在这里插入图片描述
如上图,我们存储整型数据数组{500,562,14}.如果知道第0个数组的内存地址,我们就知道1这个下标的内存地址。因为整型数组占用4个字节。
利用这个特性。我们只要知道数组下标,也就是数据在数组中的位置,就可以知道内存地址位置。从而快速访问,时间复杂度O(1)。

但是,如果不是数组下标,只是知道数据的值,那么必须整体遍历一遍复杂度O(n)。

为什么数组O(1),Hash O(1)

举个例子,如果校园运动会89名选手参加,胸前贴号码牌。这个号码牌就是1-89.
如果将这89名选手放到数组里面,标号为1的选手,放到1的数组下标中,2就放2的下标这样数组下标和标号一一对应了,我们查找的时间复杂度就是O(1)。
在这里插入图片描述

当然实际情况hash不会这么简单。
就像现实情况下,参赛的编号可能不是1-89,而是3年二班第二位选手就是我们之前的2号,我们可以表示成030202 -->2 号选手–>数组下标2。我们只需要截取后两位,这样我们还是能找到数组下标2的位置。
在这里插入图片描述

我们抽象出来就是一个(key,value)形式。 (“1号选手”,“张三”)
1号选手叫:key 键
张三叫:value值
key的后两位的值叫做:散列值,hash值,hash code
选手编号转化为数组下标的映射方法hash(key),叫做散列函数(Hash函数)。
在这里插入图片描述

顺便提一下hash code
key的后两位的值叫做hash code。所以我们需要注意的是,散列值不是内存地址,就是代表对象中hash表的位置,是一个整数。

在这里插入图片描述
从整体来看key和table的关系:
在这里插入图片描述
java中String的 hash code如何实现的?:

《Effective Java》中提到Hashcode中的约定:

  1. 应用程序运行期间:只要equals方法操作,用到的信息没有被修改,同一对象调用多次,hashCode返回同一个整数。
    同一程序调用多次:每次返回的整数可以不一致。
  2. equals相等, hashCode一定要相等。
  3. equals不等,hash code不一定不同,但是程序员应该知道,给不想打呢个的对象产生截然不同的整数结果,有可能提高散列表(hash table)性能。

所以根据第三条,我们要是设计一个散列函数,散列函数生成的值要尽可能随机并且均匀分布。
并且给出了一些办法。详细请参考:
《Effective Java》
第二版第三章第9条:覆盖equals是总要覆盖hash code.

散列函数工业级别:

工业级别不能像我们描述的那么粗糙,需要满足三个条件:
1.散列非负整数
2.key1 = key2 ,hash(key1) == hash(key2),相同的key的到的散列值也是相同的。
3.key1 !=key2 ,hash(key1) != key2 散列冲突问题。

工业级别的hash算法有:MD5,SHA,CRC。但是工业级别也无法完美解决散列冲突。

3不好理解:如果存储键值对(x,“123”) 通过hash函数hash(x)得到"123"的存储位置。
如果再来一个(y,“456”),hash函数hash(y), 得到的hash值是一样的,这两个对象的存储地址就冲突了。我们叫做散列冲突问题。

如何解决散列冲突?

解决散列冲突主要有几种:

开放地址法:如果出现散列冲突,我们就重新探测一个空闲的位置,将其插入。
我们可以使用线性探测来插入位置。

橘黄色表示有值,黄色表示没有值。
插入:散列表的大小为10,如果x hash之后,散列到下标7,但是7是橘黄色的有值。所以需要顺序往后找,如果没找到,再次从开始找。
最终找到2的位置。
在这里插入图片描述
查找:还是依次查找。

二次探测(Quadratic probing):和线性探测很像。 线性探测每次探测的步长是1,那它探测的下标序列就是hash(key)+0,hash(key)+1,hash(key)+2……而二次探测探测的步长就变成了原来的“二次方”,也就是说,它探测的下标序列就是hash(key)+0,hash(key)+12,hash(key)+22.

双重散列(Double hashing):散列两次。如果散列之后被占用,再次散列。

HashMap中如何解决散列冲突?

综合考虑了所有因素,采用链地址法。即,数组(hash)+链表

在这里插入图片描述
在散列表中,每个buket或者slot对应一条链表,散列值相同的元素我们放到相同槽位对应的链表中。
顺便提一句,jdk1.8中还添加了红黑树,防止链表过长。
在这里插入图片描述

HashMap 的重要属性:

(1)Node[] table:

transient Node<K,V>[] table;

结构:

static class Node<K,V> implements Map.Entry<K,V> {
   
   
        final int hash;    //用来定位数组索引位置
        final K key;
        V value;
        Node
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值