哈希表 (哈希冲突、避免、哈希函数、负载因子、闭散列法、开散列/哈希桶)--- 永不过时的数据结构

目录

1.概念:

2.哈希冲突

3.哈希避免:

3.1哈希函数:

哈希函数的设计原则:

常用的哈希函数:

3.2负载因子:

3.3冲突解决:

闭散列法:

开散列/哈希桶


1.概念:

         在我们之前学到的顺序存储结构如链表,或者平衡二叉树,它们搜索的时候需要经过多次比较,顺序结构搜索的时间复杂度为O(N),而平衡二叉树搜索的时间复杂度为O(log_2{N})

        而我们理想的搜索方式是不做任何比较,一次直接从表中得到想要的元素,如果构造一种存储结构,通过某种函 数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快 找到该元素。

        我们在向该结构插入一个元素的时候,可以通过某种函数计算出存储位置并在该位置存放该元素,在向该结构搜索元素时,可以可以通过某种函数计算出它存储的位置并进行访问。

        该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

例如:

        在一个容量为10的数组中,依次插入1、4、5、6、7、9,那如果按照传统的从左往右给它们进行存放,效果会是这样的:

         那么此时我们如果要找“7”这个元素,我们就要从左往右依次遍历,直到在下标为4的时候才找到元素“7”.

        如果我们使用一个哈希函数来对它们要存放的位置进行计算,我们在下次查找该元素时也可以通过哈希函数直接找到它的位置,假设哈希函数为:Hash = Key % Capacity(Capacity = 10)那么效果会是这样的:

         那么我们如果要找“7”这个元素的时候,就可以直接通过哈希函数来计算了:7 % 10 = 7 -> 那么元素“7”所在的位置就在7下标的位置。

2.哈希冲突

        

        试想一下,如果我们在该数组中插入“44”元素,那么44 % 10 = 4,显然“44”元素应该需要插入在下标为4的位置上,但是下标为4的位置已经被元素“4”占了,那么这种Hash(key ) == Hash(key )的情况就是哈希冲突。

        由于我们哈希表的容量往往小于所要存储的元素关键字的数量的,那么这时候哈希冲突就无法避免了,我们要做的就是降低哈希冲突的概率。

3.哈希避免:

3.1哈希函数:

哈希函数的设计原则:

        1.哈希函数的定义域必须包括需要存储的所有关键码。

        2.哈希函数计算出来地址能够均匀的分配在整个区域中。

        3.哈希函数应该尽可能简单。

常用的哈希函数:

        1. 直接定制法--(常用) 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:Loading Question... - 力扣(LeetCode)

        2.除留余数法--(常用) 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

        3.平方取中法--(了解) 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对 它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况。

        4. 折叠法--(了解) 比特就业课 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和, 并按散列表表长,取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

        5. 随机数法--(了解) 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数 函数。 通常应用于关键字长度不等时采用此法。

3.2负载因子:

        负载因子:\alpha = 填入表中的个数 / 散列表的长度。

3.3冲突解决:

闭散列法:

        闭散列法:也叫开放定址法,如果哈希表还没有满,那么可以把Key存放到冲突位置的下一个空位置去。

1.线性探测:

        我们要在这个数组中插入元素“44”,发现元素“44”所要存放的位置与元素“4”冲突了,那么我们就继续往下找位置,到下表为5的位置发现与元素“5”冲突了,再继续往下找,直到找到下表为8的空位置,那么就可以存放元素“44”了。

2.二次探测:

         同样我们要在这个数组中插入元素“44”,发现元素“44”所要存放的位置与元素“4”冲突了,那么我们使用二次探测去寻找下一个空位置:H_{i} = (H_{0} + i^{2}) % m  (i = 1,2,3...),可得下一个空位置位(4  + 1) % 10 = 5,那么到下表为5的位置发现与元素“5”冲突了,再继续进行计算:(4 + 2  *2) % 10 = 8,找到下表为8的空位置,那么就可以存放元素“44”了。

开散列/哈希桶

        开散列法又称链地址法(开链法),首先用关键码用哈希函数计算其散列地址,将具有相同散列地址的元素存放在一个子集合中,每一个子集合称为一个桶,桶里的元素用单链表连接起来,然后再将每个桶的头节点存储在哈希表中。

从上图可以看出,开散列法就是将哈希冲突的元素存放在一个桶里。

模拟实现:

package HashBucket;

import java.util.HashSet;
import java.util.Set;

public class HashBucket<K, V> {
    private class Node<K, V> {
        private K key;
        private V value;
        Node next;


        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }


    private Node<K, V>[]  array = new Node[8];
    private int size;   // 当前的数据个数
    private static final double LOAD_FACTOR = 0.75;
    private static final int DEFAULT_SIZE = 8;//默认桶的大小

