目录
两种模型
Map的使用
Map接口方法的使用
注意事项
Set的使用
哈希表
冲突
如何避免冲突
在我们日常生活中,会进行一些查找操作,比如根据姓名查询考试成绩,根据姓名查询联系方式等在查找是进行一些插入和删除操作,即动态查找. 而Map和Set是一种适合动态查找的集合容器.
两种模型
一般把搜索的数据称为关键字Key,和关键字对应的称为值Value,将其称为Key- Value的键值对.会有下面两种模型
1 纯 key 模型
比如在一本语文书中查找某篇文章是否在书中,查找某个名字在不在通讯录中.
2 key - value模型
比如统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数 <单词,单词出现的次数>
Map中存储的是Key- Value的键值对,Set只存储了Key
Map的使用
Map是一个接口类,该类没有继承自Collection,该类存储的是<Key- Value>结构的键值对,并且Key一定是唯一的,不能重复.
Map接口方法的使用
在这里主要讲一下Map.Entry<K,V>的使用
Map.Entry<K,V>是Map内部实现的用来存放<Key,Value>键值对映射关系的内部类,该类主要提功了<Key,Value>的获取,value的设置以及Key的比较方式
代码表示:
运行结果:
注意事项
1. Map是一个接口,不能实例化对象,如果要实例化对象只能实例化其实现类TreeMap和HashMap
2. Map中存放键值对的key是唯一的,value是可以重复的,在插入是如果插入一个相同的key,而value不同时,会更新原来的value.
3. 在TreeMap中插入键值对时,Key不能为空,否则会抛NullPointerException异常,Value可以为空.但在HashMap的key和value都可以为空
4. Map中的key可以分离出来,存储到set中进行访问(因为key可以重复)
5. Map中的value 可以分离出来,存储到Collection的任何一个子集合中(value可以重复)
6. Map中键值对的key不能直接修改,Value可以修改,如果要修改key,只能先将key删除,在重新插入
Set的使用
Set和Map有两个不同点: Set是继承自Collection的接口类,Set中只存储了Key.
常用方法
使用迭代器进行遍历
运行结果:
注意事项:
1. Set是继承Collection的一个接口类
2. Set只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,并且使用key与Object的一个默认对象作为键值对插入到Map中
4. set最大的功能就是对集合中的元素进行去重,因为集合当中的内容是不重复的
5. 实现Set接口常用常用的类有TreeSet和HashSet,还有一个LInkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录插入的次序.
6. Set中的key不能修改,如果要修改,先将原来的删除掉,在重新插入
7. TreeSet中不能插入null的key,HashSet可以
哈希表
在我们已知的搜索方法中,在查找一个元素的时候,必须要经过关键码的多次比较.顺序查找的时间复杂度为O(N), 平衡树中时间复杂度为树的高度,即O(log₂N), 在理想的搜索方法中可以不经过任何比较,一次直接从表中得到要搜索的元素,通过构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码建立一 一映射关系,在查找时通过该函数可以很快找到该元素.在该结构中:
插入元素: 根据待插入元素的关键码,从此函数计算出该元素的存储位置并按此位置存放
搜索元素: 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,按此位置取元素比较,若关键码相等,则搜搜成功.
该方式即为哈希方法,哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表
例如将数据集合{1 , 4 , 6 , 5 , 7 , 9} 存储到哈希表中
设置哈希函数为 : hash(key) = key % m; m为存储元素底层空间的大小. m = 10;
用关键码 % 存储空间的大小 得到的余数为该元素的存储位置.
当我们将数据集合 { 4, 14} 用哈希函数 hash(key) = key % m,(m = 10)时,得到的余数都是4则存储到了相同的位置,就发生了冲突.
冲突
不同关键字通过相同的哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或哈希碰撞
冲突时必然发生的,我们要设计合理的哈希函数降低冲突率.
如何避免冲突
1.选取合适的哈希函数
● 直接定址法
取关键字的某个线性函数值为哈希地址,即 :
Hash(key) = a * key + b ( a , b为常数)
例如有一个关键字集合{200, 300, 500, 700,},选取哈希函数hash(key) = key / 100,假设表长为8,则哈希表如下:
直接定址法的特点是哈希函数简单,并且对于不同的关键字不会产生冲突,但也有缺点,需要事先知道关键字的分布情况,适合查找比较小且连续的情况.
● 除留余数法
设散列表中的地址数为m, 取一个不大于m但最接近或者等于m的质数p作为除数,取余数作为哈希地址,即 : hash(key) = key % p (p <= m)
2. 调节负载因子
负载因子 : α = 填入表中的元素个数 / 散列表的长度
负载因子和冲突率的关系
负载因子越大,冲突率越高
在已知哈希表中,关键字的个数是不可变的,要改变冲突率就只能改变哈希表中数组的大小
1. 开放定址法
当发生冲突时,形成一个地址序列,沿着这个序列逐个探测,直到找到一个空的开放地址,将发生冲突的关键码值存放到该地址中去.
例如将数据集合{1 , 4 , 6 , 5 , 7 , 9, 44} 存储到哈希表中
设置哈希函数为 : hash(key) = key % m; m为存储元素底层空间的大小. m = 10;
当存储44的时候发生冲突,从4位置往后逐个探测,在8位置位空,将44存放到里面.若整个空间都找遍仍然找不到空余的地址,则产生溢出.
线性探测的缺陷是产生冲突的元素都堆积到一起,二次探测法就避免了这种问题
二次探测法
二次探测法和开放定址法的方法类似,只是设置的增量序列不同
增量序列为: di = 1² , -1², 2², -2², ... , q², -q² (q <= m / 2)
哈希函数 : Hi = (H0 + i²) % m;
2 链地址法
将所有具有相同哈希地址的不同关键字的数据元素链接到同一个单链表中.
每一个关键字都相当于一个节点.每一个哈希地址都相当于一个单链表
用代码实现一个简单的哈希桶
public class HashBuck {
static class Node {
public int key;
public int val;
public Node next;
public Node(int key,int val) {
this.key = key;
this.val = val;
}
}
public Node[] array;
public int usedSize;
//负载因子
public static final double LOAD_FACTOR = 0.75;
public HashBuck() {
array = new Node[8];
}
public void put(int key,int val) {
int index = key % array.length;
Node cur = array[index];
while(cur != null) {
if(cur.key == key) {
cur.val = val;
return;
}
cur = cur.next;
}
//头插法
Node node = new Node(key,val);
node.next = array[index];
array[index] = node;
usedSize++;
if(calculateLoadFActor() >= LOAD_FACTOR) {
//扩容 每个元素都要进行重新的哈希
resize();
}
}
private void resize() {
Node[] newArray = new Node[2 * array.length];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while(cur != null) {
Node curNext = cur.next;
int index = cur.key % newArray.length;
cur.next = newArray[index];
newArray[index] = cur;
cur = curNext;
}
}
array = newArray;
}
//计算负载因子
private double calculateLoadFActor() {
return usedSize*1.0 / array.length;
}
public int get(int key) {
int index = key % array.length;
Node cur = array[index];
while(cur != null) {
if(cur.key == key) {
return cur.val;
}
cur = cur.next;
}
return -1;
}
}
当用自定义类作为hashMap的值或者hashSet的值.要重写hashCode()和equals()方法.将自定义类型变成整型.而且要做到equals相等的对象.hashCode一定是一致的.
面试问题:
hashCode一样,equals一定一样吗?
hashCode一样,只能说明哈希地址相同,而我们每一个哈希地址是一个单链表,里面存放了很多数据,所以equals不一定相同
equals一样,hashCode一定一样吗?
equals一样,说明数据相同,则存放在相同的哈希地址,hashCode一定相同