你还在为哈希表而烦恼吗?

 今以结Map一

目录

哈希表

1.概念

1.1冲突

1.2避免冲突

1.3哈希函数设计

1.3.1常见哈希函数

直接定制法--(常用)

除留余数法--(常用)

平方取中法--(了解)

折叠法--(了解)

随机数法--(了解)

数学分析法--(了解)

1.4负载因子调节(重点掌握)

1.5解决冲突

1.5.1闭散列

线性探测

二次探测

1.5.2开散列/哈希桶(重点掌握)

1.5.3冲突严重时的解决办法

1.6性能分析


哈希表

1.概念

  ①哈希表的引出:
有没有 一种数据结构可以不经过任何比较,一次直接从表中得到要搜索的元素 如果构造一种存储结构,通过某种函 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系 ,那么在查找时通过该函数可以很快 找到该元素
②当插入元素时 ,以此函数计算出该元素的存储位置并按此位置进行存放
③当搜索元素时 ,对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
哈希方法中使用的转换函数称为哈希(散列) 函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)  
用一个图来简单说明一下:(通过哈希函数映射关系来得到相应位置)

1.1冲突

①什么是冲突:

对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

②如果遇到负数怎么办? 

如果在存入过程中遇到负数,让整个数组让其加入负数的最小值,将其变为正数

1.2避免冲突

由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一
个问题, 冲突是不可避免的,但我们能做的应该是尽量的 降低冲突率

1.3哈希函数设计

①哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须 在0到m-1 之间
②哈希函数计算出来的地址能 均匀分布 在整个空间中
③哈希函数应该 比较简单

1.3.1常见哈希函数

直接定制法--(常用)

①操作:
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
②特点: 简单、均匀 ;缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况(该哈希函数是一个一次函数,那么可以保证其一定是均匀分布的)
③用一个题目进行示例: 387. 字符串中的第一个唯一字符 - 力扣(LeetCode) (leetcode-cn.com)icon-default.png?t=M276https://leetcode-cn.com/problems/first-unique-character-in-a-string/a.根据ASCLL进行存储,因为a的ascll为97,为了使空间利用最大化,我们奖每个字母减去97,则24个字母则所表示的下标为0~23;
b.进行存入。
c.每存入一次记为1,然后累积,最后按照字符串出现的顺序进行读取第一次出现且只出现了一次的字符

代码如下:

class Solution {
    public int firstUniqChar(String s) {
if(s==null)return -1;
int []nums=new int[26];
for(int i=0;i<s.length();i++){
    char ch=s.charAt(i);
    nums[ch-97]++;
}
for(int i=0;i<s.length();i++){
    char ch=s.charAt(i);
    if(nums[ch-97]==1){
        return i;
    }
}return -1;
    }
}

除留余数法--(常用)

①操作: 设散列表中允许的地址数为m ,取一个不大于m,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址(p<m)
②特点: 缺点:浪费空间(长度为5,取了3,就会有两个存在浪费的情况)

平方取中法--(了解)

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

折叠法--(了解)

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

随机数法--(了解)

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

数学分析法--(了解)

设有 n d 位数,每一位可能有 r 种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

1.4负载因子调节(重点掌握)

①什么是负载因子以及如何计算负载因子:

 ②负载因子与冲突率之间的关系

而输入的关键字的个数是不会改变的,那么为了降低冲突率,我们常常采用的方法是增大哈希表的长度 

1.5解决冲突

解决哈希冲突两种常见的方法是:闭散列开散列

1.5.1闭散列

①什么是闭散列???
也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 key 存放到冲突位置中的 下一个 空位置中去。
②闭散列是怎么存放的???
线性探测和二次探测

线性探测

①什么是线性探测:

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

②线性探测的相关操作:

当插入操作时,通过哈希函数获取待插入元素在哈希表中的位置 ;如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到 ;下一个空位置,插入新元素

简而言之就是寻找下一个空的地方

③弊端:(可能会导致冲突元素均被放在一起)

二次探测

①如何进行二次探测:

