数据结构之—哈希表

这篇具有很好参考价值的文章主要介绍了数据结构之—哈希表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、哈希表的概念

1.前言

2.概念

二、哈希函数:将任意一个key值映射成整数

1.哈希函数最常用的方法:取模

2.哈希函数设计原则

3.比较对象相等时,hashCode与equals关系

4.MD5:一般给字符串进行hash运算

1)MD5的三大特点:定长、分散、不可逆

2)MD5应用

三、哈希冲突

1.概念

2.哈希冲突时最常见的两大解决思路

1)闭散列(key存放到冲突位置的“下一个”空位置)

2)开散列(冲突位置变为链表)

3.开散列下冲突严重时(导致链表过长)的优化

1)整个哈希表进行扩容

2)单个链表转为哈希表或者变为搜索树

4.负载因子

四、实现哈希表

1.添加一个键值对

2.查询key/value是否存在

 3.删除key值对应节点

4.扩容 ​

​五、JDK8的HashMap

1.Set和Map集合有何关系?

2.JDK的HashMap源码的大体结构和重点方法

1)大体结构

2)冲突严重时,什么时候才会将变为红黑树?

3)HashMap的属性解读(重点方法)

3.JDK的HashMap中的hash方法的实现


一、哈希表的概念

1.前言

      哈希表(有的书上叫散列表)实际上是通过数组衍生出来的,哈希表高效查找的奥秘就在于数组的随机访问特性(哈希表是天然的查找/搜索)。假设有个数组[9,5,2,7,3,6,8],需要判断3这个元素是否存在,两种方法:

1)使用搜索树存储这个集合,然后查找3是否存在:二分查找

2)可以创建一个boolean数组,这个数组的长度取决于原集合最大值是谁

数据结构之—哈希表

有了这个新的boolean数组后,要查询原数组中3是否存在,只需看hash[3]==true?

2.概念

哈希表是一个典型的以空间换时间的数据结构。

1.遍历原集合,创建一个新数组,建立元素和索引的对应关系,当元素在集合中存在,就将新数组对应的索引位置标记位true

2.判断一个数是否在原集合存在,只需要拿着这个数对应在新数组的索引位置看是否为true就能知道是否存在(O(1)复杂度)。

二、哈希函数:将任意一个key值映射成整数

      假设现在的数据集:[101,3000,0,10,-2,10000],这种集合元素跨度大,有的元素本身值也大,若采用一一对应的方式的话就得开辟至少10001的长度的空间,非常浪费空间,因此大部分场景下,我们需要将原数组的元素与数据的索引建立一个映射关系,这个映射关系就叫哈希函数

1.哈希函数最常用的方法:取模

      key是数组下标,采用取模法,比如数组长度为10,将key先做绝对值(key有的是负数)再进行%10。【哈希函数对哪个数字取模数组长度就是几

a.将key取绝对值:Math.abs(key)

b.再将取绝对值后的key%10:Math.abs(key)%10

      通过取模可以将一个很大范围的数据集映射到一个小区间(区间的大小取决于我们取模数的大小),但有可能出现多个不同的key经过hash之后得到了相同的值,这被我们称为哈希冲突(在数学上理论存在)。

2.哈希函数设计原则

1)不同的key值经过hash函数运算后得到的结果分布越均匀(假设现在新数组长度为n,得到的结果平均分布在新数组的各个位置)越好最常用的哈希函数的设计就是取模,一般来说,模一个素数会得到一个比较均衡的值

eg: 10 20 30 40 50

%4 2 0 2 0 2【分布不均匀(哈希冲突严重)】

%7 3 6 2 5 1【分布均匀】

2)稳定性:相同的key值在经过N次哈希运算后得到的值一定是相同的

3)哈希函数一般不需要自己设计,一般用现成的就好,jdk中Object中有hashCode()方法

数据结构之—哈希表

任意一个数据类型都可以通过hashCode方法转为整型:

数据结构之—哈希表

观察结果:无论得到的值是什么,他始终是整型:

数据结构之—哈希表

3.比较对象相等时,hashCode与equals关系

a.hashCode相同他们的equals一定相同吗:false

      不同的key值可能对应相同的hash值,有哈希冲突,此时对象的equals是不相等的。

b.equals相同的对象,他们的hashCode的值一定相同吗:true

      equals同,key值同,所以hash的值肯定相同。

4.MD5:一般给字符串进行hash运算

数据结构之—哈希表

      MD5,MD4,MD3;SHA1,SHA256(两大家族),以MD5为例,MD5一般给字符串进行hash运算。

