数据结构与算法---哈希表

需求

假设一个写字楼要统计所有公司的电话,要做成一个通讯录
那么,一个电话号码对应一个公司。则,电话号(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不一定相等。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值