一、哈希表
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法: 可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
哈希表依赖了数组下标的随机的访问能力。
设计哈希表的过程中,如何把key转成数组下标,转换的具体过程/公式 称之为 哈希函数。
针对哈希表的查找操作时间复杂度:O(1).
例如有这么一个集合:{1,7,6,4,5,9};
通过一个数学函数把key映射到下标。
2.哈希冲突
概念:两个key经过同一个hash算法得到的下标相同,意味着两个元素要放置的位置出现冲突。
本质原因:把一个比较大范围的元素映射到范围比较小的元素。hash冲突是客观存在的。
尽量避免哈希冲突
- 使用素数长度 capacity ,降低冲突的概率
- 线性探测(闭散列)
2.1 针对key计算hash值
2.2 需要比较key是否相等(支持equals)。
3.开散列(哈希桶)
3.开散列(哈希桶解决冲突)
一个下标对应多种元素开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
常见哈希函数
1.直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
2.除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
字符串hash算法:MD5,SHA1.
- 字符串原串有多长,最终得到的md5值都是固定长度的(64位/128位).
- 原字符串只要有一点点变化,得到的md5值就会变化很多
- 根据原串计算md5很容易,反之很难。
- 补充:md5还可以作为校验和,验证本地的文件和服务器的文件一模一样。服务器的文件计算一个md5值,本地的文件计算一个md5,根据2
负载因子
散列表负载因子定义为:哈希表中元素个数/capacity(哈希表的capacity)
负载因子表示哈希表的拥挤程度,随着负载因子越大,冲突也就越大
性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1)
实现
package package1124;
public class MyHashMap {
public static class Node{
public int key;
public int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array = new Node[101];//哈希桶
private int size = 0;
private static final double FACTOR = 0.75;//负载因子
//3个核心操作,增删查,不能改key,破坏数据结构
//1.插入键值对,put
public void put(int key,int value){
//1.根据key调用hash函数获取下标
int index = key % array.length;
//2.根据下标找到对应的链表
Node head = array[index];
//3.判断一下当前的key是不是已经存在
for(Node cur = head;cur != null;cur = cur.next){
if (cur.key == key){
//如果这个key已经存在,就不插入新节点,修改旧节点的value
cur.value = value;
return;
}
}
//4.把新的节点插入到链表中,头插的效率比较高
Node newNode = new Node(key,value);
newNode.next = array[index];
array[index] = newNode;
size++;
//5.进行扩容的判定
if ((double)size / array.length > FACTOR){
//浮点数不能比较相等
resize();
}
}
private void resize(){
Node[] newArray = new Node[array.length*2 + 1];
//遍历原来哈希表的每个元素,把元素插入到新的Array中
for (int i = 0; i < array.length; i++) {
//取到每个链表,遍历链表
for (Node cur = array[i];cur != null;cur = cur.next){
//capacity变了得到的下标是不一样的
Node newNode = new Node(cur.key,cur.value);
int index = cur.key % newArray.length;
newNode.next = newArray[index];
newArray[index] = newNode;
}
}
array = newArray;
}
//2.查找元素,get
public Node get(int key){
//1.根据key计算下标
int index = key % array.length;
//2.根据下标找到对应链表的头节点
Node head = array[index];
//3.在链表上遍历找到对应的key
for (Node cur = head;head != null;cur = cur.next){
if (cur.key == key){
return cur;
}
}
return null;
}
//3.删除元素,remove
public void remove(int key){
//1.根据key找到下标
int index = key % array.length;
//根据下标找到对应的链表
Node head = array[index];
//在链表查找到对应的key并删除既可
if(key == head.key){
//删除的是头节点
array[index] = head.next;
size--;
return;
}
//找到该节点的前一个节点
Node prev = head;
while (prev != null && prev.next != null){
if(prev.next.key == key){
//找到当前元素的前一个元素
prev.next = prev.next.next;
size--;
break;
}
}
}
}
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
哈希编程思想
哈希不仅仅是简单的数据结构,还是重要的编程思想
给定一个很大的文件(内存肯定放不下)
文件里面有很多ip地址
随便给一个ip,问你这个ip是否在文件中存在
这类问题就是哈希切分的思想。
我们可以把文件切分成很多部分(这里取100,在内存中存得下),遍历文件,针对每次取出的一个ip地址,计算一个hash值,(和100取余数)
如果这个ip求余数之后结果是0,把这个记录写到0号文件中
如果余数1,就把记录写到1号文件中(往文件里面一写)
……
得到100个文件
如果随便拿一个ip是否存在,求这个ip的模数,如果模是5,就在五号文件找,把五号文件加载到内存中通过哈希表的方式取找。