散列表
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
-
存储数据时:
- 输入一个
key,把key传递给散列函数处理,计算出一个散列地址 - 把
散列地址作为数组索引,将数据存储在该索引对应的空间上
- 输入一个
-
查询数据时:
- 输入
key,通过散列函数计算出对应的数组下标(散列地址),通过索引获取数据
- 输入
散列函数
key => hashFn(key) => value
必须满足以下条件:
- 散列值(value)是一个非负数:常见的学号、内存寻址呀,都要求散列值不可能是负数
- key 值相同,通过散列函数计算出来的散列值(value)一定相同
- key 值不同,通过散列函数计算出来的散列值(value)不一定不相同
有时候不同值可能会计算出同样的值,这时就会出现冲突,有时候散列函数太复杂,计算耗时过长,这些都会影响性能,所以好的散列函数应该具有如下特点:
好的散列函数需要具有以下基本要求:
- 易计算:它应该易于计算,并且不能成为算法本身。
- 统一分布:它应该在哈希表中提供统一分布,不应导致群集。
- 冲突少:当元素对映射到相同的哈希值时发生冲突。应该避免这些。
例子
试想你在写一个快餐店的点单程序,准备实现一个展示各种食物及相应价格的菜单。你可能会用数组来做(当然这没问题)。
menu = [ ["french fries", 0.75],
["hamburger", 2.5],
["hot dog", 1.5],
["soda", 0.6]
]
该数组由一些子数组构成,每个子数组包含两个元素。第一个元素是表示食物名称的字符串,第二个元素是该食物的价格。
如果现在需要查询hot dog的价格,需要遍历数组,取出每一个子数组,查询出食物名称为hot dog的,在获取价格,如果数组是无序的,需要O(N)步。有序数组则可以用二分查找,只需要O(log N)步。
使用散列表可以O(1)步查询到价格
常见散列函数
- 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。
- 数字分析法:对数据的分析,发现数据中冲突较少的部分,并构造散列地址。
- 平方取中法:先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。
- 取随机数法:使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。
- 除留取余法:取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。
解决冲突
在散列里,冲突是不可避免的,常见的解决冲突方法有:
- 开放地址法(也叫开放寻址法):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希
- 链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。
- 再哈希法:在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。
- 建立一个公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。
常数时间插入,删除和获取随机元素
设计一个支持在平均时间复杂度 O(1) 下,执行以下操作的数据结构。
insert(val):当元素 val 不存在时,向集合中插入该项。remove(val):元素 val 存在时,从集合中移除该项。getRandom:随机返回现有集合中的一项。每个元素应该有 相同的概率 被返回。
示例
// 初始化一个空的集合。
RandomizedSet randomSet = new RandomizedSet();
// 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomSet.insert(1);
// 返回 false ,表示集合中不存在 2 。
randomSet.remove(2);
// 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomSet.insert(2);
// getRandom 应随机返回 1 或 2 。
randomSet.getRandom();
// 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomSet.remove(1);
// 2 已在集合中,所以返回 false 。
randomSet.insert(2);
// 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
randomSet.getRandom();
方式一:暴力破解法
- 插入val时
- 判断数组中是否存在val,存在就直接返回false; 否则把val存储到数组中,并返回true
- 移除val时
- 查询数组中是否存在val,已存在,移除元素,返回true; 否则返回false
- 获取随机一项
- 获取0到数组长度-1范围内的随机数,使用随机数作为索引,查询一个元素返回
class RandomizedSet {
constructor() {
this.items = []
}
isEmpty() {
return this.isEmpty.length
}
isExist(val) {
return this.items.indexOf(val) !== -1
// return this.items.includes(val)
}
size() {
return this.items.length
}
insert(val) {
// 已存在,返回false
// 不存在,存储元素,返回true
if(this.isExist(val)) {
return false
} else {
this.items.push(val)
return true
}
}
remove(val) {
// 已存在,移除元素,返回true
// 不存在,返回false
if(this.isExist(val)) {
this.items = this.items.filter(item => item !== val)
return true
} else {
return false
}
}
getRandom() {
// 相同概率返回集合中的一项数据
const size = this.size()
// 获取0到数组长度-1范围内的随机数
const random = parseInt(Math.random() * size)
return this.items[random]
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* var obj = new RandomizedSet()
* var param_1 = obj.insert(val)
* var param_2 = obj.remove(val)
* var param_3 = obj.getRandom()
*/
方式二:map+数组
- 数组
items存储元素 map存储元素和对应数组下标
class RandomizedSet {
constructor() {
this.items = []
this.map = new Map()
}
size() {
return this.items.length
}
insert(val) {
// 已存在,返回false
if(this.map.has(val)) {
return false
}
// 不存在,存储元素,返回true
this.map.set(val, this.size())
this.items.push(val)
return true
}
remove(val) {
// 不存在,返回false
if(!this.map.has(val)) {
return false
}
const index = this.map.get(val)
// 是数组最后元素
if(index === this.size() - 1) {
this.items.pop()
this.map.delete(val)
} else {
// 不是最后一个元素,移除最后一个元素
const lastVal = this.items.pop()
// 把最后元素覆盖掉当前位置的元素
this.items[index] = lastVal
// 更新map中最后一个元素的索引
this.map.set(lastVal, index)
// 移除map中的当前val
this.map.delete(val)
}
return true
}
getRandom() {
// 相同概率返回集合中的一项数据
const size = this.size()
// 获取0到数组长度-1范围内的随机数
const random = parseInt(Math.random() * size)
return this.items[random]
}
}
方式三:使用set
class RandomizedSet {
constructor() {
this.set = new Set()
}
insert(val) {
// 已存在,返回false
if(this.set.has(val)) {
return false
}
// 不存在,存储元素,返回true
this.set.add(val)
return true
}
remove(val) {
// 不存在,返回false
if(!this.set.has(val)) {
return false
}
// 存在,移除val元素
this.set.delete(val)
return true
}
getRandom() {
// 相同概率返回集合中的一项数据
const size = this.set.size
// 获取0到数组长度-1范围内的随机数
const random = parseInt(Math.random() * size)
return Array.from(this.set)[random]
}
}
第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
思路
- 遍历字符串,使用map记录字符和出现次数
- 第二次遍历字符串,获取当前字符串,使用当前字符在map中获取出现次数,判断次数是否等于1
- 如果等于1,返回当前字符
- 如果字符串中没有满足条件的,返回单个空字符
' '
const firstUniqChar = function(s) {
const map = new Map()
// 统计每个字符串出现的次数
for(const ch of s) {
map.set(ch, (map.get(ch) || 0)+1)
}
for(const ch of s) {
// 当前字符只出现一次
if(map.get(ch) === 1) {
return ch
}
}
return ' '
}