1)MD5的三大特点:定长、分散、不可逆

a.长度固定:无论输入的数据有多长,得到的MD5值是长度固定的

数据结构之—哈希表

b.分散:如果输入的数据稍微有点变化,得到的MD5值相差非常大【一般认为两个str的md5值相同,这两个str就是相同的字符串,工程上几乎可以忽略md5的冲突】

数据结构之—哈希表

c.不可逆:根据任意值计算出的MD5值很容易,但是MD5值还原为原数据(难如登天),基本不可能(可以用在加密领域)。

d.稳定性:相同字符串得到MD5内容完全一样

数据结构之—哈希表

2)MD5应用

a.作为hash值

b.作为加密

c.对比文件内容(常用:一般来说大文件都有一个MD5值,大文件在传输过程中有可能由于网络问题有的片段丢失了,要想知道下载后的文件内容是正确的,我们就拿着下载后的文件计算md5值,看和原文件的MD5值是否相同【就是根据文件内容是否相同校验文件内容是否修改】)

三、哈希冲突

1.概念

      对于两个不同的数据元素通过相同哈希函数计算出来相同的哈希地址(即两不同元素通过哈希函数取模得到了同样的模值),这种现象称为哈希冲突或哈希碰撞。

2.哈希冲突时最常见的两大解决思路

1)闭散列(key存放到冲突位置的“下一个”空位置)

也叫开放定址法,当发生哈希冲突时,如果哈希表(新的数组)未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的“下一个”空位置中去。那么如何寻找下一个空位置呢?两个方法:

a.线性探测

      从冲突位置开始继续向后查找空余位置直到找到第一个空余位置为止,就把冲突元素放到此位置。

eg: 一组数据[1,2,3,19,120,121],哈希函数堆101取模【新的数组长度就是101】

数据结构之—哈希表

分析:

      好放难取更难删:以查询121为例,先对121取模找到他应该存放的位置:20;2.发现索引为20的位置的元素并不是121,继续向后查询(从20这个位置开始向后遍历)直到找到121为止,21这个位置就找到了121,存在。以查询20为例,先对20取模,得到查询的位置索引为20,从索引20位置开始向后遍历数组,不断找20这个元素,走到数组结尾还没找到,说明不存在。观察发现,当元素不存在时就变成遍历数组查找了,效率比较低

b.再哈希/二次探测:再对冲突的key值取模

c.闭散列总结:闭散列方式最大的问题就在于,若整个哈希表冲突比较严重,此时查找元素的过程就相当于遍历数组,查找效率退化为O(N)

2)开散列(冲突位置变为链表)

当发生冲突时,就让冲突位置变为链表(既简单又实用)

数据结构之—哈希表

      所谓的开散列方案就是把单纯的数组变为数组+链表的方式,当发生哈希碰撞时就将对应冲突位置的元素转换为链表,之后的查询和删除操作都是针对这个单个链表来进行处理。

eg:

数据结构之—哈希表

      线性探测法(闭散列)。等概率平均查找:平均查找长度=这六个元素在哈希表中查找的总次数/6。

数据结构之—哈希表

拓展:若转为开散列查找呢?

数据结构之—哈希表

3.开散列下冲突严重时(导致链表过长)的优化

1)整个哈希表进行扩容

      假设以前%7(数组长度为7),现在就扩容为原数组的一倍,现在%14(数组长度变为14),就可以大大降低冲突的概率,减少链表长度(C++中)。

2)单个链表转为哈希表或者变为搜索树

      单个链表长度过长,查询效率就会变为链表的遍历O(N),就针对这单个链表进行转换处理:可以把单个链表转为哈希表或者变为搜索树(JDK8的HashMap就采用此方案,当某个链表的长度>8且整个哈希表元素个数>64,把此链表转为RBTree【源码解读处有】)(java)

4.负载因子

数据结构之—哈希表

1.负载因子越大,发生哈希冲突的概率越大,数组长度就会偏小,但节省空间

2.负载因子越小,发生哈希冲突的概率越小,数组长度就会偏大,但浪费空间

3.负载因子取多大,要根据当前系统需求做实验得知JDKHashMap的负载因子是0.75。【不同情况下负载因子可能不同,阿里内部对于负载因子建议取值为10,允许每个链表的平均长度为10以内,可以接收】。

4.当元素个数/负载因子>=数组长度,此时我们认为冲突比较严重,需要进行扩容,即假设此时HashMap的哈希表长度为16,当元素个数超过12就会发生扩容。

      负载因子就是在空间和时间上求平衡(即它不是固定的,是一个可以动态变化的)

