目录
1.概念
理想的搜索方法:不经过任何比较,一次直接从表中得到要搜索的元素。
哈希表就可以实现这种功能:构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)。
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
这样一一映射的关系就使搜索的速度非常快了。
但是------在上述集合基础上插入数据14,会出现什么问题?
2.哈希冲突
由上易知:14和4所放入的位置冲突了。
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突.
3.常见哈希函数
3.1直接定制法
取关键字的某个线性函数为散列地址:Hash(key) = A*key + B
优点:简单、均匀;缺点:需要事先知道关键字的分布情况.
适用场景:适合查找比较小且连续的情况.
面试题:字符串中第一个只出现一次的字符
3.2除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p (p<=m),将关键码转换成哈希地址.
还有一些只需要了解的方法:平方取中法:假设关键字为1234,平方后为1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,平方后为18671041,抽取中间的3位671(或710)作为哈希地址.
适用场景:不知道关键字的分布,且位数不是很大的情况.
折叠法:将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址.
适用场景:事先不需要知道关键字的分布,适合关键字位数比较多的情况.
随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即Hash(key) = random(key),其中random为随机数函数.
适用场景:关键字长度不等的情况.
数字分析法:根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址.
适用场景:关键字位数比较大,事先知道关键字的分布且关键字的若干位分布较均匀的情况.
4.负载因子调节
散列表的载荷因子定义:α = 填入表中的元素个数 / 散列表的长度
α 越大,说明填入表中的元素越多,产生冲突的可能性就越大,已知哈希表中已有的关键字个数是不可变的,所以我们只能调整哈希表中的数组的大小。
解决哈希冲突的常见方法:闭散列和开散列:
4.1冲突解决-闭散列
闭散列,又叫开放定址法。当发生哈希冲突时,如果哈希表未被填满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的"下一个"空位置中去。
问题是:如何寻找下一个空位置呢?
1.线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
注意:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
2.二次探测
只是单纯挨个向后探测,数据量大的情况下还是会有很大概率出现哈希冲突,二次探测为了避免该问题,更换了寻找下一个空位的方法:Hi = (Ho+i^2)%m,或Hi = (Ho-i^2)%m。其中 i=1,2,......,Ho是通过散列函数Hash(x)对元素关键码key进行计算得到的位置,m是表的大小。
研究表明:当表的长度为质数且表的负载因子α不超过0.5时,新的表项一定能够插入,且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子α不超过0.5,如果超出必须考虑扩容。
------因此,闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
4.2冲突解决-开散列
开散列,又叫链地址法(开链法)。首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头点存储在哈希表中。
这就是将大集合的搜索问题转化为小集合的搜索问题,但如果冲突严重,就意味着小集合的搜索性能其实也是不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:每个桶的背后是另一个哈希表或每个桶的背后是一棵搜索树。
5.实现哈希表
5.1Integer类型
java
public class HashBuck {
static class Node { //静态内部类
private int key;
private int value;
private Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
public Node[] array;
public int usedSize;
private static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子默认为0.75
public HashBucket() {
this.array = new Node[10]; //初始大小为10
}
//计算负载因子
private float loadFactor() {
return usedSize*1.0f/array.length;
}
// 插入
public void put(int key,int val) {
Node node = new Node(key,val);
int index = key % array.length;//找到元素对应的位置
Node cur = array[index];
while (cur != null) { //遍历index位置下方的链表
if(cur.key == key) { //插入的元素已经存在则不会重复插入
cur.value = val;
return;
}
cur = cur.next;
}
//元素没有重复,进行头插法
node.next = array[index];
array[index] = node;
usedSize++;
//计算负载因子,大于0.75则扩容
if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
resize();
}
}
//扩容,即重新哈希原来的数据
private void resize() {
Node[] tmpArray = new Node[array.length * 2];//两倍扩容
//遍历原来的数组下标的每个链表,将旧数组的所有元素放到新位置上
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null) {
Node curNext = cur.next;//记录原来链表的下一个节点的位置
int index = cur.key % tmpArray.length;//找到新数组的位置
cur.next = tmpArray[index];//采用头插法放到新数组的index位置
tmpArray[index] = cur;
cur = curNext;//继续向后遍历
}
}
array = tmpArray;
}
//通过key值返回value
public int get(int key) {
int index = key % array.length;
Node cur = array[index];
while (cur != null) {
if(cur.key == key) {
return cur.value;
}
cur = cur.next;
}
return -1;
}
public static void main(String[] args) {
HashBucket hashBucket = new HashBucket();
hashBucket.put(1,11);
hashBucket.put(2,22);
hashBucket.put(12,122);
hashBucket.put(3,33);
hashBucket.put(4,44);
hashBucket.put(14,144);
hashBucket.put(5,55);
hashBucket.put(16,166);
System.out.println(hashBucket.get(16));//166
}
}
若hash表中存储的就只是Integer类型的数据,上述代码完全没问题,但若是想要存放引用类型的数据,又该怎么办呢?
5.2引用类型
在实现引用类型哈希表之前,我们先来看看哈希表与Java类集的关系:
1> HashMap和HashSet即Java中利用哈希表实现的Map和Set;
2> Java会在冲突链表长度大于一定阈值后(1.链表长度大于等于8; 2.数组的大小大于等于64),链表将转变为搜索树(红黑树);
3> Java中计算哈希值实际上是调用类的hashCode方法,进行 key 的相等性比较是调用 key的equals方法,所以如果要用自定义类作为HashMap的key或者HashSet的值,必须重写hashCode和equals 方法,而且要做到equals相等的对象,其hashCode一定是一致的。
java
public class HashBuck<K,V> {
static class Node<K,V> {
private K key;
private V val;
private Node<K,V> next;
public Node(K key, V val) {
this.key = key;
this.val = val;
}
}
public Node<K,V>[] array = (Node<K,V>[])new Node[10];
public int usedSize;
public HashBuck() {
}
public void put(K key,V val) {
Node<K,V> node = new Node<>(key,val);
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while (cur != null) {
if(cur.key.equals(key)) {
cur.val = val;
return;
}
cur = cur.next;
}
node.next = array[index];
array[index] = node;
usedSize++;
//计算负载因子
//扩容
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while (cur != null) {
if(cur.key.equals(key)) {
return cur.val;
}
cur = cur.next;
}
return null;
}
}
java
import java.util.Objects;
class Person { //引用类型
public String id;
public Person(String id) {
this.id = id;
}
@Override //重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}
@Override//重写hashCode方法
public int hashCode() {
return Objects.hash(id);
}
@Override//重写toString方法
public String toString() {
return "Person{" +
"id='" + id + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) {
Person person1 = new Person("1234");
Person person2 = new Person("1234");
HashBuck<Person, String> hashBuck = new HashBuck<>();
hashBuck.put(person1, "张三");
String name = hashBuck.get(person1);
System.out.println(name);//张三
}
}
6.哈希表oj题
1)只出现一次的数字
这题可以使用之前学过的异或解决,超级快,这里就用HashSet来解决了哈~
java
public int singleNumber1(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
if (set.contains(nums[i])) {
set.remove(nums[i]);
}else {
set.add(nums[i]);
}
}
for (int i = 0; i < nums.length; i++) {
if (set.contains(nums[i])) {
return nums[i];
}
}
return -1;
}
2)复制带随机指针的链表
java
public Node copyRandomList(Node head) {
//第一遍遍历,将原来所有的元素重新new一个节点,放入map中
Map<Node,Node> map = new HashMap<>();
Node cur = head;
while(cur != null) {
Node node = new Node(cur.val);
map.put(cur,node);
cur = cur.next;
}
//第一遍遍历完成,此时需要遍历第2遍来修改next和random的值
cur = head;
while(cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
3)石与石头
java
public int numJewelsInStones(String jewels, String stones) {
Set<Character> set = new HashSet<>();
for (int i = 0; i < jewels.length(); i++) {
char ch = jewels.charAt(i);
set.add(ch);
}
int count = 0;
for (int i = 0; i < stones.length(); i++) {
char ch = stones.charAt(i);
if (set.contains(ch)) {
count++;
}
}
return count;
}
4)坏键盘打字
java
/**
* @param str1 应该输入的
* @param str2 实际输入的
*/
public static void func(String str1, String str2) {
Set<Character> set = new HashSet<>();
for (char ch:str2.toUpperCase().toCharArray()) {
set.add(ch);
}
Set<Character> setBroken = new HashSet<>();
for (char ch: str1.toUpperCase().toCharArray()) {
if (!set.contains(ch) && !setBroken.contains(ch)) {
setBroken.add(ch);
System.out.println(ch);
}
}
}
public static void main(String[] args) {
func("7_This_is_a_test","_hs_s_a_es");
}
5)前k个高频单词
java
public static List<String> topKFrequent(String[] words, int k) {
//1.求每个单词出现的次数
Map<String,Integer> map = new HashMap<>();
for(String s : words) {
if(map.get(s) == null) {
map.put(s,1);
}else {
//以前有这个元素了
int val = map.get(s);
map.put(s,val+1);
}
}
//2.统计完毕,创建小根堆
PriorityQueue<Map.Entry<String,Integer>> minHeap = new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if(o1.getValue().compareTo(o2.getValue()) == 0) {
return o2.getKey().compareTo(o1.getKey());//根据字母顺序,变成大根堆
}
return o1.getValue().compareTo(o2.getValue());
}
});
//3.遍历Map
for(Map.Entry<String,Integer> entry : map.entrySet()) {
if(minHeap.size() < k) {
minHeap.offer(entry);
}else {
Map.Entry<String,Integer> top = minHeap.peek();
if(top.getValue().compareTo(entry.getValue()) == 0) {
//频率相同的情况下,比较key值,key值小的入
if(top.getKey().compareTo(entry.getKey()) > 0) {
//出top,入entry
minHeap.poll();
minHeap.offer(entry);
}
}else {
if(top.getValue().compareTo(entry.getValue()) < 0) {
minHeap.poll();
minHeap.offer(entry);
}
}
}
}
//4.把小根堆里面的元素拿出来放到一个集合里面,然后逆置
List<String> ret = new ArrayList<>();
for (int i = 0; i < k; i++) {
String s = minHeap.poll().getKey();
ret.add(s);
}
Collections.reverse(ret);
return ret;
}