1、由问题引出哈希表
为了介绍哈希表,咱们先来看leetcode上一个简单的问题。算法
解决思路:
先建立一个映射,而后扫描一遍传入的字符串,将每一个字符对应出现的频率存入到映射中,以后在扫描一遍传入的字符串,返回第一个频率为1的字符,若是不存在则返回-1.缓存
另外,题目中告诉咱们假定该字符串只包含小写字母,那么也就是说咱们这个映射所映射的字符只多是a~z这26种。数据结构
在这种状况下,咱们可使用一种简单思惟就是咱们不使用像二叉树这样的数据结构来实现这个映射,咱们直接制做一个包含有26个元素的数组,数组的中存储的数据就是对应索引在字母表中对应的字母出现的频率函数
例如:索引为0的位置存储的值表明的是字母a出现的频率,索引为1表明字母b的频率,索引2表明字母c的频率,以此类推。学习
这样作不只仅是理解起来简单,更重要的是当咱们进行读写操做时,时间复杂度为O(1) url
解决代码:
public int firstUniqChar(String s) { int[] freq = new int[26]; for(int i = 0 ; i < s.length() ; i ++) freq[s.charAt(i) - 'a'] ++; for(int i = 0 ; i < s.length() ; i ++) if(freq[s.charAt(i) - 'a'] == 1) return i; return -1; }
上述问题理解起来仍是比较容易的,介绍这个问题的缘由是由于这个问题背后隐藏着哈希表这种数据结构。那么到底什么是哈希表呢?spa
2、什么是哈希表?
在解决上面说的问题时,咱们开辟了一个有26个空间的int数组frequency,其实就是一个哈希表。.net
具体来讲,咱们想要作的其实就是让每个字符和一个数字之间进行一个映射关系,那么这个数字呢表示的是字符在整个字符串中出现的频率。设计
这里有一个关键点,咱们能使用这样一个数组就能解决问题,即咱们能直接使用freq[0]获取到字符a的频率,是由于咱们将每个字符都和一个索引进行了对应,使得字符和索引之间有个对应关系,以后就能够直接使用这个索引在数组中寻找到相应的字符的映射内容。这里的关系就是这个索引的值等于这个字符的ASCII码减去字符a的ASCII码。
哈希表的本质就是把咱们真正关心的那个内容在咱们这个问题中是字符对应的这个内容(键)转化成一个索引,而后直接用一个数组来存储相应的频率(值),那么因为数组自己是支持随机访问的,因此咱们可使用O(1)的时间复杂度来完成各项的操做,这就是哈希表的一个巨大优点。
3、使用哈希表须要处理的两件任务
在对哈希表有必定的了解后,咱们能够看出来,对于哈希表有两个核心问题。
一、设计合理的哈希函数
对于咱们所关注的这个内容,拿上述问题来讲,咱们关注的是字符它所对应的频率,那么对于每个字符,咱们必须首先把它转化成一个索引。在通常状况里,一个哈希表中是能够存储各类相应的数据类型的,对于每种数据类型,咱们都须要一个方法把它转化成一个索引,相应的咱们关心的这个类型转换成索引的这个函数,就称之为是哈希函数。
若是更严谨的来讲,在上述问题中,咱们的哈希函数其实就能够写成这个样子
F(ch) = ch – ‘a’
F(ch)就是对于给定的一个字符,咱们经过这个函数f就把它转化成一个索引,这个转化的方法就是很是简单的,用ch对应的ascii值减去a对应的ascii的值就行了。
那么咱们有了这个哈希函数,可以把咱们真正关心的这个数据类型转换成索引,以后咱们只须要在哈希表上进行操做就行了,在这个问题中这个操做很是的简单,直接在这个数组对应的索引上去查找或者进行加操做就行了,很是的容易。
不过并非全部的时候哈希表都是这么容易的,咱们在这个问题中把键转化为索引的转化方式正好是一一对应的这样的一种转化,因此咱们能够很是容易地直接用一个数组来存储全部的内容,但在大多数状况下咱们处理的数据会更加复杂。
好比说咱们对居民的信息感兴趣,那么在这里咱们识别每个居民的这个键多是他对应的身份证号,可是在我国身份证号是很是复杂的,一共有18位数,很显然这个18位数咱们总体把它看做一个整数来讲就太大了,此时咱们就不能直接用这个数字来当作数组的索引,由于这样作,实际上也浪费了不少的空间,固然有更多的数据类型,它自己就跟索引可能8竿子打不着,区别巨大。
最典型的状况就是字符串,好比说咱们关注学生的信息,可是咱们是用学生的名字来做为键查看每一个学生的具体信息,那么这会这个键就是一个字符串,咱们如何设计一个哈希函数,将字符串转化为索引?这就是哈希表中咱们须要考虑的问题。
除此以外还有各类数据结构牵扯到这种问题,包括好比说咱们关系的这个键多是一个浮点型或者是一个复合的类型,好比说是一个日期,每个日期包含可能有年月日相关的信息,甚至更多一些例如时分秒这样的信息,那么这样一种复合的数据类型,咱们在使用哈希表这种数据结构的时候都须要将它们首先转化为一个索引才可使用,那么相应的,咱们就须要设计一个合理的哈希函数,那么这就是咱们在学习哈希表的时候要处理的第1个任务。
二、避免哈希冲突
不少时候,咱们就不得不处理一个在哈希表中很是关键的问题就是两个不一样的键,经过咱们的哈希函数以后,它们对应了一样一个索引,那么这种状况呢,一般称之为叫作产生了哈希冲突。
那么咱们在哈希表上的操做其实最复杂的部分也就是在解决这种哈希冲突上。
若是咱们设计的哈希函数很是好,是一一对应的,那就像咱们在以前所完成的这个问题同样,咱们对哈希表的操做也将很是简单,不过对于更通常的状况,咱们在哈希表上的操做主要就要考虑如何解决哈希冲突,那么这就是咱们学习哈希表要主要解决的第2个关键问题。
另外还有一种极端状况,就是假设咱们只有一个空间,全部的元素都存储在其中,这样其实就变成了一个链表,时间复杂度变为O(n)。不过这种状况基本不可能出现。
4、总结
对于哈希表这种数据结构,它充分的体现了咱们在学习算法设计领域的时候,一个很是重要的很是经典的思想,也就是用空间换时间。仔细的思考一下本身处理的不少算法问题,包括学习的不少经典算法,其实本质上都是用空间换时间,不少时候咱们多存储一些东西、预处理一些东西、缓存一些东西,那么在实际执行咱们的算法任务的时候,咱们完成这个任务获得这个结果就会快不少,哈希表很是完美的体现了这一点。
回到我么上面说的身份证号的例子,那么假如咱们能够开辟无限大的数组或者更准确的说的话,咱们能够开辟这个18个9这么多的空间,这样的一个数组的话,那么咱们彻底能够用在这一小节所使用的这种最为简单的一个数组的方式,来解决咱们所须要的这种数据存储的功能,或者是这种映射的功能,咱们在这样巨大的一个数组中可使用O(1)的时间完成对任意一个身份证号,相应的这个居民的信息增删改查相应的内容是很是容易的,不过实际上很难能开辟一个这么大的空间,有兴趣能够实际的计算一下,开辟这么大的一个空间,就算每个空间只存储一个32位的整型的话,那么总体这是多么大的一个空间。
另外的一个极端就是咱们对应的这个数组只有一个位置,只有1的空间,此时其实就至关于咱们要存储的全部的内容都会产生哈希冲突,咱们把全部的内容都堆着。在这惟一的这个数组空间中,假设咱们以链表的方式来组织咱们总体的这个数据的话,那相应的各项操做所完成的时间复杂度就变成了o(n)这个级别,这就是设计哈希表的两个极端状况。
哈希表总体就是在这两者之间产生一个平衡。