目录
一、哈希表的引出
1、例题
2、哈希表以及哈希函数
3、哈希冲突
二、基于开散列方式实现的哈希表
1、哈希表的基本内容
2、哈希表的添加操作
3、哈希表的扩容操作
4、哈希表的查找操作
5、哈希表的删除操作
三、相关代码
字符串中第一个唯一的字符
第一种思路,使用Map集合:
先将字符串转换为字符数组,在Map
集合中保存字符以及字符出现的次数,遍历Map集合取出次数为1所对应的key值,再次遍历字符串,找到对应的索引就解决了
public int firstUniqChar(String s) {
char[] data=s.toCharArray();
Map map=new HashMap<>();
for (char i:data){
map.put(i,map.getOrDefault(i,0)+1);
}
for (int i = 0; i < data.length; i++) {
if (map.get(data[i]) == 1) {
return i;
}
}
return -1;
}
将每个字符出现的次数保存到整型数组中,最后找到次数为1的字符即可
public int firstUniqChar(String s) {
//由于s中只包含小写字母,因此就将每个字符出现的频次保存到整型数组中
int[] arr=new int[26];
//遍历字符串,将字符出现的频次保存到arr数组中
for (int i = 0; i
上述例题中的第二种方法的arr数组就是一个哈希表,每一个不重复的字符都和一个整型数字一一对应,按照规则(字符-‘a’)将每个字符转换为数字,这个转换操作就叫做哈希函数。
哈希表中需要一种方法将任意数据类型转换为数组的索引,这样的一种方法就称为哈希函数
哈希表就是基于数组的扩展,在数组中如果知道索引,就可以在O(1)的时间复杂度内找到该元素,哈希表体现了空间去换取时间的策略方法
例题:
在数组[9,5,2,7,3,6,8]中查找元素是否存在?
就建立一个长度为10的arr数组,遍历原数组,若元素存在就在arr数组对应的位置添加true
int[] arr=new int[10];
arr[9]=true;
arr[5]=true;
.........直到扫描完整个集合
此时要查询7是否存在,就判断arr[7]的值是否为true
在上述例题中,我们开辟了原数组最大值+1的新的哈希数组,但是当原数组的数字之间的跨度非常大时,或者包含负数,这种方法就不适用了,比如[9,100000,-34,30000000,44]这个数组来说,就没法创造一个数字一一对应的索引的哈希数组了。
(1)哈希冲突的定义
当原数组的数字跨度非常大时,就需要让数字和下标建立一个映射关系(hash函数),让跨度很大的一组数据转为跨度很小的一组数据
哈希函数可以将任意数据转换为索引
对于数据跨度很大的原数组来说,一般来说我们将任意正整数映射为小区数字最常用的方法是“取模”
[10,20,30,40,50] 映射为[0,1,2,3,4]对原数组进行%4
10%4=2
20%4=0
30%4=2
40%4=0
50%4=2
哈希冲突:不同的数据经过函数的计算后得到了相同的值
解决方法就是对原数组取模一个素数7就可以了
(2)哈希冲突的解决方法
①闭散列:当发生冲突时,找到冲突位置的旁边是否存在空闲位置,直到找到第一个空闲位置放入元素(好存难查更难删),当哈希表的冲突十分严重时,此时查找一个元素就从O(1)变成了遍历数组O(n)
②开散列:若出现哈希冲突时,就让这个位置变为链表,此时的哈希表就是数组加链表
如果当前哈希表的某个元素位置,就比如19这个位置,恰好后面好多元素取模后都等于19,此时19所在的链表就会非常长,查找效率就会降低
解决方法:
针对整个数组进行扩容(原先数组长度101,下载扩容到102,就会由原先的%101变成%202),此时很多原来同一个链表的元素就会被均分到新的位置,降低了哈希冲突
将这两个冲突严重的链表再次变为新的哈希表或者二分搜索树,只处理冲突严重的部分就好了,
public class MyHashMap {
//有效节点个数
private int size;
//实际存储元素的Node数组
private Node[] hashtable;
//取模数
private int M;
public MyHashMap(){
//默认初始化容量
this(16);
}
public MyHashMap(int init){
//初始化容量
this.hashtable=new Node[init];
this.M=init;
}
/**
* 对key值进行hash运算
* @param key
* @return
*/
public int hash(int key){
return Math.abs(key)%M;
}
}
class Node{
//对key进行hash运算
int key;
int value;
//下一个节点
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
/**
* 将一堆键值对保存到哈希表中,若key存在就修改原来的键值对,返回修改前的元素
* @param key
* @param val
*/
public int add(int key,int val){
//先对key取模,取模后的值就是索引
int index=hash(key);
//遍历index对应的链表,查看key值是否存在
for (Node x=hashtable[index];x!=null;x=x.next){
if (x.key==key){
int oldval=x.value;
x.value=val;
return oldval;
}
}
//此时整个链表中不包含相对应的key值,就头插到当前链表
Node node=new Node(key,val);
//头节点就是hashtable[index]
node.next=hashtable[index];
hashtable[index]=node;
size++;
return val;
}
那么该在什么时候进行扩容操作呢?
哈希冲突严重的时候,如何判断哈希冲突的严重程度,在此引入负载因子
负载因子(LoadFactor)=哈希表的有效元素个数/哈希表长度
负载因子的值越大,就说明冲突越严重,但是数组的的利用率较高(数组中存储的元素很多),反之负载因子越小,就说明冲突越小,数组的利用率越低(数组中存储的元素比较少)。负载因子就是在空间和时间中取平衡值。
对于数组的扩容来说,当数组长度*负载因子<=有效元素个数时,就需要扩容。
此时在哈希表的基本属性中加入负载因子,假设此时是0.75
//负载因子
private static final double LoadFactor=0.75;
/**
* 将一堆键值对保存到哈希表中,若key存在就修改原来的键值对,返回修改前的元素
* @param key
* @param val
*/
public int add(int key,int val){
//先对key取模,取模后的值就是索引
int index=hash(key);
//遍历index对应的链表,查看key值是否存在
for (Node x=hashtable[index];x!=null;x=x.next){
if (x.key==key){
int oldval=x.value;
x.value=val;
return oldval;
}
}
//此时整个链表中不包含相对应的key值,就头插到当前链表
Node node=new Node(key,val);
//头节点就是hashtable[index]
node.next=hashtable[index];
hashtable[index]=node;
size++;
//添加完元素后判断是否需要扩容
if (size>=hashtable.length*LoadFactor){
//扩容方法
Expansion();
}
return val;
}
/**
* 哈希表的扩容方法。默认让新数组的长度变为原来的一倍
*/
private void Expansion() {
//新数组长度时原来的一倍
Node[] newHashTable=new Node[hashtable.length*2];
//将原数组的所有元素搬移到新的数组,此时的取模值M变为了新数组长度
this.M=newHashTable.length;
//进行元素搬移操作
//遍历哈希表数组
for (int i = 0; i
/**
* 判断当前key是否在表中存在
* @param key
* @return
*/
public boolean containskey(int key){
int index=hash(key);
for (Node x=hashtable[index];x!=null;x=x.next){
if (x.key==key){
return true;
}
}
return false;
}
/**
* 判断value是否存在
* @param val
* @return
*/
public boolean containsvalue(int val){
//全表扫描
for (int i=0;i
**
* 哈希表的删除操作
* @param key
* @param val
* @return
*/
public boolean remove(int key,int val){
int index=hash(key);
//判断头节点是否时待删除节点
Node head=hashtable[index];
if (head.key==key&&head.value==val){
//此时头节点就是待删除的节点
hashtable[index]=head.next;
head=head.next=null;
size--;
return true;
}
Node prev=head;
while (prev.next!=null){
if (prev.next.key==key&&prev.next.value==val){
//此时prev恰好就是待删除节点的前驱
Node cur=prev.next;
prev.next=cur.next;
cur=cur.next=null;
size--;
return true;
}else {
prev=prev.next;
}
}
//当前节点在哈希表中找不到
throw new NoSuchElementException("can't find node!cannot remove!");
}
https://gitee.com/ren-xiaoxiong/rocket_class_ds/tree/master/src/hashhttps://gitee.com/ren-xiaoxiong/rocket_class_ds/tree/master/src/hash