利用这个公式进入插入。其中:i = 1,2,3…,Hi是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

对于上述线性探测中的问题如果要插入44,产生冲突,使用解决后的情况为:

②重要结论:
当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情 况,但在插入时必须确保表的装载因子a 不超过 0.5 ,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

1.5.2开散列/哈希桶(重点掌握)

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

③若遇到负载因子过大,要扩容,那么存入的数据又该怎么进行处理???(链表中的每一个数要进行重新哈希)

以下为二倍扩容后的图

代码如下:

public class HashBuck {

    static class Node {
        public int key;
        public int val;
        public Node next;

        public Node(int key,int val) {
            this.key = key;
            this.val = val;
        }
    }

    public Node[] array;
    public int usedSize;

    public static final double DEFAULT_LOAD_FACTOR = 0.75;

    public HashBuck() {
        this.array = new Node[10];
    }

    /**
     * put函数
     * @param key
     * @param val
     */
    public void put(int key,int val) {
        //1、找到Key所在的位置
        int index = key % this.array.length;
        //2、遍历这个下标的链表,看是不是有相同的key。有 要更新val值的
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                cur.val = val;//更新val值
                return;
            }
            cur = cur.next;
        }
        //3、没有这个key这个元素,头插法
        Node node = new Node(key, val);
        node.next = array[index];
        array[index] = node;
        this.usedSize++;
        //4、插入元素成功之后,检查当前散列表的负载因子
        if(loadFactor() >= DEFAULT_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 index = cur.key % newArray.length;//获取新的下标 11
                //就是把cur这个节点,以头插/尾插的形式 插入到新的数组对应下标的链表当中
                Node curNext = cur.next;
                cur.next = newArray[index];//先绑定后面
                newArray[index] = cur;//绑定前面
                cur = curNext;
            }
        }
        array = newArray;
    }

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

    /**
     * 根据key获取val值
     * @param key
     * @return
     */
    public int get(int key) {
        //1、找到Key所在的位置
        int index = key % this.array.length;
        //2、遍历这个下标的链表,看是不是有相同的key。有 要更新val值的
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }
④hashcode(解决引用类型情况下,把它变成是一个合法的整数)

但是这个时候直接输出他们的hashcode却是不相同的

所以我们进行了重写


hashcode 和 equals

①hashcode一样,equals不一定一样(只能说明在同一个位置,但可以挂多个结点) 

②equals一样,hashcode一定一样(都确定是哪个结点了,那么必然也是在同一个位置)

代码如下:

class Person {
    public String ID;

    public Person(String ID) {
        this.ID = ID;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(ID, person.ID);
    }

    @Override
    public int hashCode() {
        return Objects.hash(ID);
    }

    @Override
    public String toString() {
        return "Person{" +
                "ID='" + ID + '\'' +
                '}';
    }
}
public class HashBuck2<K,V> {

    static class Node<K,V> {
        public K key;
        public V val;
        public Node<K,V> next;

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

    public Node<K,V>[] array = (Node<K,V>[])new Node[10];
    public int usedSize;

    public void put(K key,V val) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                cur.val = val;//更新val值
                return;
            }
            cur = cur.next;
        }
        Node<K,V> node = new Node<>(key, val);
        node.next = array[index];
        array[index] = node;
        this.usedSize++;
    }

    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                //更新val值
                return cur.val;
            }
            cur = cur.next;
        }
        return null;
    }

    public static void main(String[] args) {
        Person person1 = new Person("123");
        Person person2 = new Person("123");

        HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();
        hashBuck2.put(person1,"bit");

        System.out.println(hashBuck2.get(person2));
    }


1.5.3冲突严重时的解决办法

哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味 着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如: 1. 每个桶的背后是另一个哈希表 2. 每个桶的背后是一棵搜索树

1.6性能分析

工作中,其实哈希表的冲突率是相当低的,以此同时它的效率极高,因此总是被我们进行使用哈希表的插入/删除/查找时间复杂度是 O(1)

感谢观看~

猜你喜欢

转载自blog.csdn.net/weixin_58850105/article/details/123371418