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

需求

假设一个写字楼要统计所有公司的电话,要做成一个通讯录
那么,一个电话号码对应一个公司。则,电话号(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的哈希值跟数组的大小进行相关运算,生成一个索引。

也就是,并不是之前所说的直接将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不一定相等。

1.算法是程序的灵魂,优秀的程序在对海量数据处理时,依然保持高速计算,就需要高效的数据结构算法支撑。2.网上数据结构算法的课程不少,但存在两个问题:1)授课方式单一,大多是照着代码念一遍,数据结构算法本身就比较难理解,对基础好的学员来说,还好一点,对基础不好的学生来说,基本上就是听天书了2)说是讲数据结构算法,但大多是挂羊头卖狗肉,算法讲的很少。 本课程针对上述问题,有针对性的进行了升级 3)授课方式采用图解+算法游戏的方式,让课程生动有趣好理解 4)系统全面的讲解了数据结构算法, 除常用数据结构算法外,还包括程序员常用10大算法:二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法、马踏棋盘算法。可以解决面试遇到的最短路径、最小生成树、最小连通图、动态规划等问题及衍生出的面试题,让你秒杀其他面试小伙伴3.如果你不想永远都是代码工人,就需要花时间来研究下数据结构算法。教程内容:本教程是使用Java来讲解数据结构算法,考虑到数据结构算法较难,授课采用图解加算法游戏的方式。内容包括: 稀疏数组、单向队列、环形队列、单向链、双向链、环形链、约瑟夫问题、栈、前缀、中缀、后缀达式、中缀达式转换为后缀达式、递归与回溯、迷宫问题、八皇后问题、算法的时间复杂度、冒泡排序、选择排序、插入排序、快速排序、归并排序、希尔排序、基数排序(桶排序)、堆排序、排序速度分析、二分查找、插值查找、斐波那契查找、散列、哈希、二叉树、二叉树与数组转换、二叉排序树(BST)、AVL树、线索二叉树、赫夫曼树、赫夫曼编码、多路查找树(B树B+树和B*树)、图、图的DFS算法和BFS、程序员常用10大算法、二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法马踏棋盘算法。学习目标:通过学习,学员能掌握主流数据结构算法的实现机制,开阔编程思路,提高优化程序的能力。
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页