leetcode每日一题之设计哈希集合
题目链接:https://leetcode-cn.com/problems/design-hashset/
题目描述:不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet 类:
void add(key) 向哈希集合中插入值 key 。
bool contains(key) 返回哈希集合中是否存在这个值 key 。
void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。
分析:
为了实现哈希集合这一数据结构,有以下几个关键问题需要解决:
-
哈希函数:能够将集合中任意可能的元素映射到一个固定范围的整数值,并将该元素存储到整数值对应的地址上。
-
冲突处理:由于不同元素可能映射到相同的整数值,因此需要在整数值出现「冲突」时,需要进行冲突处理。总的来说,有以下几种策略解决冲突:
-
链地址法:为每个哈希值维护一个链表,并将具有相同哈希值的元素都放入这一链表当中。
-
开放地址法:当发现哈希值 hh 处产生冲突时,根据某种策略,从 hh 出发找到下一个不冲突的位置。例如,一种最简单的策略是,不断地检查 h+1,h+2,h+3,\ldotsh+1,h+2,h+3,… 这些整数对应的位置。
-
再哈希法:当发现哈希冲突后,使用另一个哈希函数产生一个新的地址。
-
-
扩容:当哈希表元素过多时,冲突的概率将越来越大,而在哈希表中查询一个元素的效率也会越来越低。因此,需要开辟一块更大的空间,来缓解哈希表中发生的冲突。
package com.tao.hashtable;
import java.util.Iterator;
import java.util.LinkedList;
/**
* @Classname MyHashSet
* @Description 利用双链表设计哈希集合
* Java 标准库的 HashMap 基本上就是用 拉链法 实现的。
* 拉链法 的实现比较简单,将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。
* 若遇到哈希冲突,则将冲突的值加到链表中即可。
* <p>
* 实现步骤
* <p>
* *得到一个 key
* *计算 key 的 hashValue
* *根据 hashValue 值定位到 data[hashValue]。(data[hashValue] 是一条链表)
* *若data[hashValue]为空则直接插入
* *不然则添加到链表末尾
* 这里需要注意的是,哈希函数必须保证哈希值的均匀分布 ,若全部集中在一条链表中,则时间复杂度和顺序链表相同。
* <p>
* 还有一点则是数组的大小,若你能估计数据的大小,则直接指定即可,否则就需要 动态扩充 数组。
* int[] newArray = new int [array.length*2];
* //将array数组从0位置至array.length位置,复制到newArray数组0位置到array.length位置。
* System.arraycopy(array,0,newArray,0,array.length);
*
* @Date 2021/8/4 21:36
* @Author Anonymous
*/
@SuppressWarnings("all")
public class MyHashSet2 {
public static void main(String[] args) {
}
private static final int BASE = 769;//基准值,作为哈希取模运算
private LinkedList<Integer>[] data;//数组中每一格就是一个链表。
/**
* Initialize your data structure here.
*/
public MyHashSet2() {
data = new LinkedList[BASE];
for (int i = 0; i < data.length; i++) {
data[i] = new LinkedList<>();
}
}
/*
* @Author Anonymous
* @Description //向集合中添加元素
* @Date 10:04 2021/8/9
* @Param [key] 表示要插入的值
* @return void
**/
public void add(int key) {
int hash = hashValue(key);//先计算哈希值
Iterator<Integer> iterator = data[hash].iterator();//获得hash所在链表的迭代器
while (iterator.hasNext()) {//遍历hash值为hash所在的链表
Integer next = iterator.next();//得到下一个值
if (next == key) {//如果里面有重复的元素直接返回
return;
}
}
data[hash].offerLast(key);//如果没有重复元素就添加到链表末尾
}
/*
* @Author Anonymous
* @Description //从集合中删除元素
* @Date 10:05 2021/8/9
* @Param [key] 表示要删除的元素
* @return void
**/
public void remove(int key) {
int hash = hashValue(key);//获得哈希值
Iterator<Integer> iterator = data[hash].iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();//得到下一个元素
if (next == key) {
data[hash].remove(next);
return;
}
}
}
/**
* Returns true if this set contains the specified element
*/
public boolean contains(int key) {
int hash = hashValue(key);
Iterator<Integer> iterator = data[hash].iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next == key) {
return true;
}
}
return false;
}
/*
* @Author Anonymous
* @Description //
* 设哈希表的大小为base,则可以设计一个简单的哈希函数:hash(x)=x mod base。
* 我们开辟一个大小为base的数组,数组的每个位置是一个链表。当计算出哈希值之后,就插入到对应位置的链表当中。
* 由于我们使用整数除法作为哈希函数,为了尽可能避免冲突,应当将 base 取为一个质数。在这里,我们取base=769。
* @Date 10:06 2021/8/9
* @Param [value] 要要计算的插入的数字的哈希
* @return int 哈希值
**/
public int hashValue(int value) {
return value % BASE;
}
}
查了下质数取模,其实是利用了同余的概念:当元素是个有规律的等差数列时,并且和基数(数组大小)最大公约数不为1时,就会造成哈希映射时冲突变高(数组某些位置永远不会有值)。比如数列0,6,12,18,24,30…,
- base为10,取模(0,6,2,8,4,0…)后,放入哈希表中位置将只能在0,2,4,6,8这几个数组位置上;
- 但我们如果把base取7(数组大小甚至比10小),同样数列取模后(0,6,5,4,3,2,1,0,…),可以分布在哈希表中的0,1,2,3,4,5,6所有位置上;
后续:若x和y的最大公约为z,x和y的最小公倍数就为(xy)/z,很明显,若z为1,也就是俩数的最大公约数为1的时候,那么俩数的最小公倍数就为xy。
那么当一个数为质数时,除了其自身的倍数外,其余数和其的最大公约数都将是1,这时,步长选任何数(除了倍数)都可以满足桶的均匀分布。
所以,以取模计算哈希值在桶中的位置是,用一个质数当作基数时可以使得哈希表中每个位置都“有用武之地”。
Hash的用途很多,我们在使用Ngnix做负载均衡的时候,同样用的也是Hash的方式。总的来说,要是数据分布均匀一些,在这种时候就可以考虑使用Hash的方式对数据进行处理。
当然质数也不一定非要是769:可以参考:https://planetmath.org/goodhashtableprimes