目录

1.哈希表的起源?

2.哈希函数(hashcode())

3.哈希冲突

4.负载因子

5.基于开散列的哈希表代码实现


1.哈希表的起源?

首先我们来分析一下从一组数据中查找某个元素的效率:

  • 如果顺序查找,则需要从头开始查找,时间复杂度为O(N);
  • 即使用刚学的BST进行查找,时间复杂度也为O(log N);

那么有没有一种方法使查找效率更高呢?

  • 如下面这个例子:

给定一组数据:【6,5,2,4,7】,求该组数据中是否有元素4

我们的一种思路是这样的:

  • 该组数据中最大的元素为7,那我们就创建一个长度为8的boolean数组temp;
  • 原数据中的数字为多少,我们就令temp数组中索引下标为这个数的元素置为true;

(如原数据中不是有数字6吗,就令temp[6] = true)

  • 最后,查找哪个元素是否存在就直接返回temp数组中该索引下标位置即可

(如本题中查找数字4,直接返回temp[4]即可)

    public static void main(String[] args) {
//        【6,5,2,4,7】int[] a = {6,5,2,4,7};boolean[] temp = new boolean[8];for (int i = 0; i < a.length; i++) {temp[a[i]] = true;}
//        查找4System.out.println(temp[4]);
//        查找1System.out.println(temp[1]);}

输出结果:

这道题的解法就是巧妙借助了数组的索引下标,直接返回某元素对应的索引下标值即可;

于是我们想到构造一种存储结构,使元素的存储位置与它的关键码之间能够建立一一映射的关系,这样依据存储位置直接便查找到了该元素。

  • 所以我们有了哈希表这种结构,而哈希表实则就是由数组衍生而来的,借助数组的随机访问性,由索引下标即可快速查找,使得哈希表的查找操作时间复杂度可为O(1),大大提高了效率
  • 同时也可以看到,其实哈希表是用空间换时间
  • 所以哈希表由键值对构成,key和value,如若查找某值,直接返回key值对应的value值即可

2.哈希函数(hashcode())

(1)对于元素分散的数据,如 【1,100,239,3】,如果还是直接元素数据对应索引下标必然会带来很多空间浪费,或者诸如其他非数字类型,故而,有了哈希函数的产生。

(2)哈希函数就是将任意数据类型进行函数运算后使之变为整型,也就是在原数据和数据索引之间建立映射关系。

(3)哈希函数最常用的运算之一就是模运算,对于负数,通常是取绝对值再进行模运算

(4)哈希函数设计原则:

  1. 使运算结果分布尽可能均匀(一般是对一个素数取模)
  2. 稳定性,相同的Key值要保证运算结果为唯一的一个值

3.哈希冲突

(1)哈希冲突

有时候不同的key值,进行哈希运算后,可能得到一个相同的值,这就发生冲突了,也就是哈希冲突

(2)哈希冲突的解决措施

闭散列(开放地址法):当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去

那么下一个空位置如何找呢?

  • 线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
  • 二次探测法:左右跳跃式探测

【注意】对于线性探测法,若哈希冲突严重,则其实查找某元素时仍然相当于是遍历操作,效果并不好

开散列:又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

如下图:

 闭散列相当于化大集合为小集合,到小集合里去查找;

而如果哈希冲突严重时,就意味着小集合的搜索性能其实也时不佳的,这个时候我们可以继续优化:

  • 方法一:适当进行哈希表扩容
  • 方法二:将小集合的链表优化为一个新的哈希表
  • 方法三:将小集合的链表优化为一个红黑树(JDK8的HashMap就采用这种方法,当整个哈希表的长度>64并且链表长度>6,就将链表转化为红黑树

4.负载因子

  • 负载因子越大,说明填入表中元素越多,发生冲突可能性越大!
  • 负载因子越小,说明填入表中元素越少,发生冲突可能性越小
  • JDK8中,HashMap中的负载因子为0.75

5.基于开散列的哈希表代码实现

以下实现的哈希表,为基于开散列的哈希表,每个哈希桶由单链表连接起来,当负载因子大于预定默认范围时,我们暂且选择扩容操作来减缓哈希冲突

实际就是下图这种表现形式,数组的每个位置存储的其实是链表的头节点,每一个节点由key值,value值,和下一个节点引用next组成

(1)插入键值对——put操作

(在插入时要考虑是否需要扩容,故而这里还有一个扩容方法——grow操作)

先判断是否已经存储过该key值,去该key值哈希运算后对应的索引位置里的链表里查找;

如果已经存储过,只需修改value值;

如果未存储过,则需创建新节点,并头插入对应索引位置的链表中去;

    /*** 添加键值对操作* @param key* @param value* @return*/public int put(int key, int value) {int index = hashCode(key);
//        先查看是否已经存储过该key值,如果存储过,只需更新val值for (Node cur = data[index];cur != null;cur = cur.next) {if(cur.key == key){int old = cur.val;cur.val = value;return old;}}
//        此时,说明无该key值,则我们需要新建节点插入Node newNode = new Node(key,value,data[index]);data[index] = newNode;size ++;
//        查看是否需要扩容if(data.length * LOAD_FACTOR <= size){grow();}return newNode.val;}//    扩容操作,默认扩大为2倍private void grow() {Node[] newData = new Node[data.length * 2];
//            随即将取模数k更改k = newData.length - 1;
//        遍历原数组,将原数组重新哈希函数后搬入扩容后的新数组for (int i = 0; i < data.length; i++) {
//            将不为空的节点处的所有元素搬迁if(data[i] != null){for(Node cur = data[i];cur != null;){Node next = cur.next;int newIndex = hashCode(cur.key);
//                    将元素头插入新数组newIndex处cur.next = newData[newIndex];data[newIndex] = cur;
//                    然后继续搬迁下一个元素cur = next;}}else{continue;}}
//        最后将newData赋值给原来的datadata = newData;}

(2)删除(根据给定的key值删除)——remove操作

找到key值对应的哈希位置,从该位置的链表里删除,实则就是链表的删除节点操作;

如果找不到该键值对,抛出异常即可;

    /*** 根据key值删除键值对* @param key* @return*/public int remove(int key) {int index = hashCode(key);
//        先判断头节点是否就是该元素Node head = data[index];if (head.key == key) {int v = head.val;data[index] = head.next;head = head.next = null;size--;return v;} else {
//            此时说明不是头节点Node pre = head;while (pre.next != null) {if (pre.next.key == key) {Node cur = pre.next;int v = cur.val;pre.next = cur.next;cur.next = cur = null;size--;return v;}}//                如果都没找到,说明无该键值对throw new NoSuchElementException("cannot find key!!!");}}

(3)查找某key值是否存在——containsKey操作

同上,也是先计算key值对应的哈希值,哈希值就是存储位置的索引下标,然后根据索引下标去链表里找;

    /*** 查看是否存在某key值,实则就是在对应链表里查!* @param key* @return*/public boolean containsKey(int key) {int k = hashCode(key);Node head = data[k];while (head != null) {if (head.key == key) {return true;}head = head.next;}return false;}

(4)查找某value值是否存在——containsVal操作

查找val值,因为无法得知其key值对应的哈希值,也就无法直接访问该节点,所以,这里,效率低,只能逐个访问查找

    /*** 查看是否存在某val值* @param value* @return*/public boolean containsVal(int value) {
//            遍历每一个链表for (int i = 0; i < data.length; i++) {for (Node x = data[i]; x != null; x = x.next) {if (x.val == value) {return true;}}}return false;}

最后给上完整代码,并进行Test

import java.util.NoSuchElementException;/*** 基于开散列的哈希表的实现*/
public class My_Hashmap {//    node节点private class Node {private int key;private int val;private Node next;public Node() {this.val = 0;this.key = 0;}public Node(int key, int val, Node next) {this.key = key;this.val = val;this.next = next;}}//    size存储空间元素个数private int size;//        默认空间大小为10private static final int DEFAULT_CAPACITY = 10;//        默认负载因子的大小private static final double LOAD_FACTOR = 0.75;//        实际存储的数组private Node[] data;//        取模数,这里定义是因为扩容后取模数也会变private int k;//        无参构造、有参构造public My_Hashmap() {this(DEFAULT_CAPACITY);}public My_Hashmap(int length) {data = new Node[length];k = length - 1;}//        哈希函数,简单进行一个取模运算private int hashCode(int key) {return Math.abs(key) % k;}/*** 添加键值对操作* @param key* @param value* @return*/public int put(int key, int value) {int index = hashCode(key);
//        先查看是否已经存储过该key值,如果存储过,只需更新val值for (Node cur = data[index];cur != null;cur = cur.next) {if(cur.key == key){int old = cur.val;cur.val = value;return old;}}
//        此时,说明无该key值,则我们需要新建节点插入Node newNode = new Node(key,value,data[index]);data[index] = newNode;size ++;
//        查看是否需要扩容if(data.length * LOAD_FACTOR <= size){grow();}return newNode.val;}/*** 根据key值删除键值对* @param key* @return*/public int remove(int key) {int index = hashCode(key);
//        先判断头节点是否就是该元素Node head = data[index];if (head.key == key) {int v = head.val;data[index] = head.next;head = head.next = null;size--;return v;} else {
//            此时说明不是头节点Node pre = head;while (pre.next != null) {if (pre.next.key == key) {Node cur = pre.next;int v = cur.val;pre.next = cur.next;cur.next = cur = null;size--;return v;}}//                如果都没找到,说明无该键值对throw new NoSuchElementException("cannot find key!!!");}}/*** 查看是否存在某key值,实则就是在对应链表里查!* @param key* @return*/public boolean containsKey(int key) {int k = hashCode(key);Node head = data[k];while (head != null) {if (head.key == key) {return true;}head = head.next;}return false;}/*** 查看是否存在某val值* @param value* @return*/public boolean containsVal(int value) {
//            遍历每一个链表for (int i = 0; i < data.length; i++) {for (Node x = data[i]; x != null; x = x.next) {if (x.val == value) {return true;}}}return false;}//    扩容操作,默认扩大为2倍private void grow() {Node[] newData = new Node[data.length * 2];
//            随即将取模数k更改k = newData.length - 1;
//        遍历原数组,将原数组重新哈希函数后搬入扩容后的新数组for (int i = 0; i < data.length; i++) {
//            将不为空的节点处的所有元素搬迁if(data[i] != null){for(Node cur = data[i];cur != null;){Node next = cur.next;int newIndex = hashCode(cur.key);
//                    将元素头插入新数组newIndex处cur.next = newData[newIndex];data[newIndex] = cur;
//                    然后继续搬迁下一个元素cur = next;}}else{continue;}}
//        最后将newData赋值给原来的datadata = newData;}}

对增加、删除、查找进行Test:

public class Test {public static void main(String[] args) {My_Hashmap map = new My_Hashmap(15);map.put(3,6);map.put(4,7);map.put(5,8);map.put(4,9);boolean a = map.containsKey(3);boolean b = map.containsKey(8);boolean c = map.containsVal(9);boolean d = map.containsVal(7);map.remove(3);System.out.println(a);System.out.println(b);System.out.println(c);System.out.println(d);boolean e = map.containsKey(3);System.out.println(e);}
}

测试结果:

可见,我们的方法操作是成功的,并且如若重复插入同一个key值,只会更新val值,而不会说会有两个key值相同的节点。

【注意】jdk8中,当哈希冲突严重时,会将链表改为红黑树,以保持查找效率的平衡性

哈希表(HashMap)的学习与实现相关推荐

  1. Hash+哈希表+HashMap+HashSet

    Hash+哈希表+HashMap+HashSet 哈希算法,是一类「算法」. 哈希表(Hash Table),是一种「数据结构」. 哈希函数,是支撑哈希表的一类「函数」. Map是映射/地图的意思,在 ...

  2. hashmap是散列表吗_一篇文章教你读懂哈希表-HashMap

    题图Pid=68670770 在最近的学习过程中,发现身边很多朋友对哈希表的原理和应用场景不甚了解,处于会用但不知道什么时候该用的状态,所以我找出了刚学习Java时写的HashMap实现,并以此为基础 ...

  3. [剑指offer]面试题第[50]题[JAVA][第一个只出现一次的字符][哈希表][HashMap]

    [问题描述][简单] 在字符串 s 中找出第一个只出现一次的字符.如果没有,返回一个单空格. s 只包含小写字母.示例:s = "abaccdeff" 返回 "b&quo ...

  4. java中哈希表HashMap详解遍历操作

    一.主题外谈 在进入主题之前,先讲一点与哈希表相关的一些东西: 1.Hash算法, 通常还可用作快速查找. 2.哈希函数,是支撑哈希表的一类「函数」. 3.哈希表(Hash Table),是一种**「 ...

  5. Java算法学习1——链表、数组与哈希表

    一.链表与哈希表的知识点讲解 1.链表 (1) 链表的实现方式 链表的实现方式有两种,分别为指针和数组,下面对这两部分的具体实现步骤做一个说明. - 指针实现 ①先要建立一个结点结构体 ②然后建立一个 ...

  6. 【转载】哈希表的原理,真的很难弄懂么?

    [转载]哈希表的原理,真的很难弄懂么? 刘小爱v 发布时间:05-0909:06科技达人 转载路径: https://baijiahao.baidu.com/s?id=1666172942887109 ...

  7. 左神算法课笔记(二):链表、栈和队列、递归Master公式、哈希表、有序表

    单向链表 双向链表 单链表.双链表最简单的面试题 1.单链表和双链表如何反转 package class02;import java.util.ArrayList;public class Code0 ...

  8. 【LeetCode笔记】560. 和为K的子数组(Java、前缀和、哈希表)

    文章目录 题目描述 思路 & 代码 暴力法 O(n2n^2n2) 前缀和 + 哈希表 O(n) 二刷 题目描述 第一道前缀和题目- 思路 & 代码 暴力法 O(n2n^2n2) 固定一 ...

  9. 【数据结构与算法】五、哈希表和链表

    前言: 大家好,我是春风 今天继续刷左神的算法视频,前面刷完了入门的查找和排序算法,也对排序算法做了一些总结. 现在开始刷结构部分,第一个结构是哈希表,然后是链表.哈希表的应用在Java中有现成的ma ...

  10. C++八股文分享---数据结构其二---哈希表

    C++八股文分享-数据结构其二-哈希表 前言 什么是哈希表? 搜索二叉树对值的查找是通过从根节点开始,逐个节点与目标值做比较,向下查找,直至找到目标值或是到达根节点未查找到,时间复杂度为O(logn) ...

最新文章

  1. 新算法可模拟人脑整体神经电路
  2. [VS2005]一个简单的CodeSnippet —— 生成类的构造函数。
  3. 搭建基础架构-Order
  4. 地理数据库 (geodatabase) 的架构
  5. Android 性能优化——之图片的优化
  6. 机械制图手册_机械制图基本知识大全!
  7. VB小技巧 文本框中屏蔽系统右键菜单用自定义菜单代替自带右键菜单
  8. .NET与COM互操作系列
  9. 高德h5地图api接口_在uniapp h5中在线引入高德地图js api的方法分享
  10. [置顶]常用存储过程集锦
  11. 你看那个人他像一条狗
  12. kali下破解小区门禁卡实例
  13. python画圆及其内接多边形_用Shapely求多边形的最大内接矩形
  14. matlab求含参数一元三次方程,matlab 求解一元三次方程,带其他参数
  15. I2C中的10bit地址读写
  16. 了解git裸仓库并利用post-receive自动化部署
  17. itchat4j -- 用Java扩展个人微信号的能力
  18. Python:1020 月饼
  19. 阿里P8亲自教你!Android高级工程师进阶学习,全套教学资料
  20. C语言函数指针与NULL

热门文章

  1. 科技作者吴军:不用低效率的算法做事情
  2. 检测屏幕.html,在线检测显示器屏幕尺寸
  3. Jenkins凭据导出
  4. 你有“隐私泄露担忧”吗?适合普通用户的6个方法来了
  5. Uncaught ReferenceError: regeneratorRuntime is not defined 解决方法
  6. Eclipse插件10
  7. 我的奋斗之黑马第一天
  8. 【Jquery】Jquery判断客户端请求来源于PC端还是移动端
  9. 打开netlogo model 出现failed to launch JVM
  10. 解决NGUI的UITexture图片不显示问题