目录
概念
模型
Map
Map的常用方法
对于Map的总结
Set
Set的常见方法
关于Set的总结
哈希表
概念
冲突
概念
哈希函数设计原则
常见的哈希函数
1.直接定制法(常用)
2.除留余数法(常用)
3.平方取中法
4.折叠法
5.随机数法
6.数学分析法
冲突避免-负载因子调节
冲突-解决
闭散列
线性探测
二次探测
开散列/哈希桶
三个例题理解Map和Set
复制带随机指针的链表
Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关,比如TreeMap和TreeSet的效率一般是不如HashMap和HashSet的.
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称为Key-value的键值对,所以对应的模型会有两种:纯Key模型和Key-Value模型.
Map中存储的就是key-value模型,Set中只存储了Key.
Map是一个接口类,该类没有继承自Collection,该类中存储的是
TreeMap实现了SortedMap接口,所以TreeMap中的元素一定是可比较的.
方法演示
TreeMap map = new TreeMap<>();
map.put("hello",2);
map.put("abc",4);
//返回key对应的value
//如果map中没有hello,会返回null
Integer e =map.get("hello");
System.out.println(e);
//有key返回对应值,没有返回默认值
Integer s = map.getOrDefault("hello2",520);
System.out.println(s);
//取出key值,进行组织
//用Set接收
Set set = map.keySet();
System.out.println(set);
//取出value值,进行组织
//用Collection接收
Collection collection = map.values();
System.out.println(collection);
//containsKey,判断是否包含key
boolean t = map.containsKey("hello");
System.out.println(t);
//containsValue,判断是否包含value
boolean r = map.containsValue("88");
System.out.println(r);
entrySet方法
Map.Entry
Set> entrySet = map.entrySet();
for (Map.Entry entry : entrySet) {
System.out.println("Key: " + entry.getKey() +" value: " + entry.getValue());
}
此方法就是将<"hello",2>作为一个整体存放在set当中,这个整体的类型Map.Entry
由于Map并没有实现Iterable接口,所以for-each无法直接遍历Map.
这个方法相当于是提供了遍历Map的方法.
Set 与Map的不同主要有两点:Set是继承自Collection的接口类,Set中只存储了Key.
在TreeSet当中存储元素的时候,其实是存在了TreeMap当中,但是value是默认的一个值.
不管存哪个key,value永远是PRESENT这个值.
以往,我们在一些记录当中查找指定数据的时候,会有以下几种方法
现在,有一种可以将时间复杂度做到O(1)的结构,那就是哈希表.
不经过任何比较,一次直接从表中得到要搜索的元素.如果构造一种存储结构,通过某种函数使元素的存储位置和它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快的找到该元素.
该港是即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表(HashTable)(散列表).
例如:数据集合{1,7,6,4,5,9};
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快.但是,如果以此方式,向集合里插入14,会出现位置占用的问题,这就出现了冲突.
不同关键字通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或者哈希碰撞.
把具有不同关键码而具有相同哈希地址的数据元素称为"同义词".
冲突避免-巧妙设计哈希函数
我们要明确,由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致冲突的发生是必然的,我们能做的应该是尽量的降低冲突率.
引起哈希冲突的一个原因可能是:哈希函数设计不合理.
设计原则:
class Solution {
public int firstUniqChar(String s) {
int[] array = new int[26];
for(int i = 0; i < s.length();i++){
char ch = s.charAt(i);
array[ch-'a']++;
}
for(int i = 0; i < s.length();i++){
char ch = s.charAt(i);
if(array[ch-'a'] == 1){
return i;
}
}
return -1;
}
}
散列表的载荷因子定义为:α =填入表中的元素个数/散列表的长度.
α 是散列表装满程度的标志因子.由于表长是定值,α 与填入表中的元素个数成正比,α 越大,表明填入表中的元素越多,产生冲突的可能性就越大.
负载因子和冲突率的关系演示
当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率.
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组大小.
α 与哈希表数组的长度是成反比的.
解决哈希冲突的两种常见方式:闭散列和开散列.
闭散列,也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么就可以把key存放到冲突位置的"下一个"空位置去.
寻找下一个空位置有两种方法:线性探测法和二次探测法.
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止.
比如要插入44,先通过哈希函数获取待插入元素在哈希表中的位置,如果该位置没有元素就直接插入新元素,显然这里4占据了位置;此时元素发生哈希冲突,则使用线性探测法找到下一个空位置,下标为8的位置,插入44.
需要注意的是,采用此种方法处理哈希冲突的时候,不能随便物理删除哈希表中已有的元素,若直接删除会影响其他元素的搜索.比如删除4,如果直接删除4,那么对查找44就会产生影响.因此线性探测采用标记的伪删除法来删除一个元素.
开散列法又叫做链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表连接起来,各链表的头节点存储在哈希表中.
开散列的每个桶中放的都是发生哈希冲突的元素.开散列可以认为是把一个在大集合中的搜索问题转化为在小集合中搜索了.
开散列采取数组+链表的方式来组织数据.当数组长度超过64并且链表长度超过8的时候,链表就会变为红黑树.
JDK1.7及以前,链表的插入采取的是头插法,JDK1.8开始采用尾插法.
开散列法也是Java中用来解决哈希冲突所采取的方法.
//统计10w个数据中,不重复的数据(去重)
public static void func1(int[] array) {
HashSet set = new HashSet<>();
for (int i = 0; i < array.length; i++) {
set.add(array[i]);
}
System.out.println(set);
}
//2、统计10W个数据当中,第一个重复的数据?
public static void func2(int[] array){
HashSet set = new HashSet<>();
for (int i = 0; i < array.length; i++) {
if (!set.contains(array[i])){
set.add(array[i]);
}else {
System.out.println(array[i]);
return;
}
}
}
//3、统计10W个数据当中,每个数据出现的次数? 对应的关系
public static void func3(int[] array){
HashMap map = new HashMap<>();
for (int i = 0; i < array.length; i++) {
//第一次存入
if (map.get(array[i]) == null){
map.put(array[i],1);
}else {
int val = map.get(array[i]);
map.put(array[i],val+1);
}
}
Set> set = map.entrySet();
for (Map.Entry entry:set) {
System.out.println(entry.getKey() + "出现了" + entry.getValue()+"次");
}
}
用Map去做,会变得非常容易.
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
//用Map存储新老节点的对应关系
HashMap map = new HashMap<>();
Node cur = head;
while(cur != null){
Node node = new Node(cur.val);
map.put(cur,node);
cur = cur.next;
}
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);
}
}
两个对象的hashcode一样,equals不一定一样.
两个对象的equals一样,hashcode一定一样.