数据结构-哈希表

哈希表是一种数据结构,通过哈希函数将数据映射到数组,实现快速查找。哈希函数需满足一致性、稳定性和均匀性。常见的哈希冲突解决方法包括闭散列(线性探测)和开散列(链地址法)。开散列是实际应用中常用的方法,当元素增多导致冲突加剧时,可通过扩容或链表转换为平衡树来优化。负载因子是衡量冲突程度的指标,影响空间和查找效率。
摘要由CSDN通过智能技术生成

概念

哈希表来源于数据的随机访问特性。在搜索的问题中,链表的查找只能从链表的头部遍历到结尾,时间复杂度为O(n),搜索树的查找(平衡搜索树)的时间复杂度为O(logn),于是人们开始不断探寻有没有查找比O(logn)还快的结构。 在数组中,如果知道元素的索引,查找的时间复杂度就是O(1),那能不能利用数组的随机访问特性来查找元素?这个思想就是哈希表产生的背景。

不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它本身的元素值之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,这种结构就称为哈希表,这种函数就称为哈希函数

哈希函数

将任意的数据类型的值转变为正整数,这样转换后的数字就可以作为数组的下标。一般来说,哈希函数不需要自己设计,用现成的方案即可。

哈希函数的设计需要满足以下三个规则:
1.一致性:对于两个相同的数据x和y,通过哈希函数得到的两个哈希值也必须相等。
2.稳定性:对于相同的数字x,任何时候计算哈希值得到的结果均相同。
3.均匀性:不同的数据x和y,经过哈希函数计算之后的结果尽量分散。(评价哈希函数的好坏)

关于整型的哈希函数

取模运算
原数组的内容都是整数,%n之后的结果在[0,n)范围之内,这样就可以把一组特别大的数据分散到一个可控的区间之内。
在这里插入图片描述
如上图所示,{10,20,30,40}这一组元素能在[0,8)这个区间都能表示,但是,当我们继续插入50这个元素时,发现数组中编号为2的位置已经被占用了,也就是说:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 因此,在以取模运算作为哈希函数时,比较好的方式就是取一个素数作为因子,显著降低哈希冲突的几率。

关于字符串的哈希函数

业内有很多成熟的关于字符串的哈希函数,比如:MD3,MD4,MD5,SHA1,SHA256等等,以MD5为例

特点:
1.定长:无论输入多长的字符串,经过MD5函数运算后得到的值是定长的(16位或者32位);
2.分散:原字符串只改动一点内容,得到的MD5值差距很大;
3.不可逆:从字符串得到MD5数值很容易,但是通过MD5值倒推原内容非常困难,基本不可能。

感兴趣的可以点击这里尝试一下。注意,在工程中,两个数据经过md5运算之后得到相同的值就可以认为两个值是相同的数据。 因为在工程中以md5作为哈希函数很难产生哈希冲突。

哈希冲突

前面已经提到了哈希冲突这个概念,那该如何解决这个问题呢?一般有两种方案。闭散列和开散列

闭散列

闭散列也叫线性探测。当发生冲突时,找到冲突位置旁边的空闲位置放入冲突元素
在这里插入图片描述
在闭散列方案中,假如我们要查找元素120在不在,先要对120取模算对应的索引,但发现此时索引为19的元素并不是120,此时向后遍历,一直等查到120位置,极端情况下遍历完之后都没查到,或者在最后一个位置才查到,这样就会将一个原本只有O(1)复杂度的查找问题变为O(n)。同样需要删除120这个元素时,先要查到这个元素。 因此闭散列方案好想,好放,但是难查,更难删。工程中很少采用这个方案。

开散列

开散列也叫链地址法,这是工程中普遍采用的方案。开散列的思路是当产生哈希冲突时,让冲突位置变为一个链表。以上图为例,要存储元素120时,由于19的位置已经有元素了,此时,开散列的方法是将120头插/尾插到19这个索引之后。这样元素120的查找和删除遍历链表即可。
在这里插入图片描述