    public void put(K key, V value) {
        int hashNum = key.hashCode();
        int index = hashNum % array.length;

        Node cur = array[index];
        while(cur != null)
        {
            if(cur.key.equals(key)){
                cur.value = value;
                break;
            }
            cur = cur.next;
        }

        Node newNode = new Node(key, value);
        newNode.next = array[index];
        array[index] = newNode;
        this.size++;

        if(loadFactor() > LOAD_FACTOR)
        {
            resize();
        }
    }


    private void resize() {
        Node[] newArray = new Node[array.length * 2];

        for(int i = 0; i < array.length; i++)
        {
            Node cur = array[i];
            while(cur != null)
            {
                int newhash = cur.key.hashCode();
                int index = newhash % newArray.length;
                Node curNext = cur.next;

                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }


    private double loadFactor() {
        return size * 1.0 / array.length;
    }


    public HashBucket() {//无参构造函数
        this.array = new Node[8];
        this.size = 0;
    }


    public V get(K key) {
        int hashNum = key.hashCode();
        int index = hashNum % array.length;
        Node cur = array[index];

        while(cur != null){
            if(cur.key.equals(key))
            {
                return (V) cur.value;
            }
            cur = cur.next;
        }
        return null;
    }
}

在冲突过于严重时有两种解决方法:

        1.在冲突严重的桶里存放另一个哈希表。

        2.在冲突严重的桶里存放一颗搜索树。

HashMap的补充:

1.为什么HashMap内部的bucket数组的长度一直都是2的整数次幂?

原因1:可以使用key.hash & (table.length - 1) 位运算的方法来快速寻址,原理是这样的:

以table.length = 16  ( 2^{4} )为例,将它用二进制表示为10000,假设key.hash = 7,如果我们用Hash(key) = key% p来寻址,结果为7 % 16  = 7。那么当用key.hash & (table.length - 1)的方法来寻址结果也为7,如下图:

用代码表示:

        int length = 16;//2^4
        String str = "helloworld";

        int key = str.hashCode();

        int a = key % length;
        int b = key & (length - 1);
        System.out.println(a == b);//true

 原因2:在HashMap扩容的时候,可以保证同一个桶中的元素均匀散列到新的桶中,确切地讲就是在同一个桶中的元素在扩容后,一半会留在原来的桶中,一半会放在新的桶中。

2.HashMap默认的数组是多大?

HashMap默认的数组容量是16,就算在构造HashMap的时候传入了不是2的整数次幂的数,那么HashMap也会找到一个最接近2的整数次幂的数来初始化数组桶。

3.HashMap什么时候开辟bucket数组来占用内存?

第一次调用put的时候调用resize方法。

4.HashMap什么时候扩容?

HashMap中的元素熟练超过阈值时,阈值计算方式是capacity * loadFactor,在HashMaploadFactor是0.75。

5.桶中的元素列表何时转换为红黑树,何时转换会链表,为什么要这样设计?

在同一个桶的元素数量大于等于8的时候转换为红黑树,在同一个桶的元素小于等于6的时候转换回链表,原因是避免红黑树和链表的频繁转换,减少性能损耗。

6.JDK8中为什么要引入红黑树,是为了解决什么场景的问题?

引入红黑树是为了避免hash性能急剧下降,引起HashMap的读写性能急剧下降的场景,正常情况下,一般是不会用到红黑树的,在一些极端场景下,假如客户端实现了一个性能拙劣的hashCode方法,可以保证HashMap的读写复杂度不会低于O(lgN)

7.HashMap如何处理keynull的键值对?

放置在桶数组中下标为0的桶中。

HashMap和HashTable 的异同?

1.二者的存储结构和解决冲突的方法都是相同的。

2.HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

3.HashTable 中 key和 value都不允许为 null,而HashMap中key和value都允许为 null(key只能有一个为null,而value则可以有多个为 null)。但是如果在 Hashtable中有类似 put( null, null)的操作,编译同样可以通过,因为 key和 value都是Object类型,但运行时会抛出 NullPointerException异常。

4.Hashtable扩容时,将容量变为原来的2倍+1,而HashMap扩容时,将容量变为原来的2倍。

5.Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在计算hash值对应的位置索引时,用 %运算,而 HashMap在求位置索引时,则用 &运算。

摘自大佬的图:

附:HashMap put方法逻辑图(JDK1.8)

 最后给一些习题:

136. 只出现一次的数字 - 力扣(LeetCode)

138. 复制带随机指针的链表 - 力扣(LeetCode)

771. 宝石与石头 - 力扣(LeetCode)

 771. 宝石与石头 - 力扣(LeetCode)

692. 前K个高频单词 - 力扣(LeetCode)

本篇内容部分摘自:

(14条消息) Java集合之一—HashMap_woshimaxiao1的博客-CSDN博客

(14条消息) 10分钟拿下 HashMap_一小页的博客-CSDN博客

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值