需求
假设一个写字楼要统计所有公司的电话,要做成一个通讯录
那么,一个电话号码对应一个公司。则,电话号(8位)作为key,公司详情作为value
要求,添加、删除、查找的时间复杂度为O(1),如何存储数据,做到满足这个需求?
如果是添加、删除、查找的时间复杂度都是O(1),目前我们学习到的:
线性表:数组、链表、栈、队列
非线性表:树、图、集合、映射
数组的查找时间复杂度是O(1)
在不改变数组的位置时,仅仅是在数组中添加某个元素,删除某个元素,而其他元素不做改动的前提下,数组的添加、删除只需要找到该元素,并且赋值或者删除即可,其时间复杂度也是O(1)
因此,上面的需求,使用数组可以满足需求。
可以写出类似下面代码:
//一个compannies数组,数组存放的类型是Company,数组大小是10^8
private Company[] compannies = new Company[100000000];
//以电话为index,存放公司信息
public void add(int phone, Company company)
{
companies[phone] = company;
}
//以电话为index,将存放公司信息清空
public void remove(int phone)
{
companies[phone] = null;
}
//以电话为index,找到公司信息
public Company get(int phone)
{
return companies[phone];
}
以上,就是最简单的以数组的方式,以电话号码为数组的下标,公司信息为数组内容的存储方式。
也被称为哈希表
其,以空间换时间,使用8位数的连续地址,存放公司信息。
虽然能满足需求,但:
空间复杂度太大,需要连续的8位数的数组
空间使用率低,有可能一栋楼就一个公司,但却占用了这么大的空间去存储数据。
以上原理是哈希表的原理,但哈希表有对它进行优化
哈希表(Hash Table)
哈希表也称为散列表
哈希表不将电话号码直接作为数组索引,而是将电话号码通过哈希函数进行转换,转换为的值作为数组下标,当然,公司信息还是作为value
也就是:
原来是电话号码作为key,也作为数组下标
现在通过一个哈希函数,将电话号码传进去,出来的值作为数组下标
复杂度分析:
将key通过哈希函数转化为index的时间复杂度为O(1)
利用index找到对应的value,其时间复杂度为O(1)
哈希函数也被称为散列函数
通过key存储到哈希表,其实哈希表table是一个数组结构。
哈希冲突(Hash Collision)
哈希冲突也被称为哈希碰撞
指的是:两个不同的key,通过某一个哈希函数算出来的索引值index相等。
这就出现了问题,明明是不一样的电话号码,却搜索出同一家公司。
也就是 key1 != key2,但是hash(key1) = hash(key2)
解决哈希冲突常见的办法有:
- 开放定值法:通过一定的规则(比如一个一个,或者2^i)向table的其他地方寻找空地址,并将value存储在空值里面
- 再哈希法:设计多个哈希函数
- 链地址法:比如用链表将同一个index的元素串起来
在java,JDK1.8中,是如何解决哈希冲突的呢?
使用的是连地址法
默认使用单向链表将元素串起来
如果重复元素太多(>=64),则使用红黑树来管理重复元素。
当重复元素变少(<64),则又变为链表结构存储元素。
这是因为,红黑树的添加、删除、查找的时间复杂度比链表要高。越是数据量大,红黑树越能体现出优势。
当然,数据都是活的,你也可以规定128使用红黑树,或者你压根就都使用红黑树都可以。这是你的使用方法,64个元素是java的使用方法。
问:为何使用的是单向链表?
对于链表结构,查找的时间复杂度是O(n),怎么不使用双向链表,给一个present指针?
这是因为,在使用哈希函数得到索引值的时候,如果索引值上的value已经有值,系统会对新的value值于已经存在的value值进行一个一个比较。
如果链表中有一个值与新值相等,则说明之前存过这个值,就不需要后面比较,而对于新值旧值的处理方法,有两种方法,一种是将新值覆盖旧值,一种是将新值抛弃。这就看你怎么处理了。
如果找遍了链表,也没有发现里面有值与新值相等,则将新值放在链表的最后面。
也就是,新值需要与链表的每一个元素进行比较,并且是比较完后插入在链表尾部。因此,单向链表满足需求。
双向链表也满足需求,但是,没有用到present指针,造成浪费,因此,使用单向链表。
哈希函数
哈希表中的哈希函数的实现步骤大致有:
1 key通过哈希函数生成哈希值(必须是整数)
2 再将生成的哈希值 跟 数组的大小 进行相关运算,生成一个索引。
也就是,并不是之前所说的直接将key通过哈希函数生成index索引。
而是:key通过哈希函数生成哈希值,然后对哈希值进行处理,才得到索引。
其中,第二步的目的是确保生成的索引小于数组的大小。
因此,可以使用:
public int hash(Object key)
{
return hasn_code(key) % table.length;
}
或者
public int hash(Object key)
{
return hasn_code(key) & (table.length - 1);
}
两种方法来满足第二步。
良好的哈希函数具有:
让哈希值更加均匀的分布
减少哈希冲突次数
提升哈希表的性能
如何生成哈希值?
哈希值指的是key通过哈希函数生成的值。
首先,key的类型可能是int float double string object
不同类型的key,哈希值的生成方式不一样,但目标一致:
- 尽量让每个key的哈希值都是唯一的
- 尽量让key的所有信息参与运算
对于整数
直接将整数的值作为哈希值。比如整数10的哈希值就是10
类似:
public static int hashCode(int value)
{
return value;
}
对于浮点数
首先我们要知道,浮点数最后存储的也是010101二进制格式
因此,我们可以将浮点数转化为整数值
对于字符串
对于整数5489是这样计算出来的:
5 * 10 ^3 + 4 * 10 ^ 2 + 8 * 10 ^1 + 9 * 10 ^ 0
应用上面方法,可以对字符串进行处理
比如jack
可以表示为:
j * n ^ 3 + a * n ^ 2 + c * n ^1 + k * n ^ 0
优化后:
((j*n + a)*n + c)*n + k
在java的JDK中,n=31;
为何n是31,这是一个数学问题。
String strinf = "jack";
int hashCode = 0;
int len = string.length();
for(int i = 0; i<len; i++)
{
char c = string.charAt(i);
hashCode = 31 * hashCode + c;
}
对于自定义的object类型
举例
public class Person{
private int age;
private float height;
private String name;
private Car car;
}
其实,在java中,系统是有实现hashCode方法的,也就是,java内部有自己的hashCode去将key转换为哈希值。该方法是在基类object中的。
然而,其hashCode生成的哈希值是跟对象的内存地址有关的。
public static void main(String[] args)
{
Person p1 = new Person(10, 1.67f, "jack", car);
Person p2 = new Person(10, 1.67f, "jack", car);
}
在实际开发中,有这种可能:比如上面的两个对象,我们认为他们是同一个key。也就是在有哈希表存储的时候,两个对象存储在同一个index下。
如果是使用系统的hashCode方法,不能满足上面的需求。
因此,我们要重写对象的hashCode方法。
(当然,如果系统方法可以满足你的需求,你也没必要重写hashCode方法)
重写方法,也是看如何能满足你的需求,毕竟,数据是活的。
根据大原则:key中的每一个元素尽量都参与运算(当然,如果你的需求是只需要name和height做判断,你也可以只使用name和height,还是那句话,数据是活的,一切都是看你自己的需求)
@Override
public int hashCode()
{
int hash = Integer.hashCode(age);//类方法
hash = 31 * hash + Float.hashCode(height);//类方法
hash = 31 * hash + name.hashCode();//对象方法
hash = 31 * hash + car.hasCode();//对象方法
return hash;
}
在上面的例子中,我们使用类似String类型的方法,处理Person里面的成员变量。
之前在叙述:如果index相同,去比较value值是否相同是不严谨的。并不是简单的value1 == value2
而是使用key跟key作比较,key1 == key2
当然,也不是简单的key1 == key2,而是一个方法equals(),对key做比较。
如果比较结果相等,则进行value的替换。
如果比较结果不相等,则插入链表尾部。
问题是,哈希表中,在index相同情况下,如果后面使用红黑树进行存储,那么红黑树需要可比较性。
然而,哈希表的key有可能类型都不一样,比如:key有可能是int float double string object null
类型都不一样,又如何去比较呢?
key - 哈希值 - 索引index
索引一样,不能说明key一定相同,也可能不相同。
也不能通过哈希值来判断key是否相同。
因此,如何判断Key是否相同,只能通过equals方法。
@Override
public boolean equals(Object obj)
{
if(obj == this) return true;//如果地址相等
if(obj == null || obj.getClass() != getClass()) return false;//如果传入的为null或者类型不同
Person person = (Person)obj;
return person.age == age
&& person.height == height
&& valueEquals(person.name, name)
&& valueEquals(person.car, car);
}
private boolean valueEquals(Object v1, Object v2)
{
return v1 == null ? v2 == null : v1.equals(v2);
}
关于自定义对象作为Key的equals和hashCode的总结:
equals:用来判断2个key是否为同一个key
hashCode:key通过哈希函数生成的哈希值
该方法保证equals为true的2个key的哈希值相等。
但,hashCode相等的两个key,其equals不一定相等。