试想一下,在开散列方法中,当元素个数不断变大,哈希冲突的概率也会越来越大,在某些数组中,某些链表的长度也会变得很长,这样查找效率又会从O(1)复杂度变为O(n)。此时通常有来两种解决方案:
1.针对整个数组扩容,扩容为原来的一倍,大概率原先冲突的元素再次哈希之后就不再冲突。(C++采用的方案)
2.将长度过长的链表转为BST/哈希表(数组+链表)。(JDK8+的方案)

在这里插入图片描述

负载因子

描述哈希表冲突的严重情况,一般来说,当哈希表的元素个数size>=哈希表的长度length * 负载因子factor就认为当前哈希表的冲突比较严重,需要进行处理。

举例:
若当前哈希表的长度为16,factor= 0.75,当哈希表保存的元素超过12时,就认为冲突严重;
若当前哈希表的长度为16,factor= 10,当哈希表保存的元素超过160时,就认为冲突严重。
结论:负载因子越大,冲突越严重,节省空间(保存的元素个数多);负载因子越小,冲突越轻微,浪费空间(保存元素少)。

JDK的HashMap默认负载因子就是0.75;阿里的实验室论证,在一般商用系统中,负载因子取10比较合适。

哈希表的实现

哈希表不考虑树化其实就是数组加链表的一个结构,数组的每个元素其实就是链表的头节点。
在这里插入图片描述
下面实现基于int开散列方案的哈希表。

扩容

    //扩容
    private void resize() {
        //扩容为原来的一倍
        this.M  = data.length << 1;
        Node[] newData = new Node[data.length<<1];
        //搬移原数组的所有节点
        for(int i =0;i< data.length;i++){
            for(Node x = data[i];x!=null;){
                Node next = x.next;
                //将当前x搬移到新数组的对应位置
                int newIndex = hash(x.key);
                //头插到新数组的对应位置
                x.next = newData[newIndex];
                newData[newIndex] = x;
                //继续搬移数组的下一个节点
                x = next;
            }
        }
        //更新data的指向
        data = newData;
    }
    //哈希函数-取模
    public int hash(int key){
        return key % this.M;
    }

添加元素

    //在当前哈希表中添加一对新元素,返回添加前的值,若新元素,返回-1
    public int put(int key, int value){
        //1、首先计算出当前元素的下标
        int index = hash(key);
        //2、在当前子链表中判断key值是否存在,若存在,只需要更新value即可
        for(Node x = data[index];x != null; x = x.next){
            if(x.key == key){
                //存在,更新value即可
                int oldvalue = x.value;
                x.value = value;
                return oldvalue;
            }
        }
        //3、若key值不存在,头插到当前的子链表中,返回-1
        Node node = new Node(key,value);
        node.next = data[index];
        data[index] = node;
        //4、判断当前哈希表的冲突情况,是否要扩容
        if(size>=this.data.length * LOAD_FACTOR){
            resize();
        }
        return -1;
    }

判断是key&value是否存在

    //判断当前哈希表中是否包含指定的key值
    public boolean containsKey(int key){
        int index = hash(key);
        for(Node x =data[index]; x!=null;x = x.next){
            if(x.key == key){
                return true;
            }
        }
        return false;
    }
    //判断当前哈希表中是否包含指定的value值
    public boolean containsValue(int value){
        //全表扫描
        for(int i =0;i<data.length;i++){
            //内层循环就是每个子链表的遍历
            for(Node x =data[i];x!= null;x = x.next){
                if(x.value == value){
                    return true;
                }
            }
        }
        return false;
    }

删除元素

    //在当前哈希表中删除指定的key值节点
    public boolean removeKey(int key){
        //1、先求索引,找到实在哪个子链表进行删除
        int index = hash(key);
        //先判空
        if(data[index] == null){
            return false;
        }
        //剩下就是链表的删除问题
        if(data[index].key== key){
            data[index] = data[index].next;
            size--;
            return true;
        }
        //此时头节点不是待删除的节点
        Node prev = data[index];
        while (prev.next !=null){
            if(prev.next.key == key){
                prev.next = prev.next.next;
                size --;
                return true;
            }
        }
        //此时不存在指定的key值
        return false;
    }

继续加油努力!!!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值