数据结构之—哈希表

%err:发生哈希碰撞概率,prime:建议的负载因子取值

四、实现哈希表

1.添加一个键值对

/**
 * 基于开散列方案下的哈希表实现
 */
public class MyHashMap {

    private class Node{
        //对key求哈希运算
        int key;
        int value;
        //发生哈希碰撞时转链表就用next属性存储它的下一个节点
        Node next;

        public Node(int key, int value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
    //当前哈希表中实际存储元素的个数
    private int size;
    //默认哈希表的长度
    //也有的教科书将每个哈希表的数组元素称为哈希桶
    private static final int DEFAULT_CAPACITY=16;
    //默认负载因子
    private static final double LOAD_FACTOR=0.75;
    //取模数,用于取到key的索引
    private int M;
    //实际存储数据的数组:Node数组
    private Node[] data;
    public MyHashMap(){
        this(DEFAULT_CAPACITY);
    }
    public MyHashMap(int initCap) {
        this.data=new Node[initCap];//可以从外部传入
        this.M=initCap;//initCap:长度//数组长度赋给取模数(对数组长度取模)
    }
    public int hash(int key){//哈希方法
        return Math.abs(key)%M;//对key直接做绝对值对M取模
    }

    /**
     * 在当前哈希表中添加一个键值对 key=value
     * @param key
     * @param value
     */
    public int add(int key,int value){
        //1.先对key取模,得到存储的索引
        int index=hash(key);
        //2.遍历这个索引对应的链表,查看当前key是否已存在
        for(Node x=data[index];x!=null;x=x.next){//每个数组存储的时node节点,每个链表的链表头就存储在数组的索引位置
            if(x.key==key){
                //此时key已经存在,更新value
                int oldValue=x.value;
                x.value=value;
                return oldValue;
            }
        }
        //3.此时key对应的元素在当前哈希表中不存在,新建节点头插在哈希表中
//        Node head=data[index];//原先的头节点
//        Node node=new Node(key,value,head);//对于构造方法,新节点的next要等与head
        Node node=new Node(key,value,data[index]);
        data[index]=node;//最新的链表头要变为当前的node
        size++;
        //4.添加一个新元素后,查看是否需要扩容
        if(size/LOAD_FACTOR>= data.length){
            //size/LOAD_FACTOR>= data.length也可以写成LOAD_FACTOR* data.length<=size
            //TODO:哈希表的扩容: resize();
        }
        return value;
    }
}

 Test:添加元素的测试:

public static void main(String[] args) {
    MyHashMap hashMap=new MyHashMap(4);//数组长度是4
    hashMap.add(1,10);
    hashMap.add(2,20);
    hashMap.add(5,55);
    System.out.println();//在此句前面打个断点观察
}

 结果:

数据结构之—哈希表

2.查询key/value是否存在

/**
 * 查询key是否存在
 * @param key
 * @return
 */
public boolean containsKey(int key){
    int index=hash(key);
    //遍历index位置对应的链表,查看是否有节点的key值与查询的key值相等
    for(Node x=data[index];x!=null;x=x.next){
        if(x.key==key){
            return true;
        }
    }
    return false;
}

/**
 * 查询val是否存在
 * 由于val没有对应索引(数组是利用key对应索引存的),所以需要数组全部遍历一遍
 * @param value
 * @return
 */
public boolean containsValue(int value){
    //遍历整个哈希表
    //数组从左到右遍历
    for (int i = 0; i < size; i++) {
        //依次遍历以数组元素为节点的每个链表
        for(Node x=data[i];x!=null;x=x.next){
            if(x.value==value){
                return true;
            }
        }
    }
    return false;
}

Test:

public static void main(String[] args) {
    MyHashMap hashMap=new MyHashMap(4);//数组长度是4
    hashMap.add(1,10);
    hashMap.add(2,20);
    hashMap.add(5,55);
    System.out.println(hashMap.containsKey(1));
    System.out.println(hashMap.containsKey(100));
    System.out.println(hashMap.containsValue(10));
    System.out.println(hashMap.containsValue(0));
}

