哈希表就是借鉴了数组的随机访问能力,利用数组的随机访问的高效性产生了哈希表
哈希函数其实就是把任意的数据类型转成数组的索引
e.g. char --> int char - 'a' = int
大的int类型转为小的int类型 一般都是取模操作,根据数论摸一个素数
String --> int 字符串的内部就是字符数组,因此还是按照字符转为int的方式
其他类型 -- > int 因为任意的数据类型都有toString方法,所以可以先转为String --> int
不同的两个key值,经过哈希函数的运算得到了两个相同的索引。
在理论上,数学中的任意函数f(x),两个不相同的x都一定有可能会映射到相同的y,哈希冲突无法避免!
当发生冲突时,找到冲突位置的旁边是否存在空闲位置,直到找到第一个空闲位置放入元素。
好存难查更难删,工程中很少使用此方案
图解:
查找:
要查找120时,先120 %101 = 19,发现19这个位置存放的不是120,则继续向后找,直到找到120位置,若一直向后遍历走完整个数组还没找到,则这个元素不存在。
若整个哈希表冲突非常严重,此时查找一个元素从O(1)=>遍历数组O(n)。所以一般不用
查找任意元素:
如果取模后若冲突,遍历链表。 在最坏情况下,开散列方案遍历链表,相较于闭散列方案查找次数会大大降低,元素删除,查找,链表的对应操作,相较于闭散列方案来说容易很多。
若遇见最最最坏的情况,若当前哈希表中某个位置,比如图中19这个位置冲突非常严重,恰好每个元素取模后都是19,某个数组对应的链表的长度过长,查找效率就会降低。面对这种情况一般有两种解决方案:
1.针对整个数组进行扩容(比如现在数组长度101,扩容到202)就会由原先%101 => %202,很多原来同一个链表上的元素均分到其他新的位置,降低哈希冲突。 e.g. C++中的STL的Map
2.将这个冲突严重的链表再次变为新的哈希表/二分搜索树(将O(n) => O(logn))不用整张哈希表进行处理,只处理冲突严重的链表。 e.g. JDK的方案
第二种方法如图:
练习题:
已知一个线性表(38,25,74,63,52,48),假定采用散列函数h (key) = key%7计算散列地址,并散列存储在散列表A[...6]中,若采用线性探测方法解决冲突,则在该散列表上进行等概率成功查找的平均查找长度为(C)
A.1.5 B.1.7 C.2.0 D.2.3
解:
成功查找的平均查找长度为:当前表中所有元素的查找次数 / 表中有效的元素个数 --> 12/6 =2
拓展:开散列时:
小结:
对于一般场景下的哈希函数的设计:
一般来说不用自己写,用现成的即可。MD5,MD4,MD3 SHA1,SHA256 MD5—般用在字符串计算hash值的特点:
1.定长。无论输入的数据有多长,得到的MD5值长度固定。
2.分散。如果输入的数据稍有偏差,得到的MD5值相差很大(冲突概率非常低,工程领域忽略不计)
3.不可逆。根据字符串计算md5很容易,想通过得到的md5还原字符串到底是啥非常难(基本不可能)
4.根据相同的数据计算的md5值稳定的,不会发生变化。 稳定性是所有哈希函数都要满足的特点
MD5用途:
1.作为hash运算
⒉.用于加密
3.对比文件内容(内容稍有修改,得到的md5值天差地别)。上传下载。
自己设计一个开散列代码:
完整代码:
- import java.util.NoSuchElementException;
-
- /**
- * @Author qiqichongya
- * @Date 2022/8/14 19:20
- * @PackageName:HashMap
- * @ClassName: HashMapTest
- * @Description: 自己设计一个 开散列
- */
- public class MyHashMap {
- // 有效节点数
- private int size;
- // 实际存储元素的 Node 数组
- private Node[] hashTable;
- // 取模数
- private int M;
- // 负载因子
- private static final double LOAD_FACTOR = 0.75;
-
-
- public MyHashMap() {
- this(16);
- }
-
- public MyHashMap(int init) {
- this.hashTable = new Node[init];
- this.M = init;
- }
-
- /**
- * 对key求哈希值
- *
- * @param key
- * @return
- */
- public int hash(int key) {
- return Math.abs(key) % M;
- }
-
- /**
- * 将一对
- *
- * @param key
- * @param val
- */
- public int put(int key, int val) {
- // 1.先取模找到key对应的Node数组中的索引下标。
- int index = hash(key);
- // 2.遍历对应索引下标的链表,查询key是否已经存在
- for (Node x = hashTable[index]; x != null; x = x.next) {
- if (x.key == key) {
- int oldValue = x.value;
- x.value = val;
- return oldValue;
- }
- }
- // 此时说明该键值对是一个新的,需要新增节点头插到哈希表中
- // 也就是说 此时 Node x == null --> hashTable[index] == null --> 还没有放值。
- Node node = new Node(key, val);
- node.next = hashTable[index];
- hashTable[index] = node;
- size++;
- if (size >= hashTable.length * LOAD_FACTOR) {
- expansion();
- }
- return val;
- }
-
- /**
- * 数组扩容为原来的二倍 和 搬移数据
- */
- private void expansion() {
- // 1.产生一个新的数组并且长度是原来长度的二倍。
- Node[] newTable = new Node[hashTable.length << 1];
- // 2.进行元素的搬移操作,此时取模应该为新的数组长度
- this.M = newTable.length;
- for (int i = 0; i < hashTable.length; i++) {
- for (Node x = hashTable[i]; x != null; ) {
- // 求出 x 在新的数组中的索引下标
- int index = hash(x.key);
- Node next = x.next;
- // 在新数组中进行头插
- x.next = newTable[index];
- newTable[index] = x;
- // 继续搬移剩下的链表节点。
- x = next;
- }
- }
- }
-
- /**
- * 判断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 value
- * @return
- */
- public boolean containsValue(int value) {
- // 全表扫描
- for (int i = 0; i < hashTable.length; i++) {
- for (Node x = hashTable[i]; x != null; x = x.next) {
- if (x.value == value) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * 在哈希表删除指定键值对(key,value)
- *
- * @param key
- * @param value
- * @return
- */
- public boolean remove(int key, int value) {
- // 先拿到当前 key 在数组中的索引
- int index = hash(key);
- // 判断头节点是不是待删除节点
- Node head = hashTable[index];
- if (head.key == key && head.value == value) {
- // 此时头节点就是待删除节点
- hashTable[index] = head.next;
- size--;
- return true;
- }
- // 此时头节点不是待删除节点
- Node prev = hashTable[index];
- while (prev.next != null) {
- if (prev.next.key == key && prev.next.value == value) {
- // 此时当前节点是待删除节点的前驱节点
- Node current = prev.next;
- prev.next = current.next;
- size--;
- return true;
- } else {
- prev = prev.next;
- }
- }
- // 哈希表中没有这个节点
- throw new NoSuchElementException("no such node!remove error");
- }
- }
-
- 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
- * @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 value
- * @return
- */
- public boolean containsValue(int value) {
- // 全表扫描
- for (int i = 0; i < hashTable.length; i++) {
- for (Node x = hashTable[i]; x != null; x = x.next) {
- if (x.value == value) {
- return true;
- }
- }
- }
- return false;
- }
哈希表删除指定键值对:
- /**
- * 在哈希表删除指定键值对(key,value)
- *
- * @param key
- * @param value
- * @return
- */
- public boolean remove(int key, int value) {
- // 先拿到当前 key 在数组中的索引
- int index = hash(key);
- // 判断头节点是不是待删除节点
- Node head = hashTable[index];
- if (head.key == key && head.value == value) {
- // 此时头节点就是待删除节点
- hashTable[index] = head.next;
- size--;
- return true;
- }
- // 此时头节点不是待删除节点
- Node prev = hashTable[index];
- while (prev.next != null) {
- if (prev.next.key == key && prev.next.value == value) {
- // 此时当前节点是待删除节点的前驱节点
- Node current = prev.next;
- prev.next = current.next;
- size--;
- return true;
- } else {
- prev = prev.next;
- }
- }
- // 哈希表中没有这个节点
- throw new NoSuchElementException("no such node!remove error");
- }