 3.删除key值对应节点

/**
 * 删除哈希表中key值对应的节点
 * 返回待删除的键值对对应的value
 * @param key
 * @return
 */
public int remove(int key){
    int index=hash(key);
    //判断当前链表头节点是否是待删除节点
    Node head= data[index];
    if(head.key==key){
        int val=head.value;
        data[index]=head.next;
        head.next=head=null;//从链表中把这个节点断掉
        size--;
        return val;
    }
    //当前链表头节点不是待删除节点
    Node prev=head;
    while(prev.next!=null){
        if(prev.next.key==key){
            //prev恰好是待删除结点的前驱
            Node cur=prev.next;
            int val=cur.value;
            //删除操作
            prev.next=cur.next;
            cur.next=cur=null;
            size--;
            return val;
        }
        prev=prev.next;
    }
    //走到这此时节点不存在,抛出异常
    throw new NoSuchElementException("no such key!remove error");
}

数据结构之—哈希表

4.扩容 

/**
 * 哈希表扩容
 */
private void resize() {
    //新数组的长度变为原来的一倍
    Node[] newData=new Node[data.length<<1];
    //细节:原节点的key对应新数组的索引hash有变化,现在的取模数应该变为新数组的长度(如之前%4,扩容后就是%8)
    this.M= newData.length;
    //遍历原数组,进行节点的搬移
    for (int i = 0; i < data.length; i++) {
        if(data[i]!=null){
            //对应链表不为空
            //进行链表遍历
            for(Node x=data[i];x!=null;){
                //暂存i索引所在链表对应节点的next(暂存后继节点)
                Node next=x.next;
                //新数组的索引
                int newIndex=hash(x.key);
                //新数组的头插
                x.next=newData[newIndex];
                newData[newIndex]=x;
                //继续进行下一个节点的搬移操作
                x=next;

            }
        }else{
            //当前数组对应的i索引下没有节点,无需搬移
            continue;//else这步可以不用写
        }
    }
    //最终只需要data=newData就可以切换成新的哈希表
    data=newData;
}

对如下两行代码的解释:

 x.next=newData[newIndex];
 newData[newIndex]=x;

五、JDK8的HashMap

源码刨析:如何查看源码->双击shift键

数据结构之—哈希表

JDK中Set和Map的源码分析:

1.Set和Map集合有何关系?

      Set集合下的子类就是Map集合下的子类,HashSet就是HashMap,TreeSet就是TreeMap。在HashSet内部元素都在HashMap的key存储之所以Set是不重复的,原因是因为Set中的元素存储在了Map的key上(key不重复),所以Set不重复【Set的E就是Map的key值】。

数据结构之—哈希表

观察Set集合的方法:

数据结构之—哈希表

1.调用Set集合的add方法实际上就是调用Map集合的put方法,将Set集合的元素放在key上,val都是同一个空的Object对象。

2.val:

      PRESENT(常量)是默认有个空元素,Set中不存在value,所以在map里面存的是空的object对象,所有的Set的value值全是这个空的Object对象。

2.JDK的HashMap源码的大体结构和重点方法

1)大体结构

a)JDK7之前的HashMap是一个基于开散列的散列表实现:数组+链表结构。

b)JDK8之后引入了红黑树:数组+链表(或红黑树),当冲突不严重就是链表,冲突严重就是红黑树

      当某个链表的长度过长时,元素的查询就会退化为链表的遍历,会趋于O(N)时间复杂度,我们把它树化,即使元素个数再多查询效率起码也是O(logN)

数据结构之—哈希表

2)冲突严重时,什么时候才会将变为红黑树?

      若链表的长度已经>8,但是整个链表的元素个数<64,此时只是做扩容(哈希表长度变为原来的一倍),若链表的长度已经>8,并且整个链表的元素个数>64,此时就将链表转为红黑树。【详细看hashMap属性解读】

3)HashMap的属性解读(重点方法)

数据结构之—哈希表数据结构之—哈希表

1.默认哈希表长度为16,a power of two:哈希表长度必须是2^N

2.HashMap的最大容量:2^30,一个HashMap最多存储2的30次方个节点

3.默认负载因子:0.75

4.⭐链表树化的阈值:是链表还是树根据这个属性判断

5.⭐解树化:当一个RBTree的节点个数小于该值时,会将其再还原为链表

6.⭐树化的另一个判断值:到底是否应该把链表变为红黑树还要看当前整个哈希表的元素个数是否超过这个值,若链表的长度已经>8,但是整个链表的元素个数<64,此时只是做扩容(哈希表长度变为原来的一倍),若链表的长度已经>8,并且整个链表的元素个数>64,此时就将链表转为红黑树。

3.JDK的HashMap中的hash方法的实现

数据结构之—哈希表数据结构之—哈希表

      hash=h^(h>>>16):这个不是最后索引,只是得到了一个比较均匀的hash后的整数。最终索引值=(数组长度-1)&上一步得到的hash【这里也体现了数组长度n为什么得是2^N,因为当数组长度是2^N时,最终索引值的这个&运算等价于hash%N】。

数据结构之—哈希表

总结:

      在自主实现的hash表中我们采取的时hash%n,而在此处源码中我们采用的是(n-1)&hash的与运算操作,这两者得到的值都是相同的,但是与运算运行速度更快,效率高,所以源码中采取了与运算。文章来源地址https://www.toymoban.com/news/detail-479834.html

到了这里,关于数据结构之—哈希表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构前言

    数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 上面是百度百科的定义, 通俗的来讲数据结构就是数据元素集合与数据元素集合或者数据元素与数据元素之间的组成形式。 举个简单明了的例子: 就像一个图书馆

    2024年02月09日
    浏览(54)
  • 数据结构入门(C语言版)图的概念和功能函数实现

    图是一种比线性表和树更复杂的数据结构。在线性表中,数据元素之间仅有 线性关系每个元素 只有 一个直接前驱 和 一个直接后继 。在树形结构中,数据元素之间存在明显的层次关系,并且每层的元素可能和下一层的多个元素(即其孩子结点)相邻,但只能和上一层的个元素(即其

    2024年02月06日
    浏览(51)
  • C语言数据结构(0)——前言

    欢迎来到博主的新专栏——C语言与数据结构 博主id:代码小豪 在前两个专栏当中,博主已经大致的讲过了C语言中的大部分使用方法。大家都知道,学习英语时,首先掌握的是单词,随后学习语法,如此才能融会贯通的学习英语。如果学英文只会单词,那么阅读虽然不成问题

    2024年01月17日
    浏览(46)
  • 数据结构-哈希-哈希表实现

    🚀理想的搜索方法:不经过任何的比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数使元素的存储位置与其关键码之间能够建立起一一映射的关系,那么在查找的时候就能通过此函数快速的找到该元素。 🚀向该结构中插入元素:根据该元素的

    2024年02月10日
    浏览(37)
  • 【数据结构】哈希底层结构

    目录 一、哈希概念 二、哈希实现 1、闭散列 1.1、线性探测 1.2、二次探测 2、开散列 2.1、开散列的概念 2.2、开散列的结构 2.3、开散列的查找 2.4、开散列的插入 2.5、开散列的删除 3、性能分析  顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查

    2024年02月06日
    浏览(45)
  • 【数据结构】哈希表与哈希桶

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》 《数据结构》 《蓝桥杯试题》 《LeetCode刷题笔记》 《实训项目》 《C++》 《Linux》 《算法》 🌝 每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.概念 2.哈希冲突 3.解决哈希冲突 3.1闭散列 3.2开散列(哈希桶) 4.模拟实

    2024年03月21日
    浏览(45)
  • 「数据结构」哈希表2:实现哈希表

    🎇 个人主页 :Ice_Sugar_7 🎇 所属专栏 :Java数据结构 🎇 欢迎点赞收藏加关注哦! 在讲插入之前需要先了解扩容,因为 插入后载荷因子如果超过阈值,那我们就要扩容,即扩容是插入操作的一部分 扩容后,原先哈希表中的元素的哈希地址会改变。之前会发生哈希冲突的元素

    2024年02月21日
    浏览(40)
  • 【数据结构】哈希表——闭散列 | 开散列(哈希桶)

    🐱作者:一只大喵咪1201 🐱专栏:《数据结构与算法》 🔥格言: 你只管努力,剩下的交给时间! 哈希(Hash):是一种方法,将数据的key值和存储位置建立关系。 在之前学习过的顺序结构以及平衡树中,所有数据的key值和存储位置之间都没有对应的关系。所以在查找一个数据

    2023年04月24日
    浏览(45)
  • 数据结构:哈希表讲解

    哈希: 一种 映射思想 ,也叫散列。即和另一个值建立一个关联关系。注意 这里指的关联关系是多样的 ,比如给你,你可以通过映射关系确定该值在不在或者获得其它信息,不一定要存储另一个值。 哈希表 :也叫散列表,体现了哈希思想。即和存储位置

    2024年02月05日
    浏览(44)
  • 哈希表----数据结构

    如果你是一个队伍的队长,现在有 24 个队员,需要将他们分成 6 组,你会怎么分?其实有一种方法是让所有人排成一排,然后从队头开始报数,报的数字就是编号。当所有人都报完数后,这 24 人也被分为了 6 组,看下方。 (你可能会让 1~4 号为第一组,5~8 号为第二组……但

    2024年02月05日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包