哈希表的底层原理


哈希概念

线性表、树结构的查找方式都是以关键字的比较为基础,查找效率比较低,顺序表的时间复杂度是O(n),平衡树中为树的高度,即O(logn),搜素的效率取决于搜索过程的元素比较次数。

理想的搜素方法:**可以用不经过比较,一次直接从表中得到要搜素的元素。**如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与他的关键字之间能够建立一一映射的关系,那么在查找元素的时候,就可以很快寻找到该元素,这就是哈希的思想.

哈希函数

插入元素

    根据待插入的元素的关键字码,以此函数计算出该元素的存储位置并且按照此位置进行存放

搜素元素

    对元素的关键字码进行同样的计算,把求得函数值当做元素的存储位置,
    在结构中按此位置取元素比较,若关键字码相同,则搜素成功。 

此方法即为哈希(散列)方法,哈希(散列)方法中使用的转换函数称之为哈希(散列)函数,构造出来的结构就是哈希表(hashTable)(或者散列表)。

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key)= key%capacity;(capacity代表的是底层空间总大小可以理解成元素个数)

在这里插入图片描述
用该方法进行搜素的时候不必多次进行比较可以直接找到想找的关键字,但是我们就会出现一个问题:如果再次插入一个44怎么办呢?这就会引出下面的部分,哈希冲突。

哈希冲突:

概念

对于两个关键字ki,kj(i != j)有ki != kj,但是有:Hash(ki)== Hash(kj)即:不同的关键字通过哈希函数计算出相同的哈希地址,这种现象称之为哈希冲突。

冲突的避免

首先我们要明确疑点,由于我们哈希表底层数组容量往往小于实际要存储的数量,这就导致了一个问题,冲突是必然的,但是我们可以降低冲突率。

哈希函数的设计

1、直接定制法

取关键字的某个线性函数为散列地址:Hash(key)=A*key+B (即是一个y=ax+b的函数,如计数排序数字91存储在(91-90)的下标上)优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。

2、除留余数法

这个就是我们上面所介绍的方法,标准的官方说法是设散列表中允许的地址数为m(如果10个数据m的范围就是0-9),取一个不大于m的数,但是最接近或者等于m的质数p作为除数,按照哈希函数Hash(key)= key%p(p<=m),将关键字码转换为哈希地址。

还有很多的方法,但是使用的很少,所以这里不过多介绍

负载因子调节

散列表的载荷因子定义为:\alpha=填入表格的元素/散列表的长度

在java系统库中限制载荷因子为0.75如果大于这个数字就需要,降低载荷因子。


2、二次探测

线性探测的缺陷是产生冲突的元素堆积到一块,这与其找下一个元素位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免这样的问题,找下一个空位置的方法为:Hi=(H0+i²)%m或者Hi=(H0-i²)%m其中:i=1,2,3…H0是通过哈希函数计算出元素所在位置,m是表的大小。

因此:闭散列表的最大缺陷就是空间利用率低,这也是哈希的缺陷。

开散列/哈希桶(重点)

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

在这里插入图片描述
开散列,可以认为把一个大集合中的搜素问题转化到小集合中进行。

代码


public class Hashbucket {
    static class Node{
        private int key;
        private int value;
        private Node next;
 
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    public Node[] array;
    public int useSize;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public Hashbucket() {
        array=new Node[10];
    }
    public void put(int key,int value){
        Node node=new Node(key,value);
        int index= node.key%array.length;
        Node cur=array[index];
        //java1.8之后采用的是尾插法 这里采用头插法
        while(cur!=null){
            if(cur.key==key){
                cur.value=value;
                return;
            }
            cur=cur.next;
        }
        node.next=array[index];
        array[index]=node;
        useSize++;
        if(loadFactor()>DEFAULT_LOAD_FACTOR){
            resize();
        }
    }
    public void resize(){
        //二倍扩容
        Node[] tmpArray=new Node[array.length*2];
        for (Node node : array) {
            Node cur = node;
            while (cur != null) {
                Node curNext = cur.next;
                int index = cur.key % tmpArray.length;
                //头插法
                cur.next = tmpArray[index];
                tmpArray[index] = cur;
                cur = curNext;
            }
        }
        array=tmpArray;
    }
    public float loadFactor(){
        return array.length*1.0f/useSize;
    }
    public int get(int key){
        int index=key%array.length;
        Node cur=array[index];
        while(cur!=null){
            if(cur.key==key){
                return cur.value;
            }
            cur=cur.next;
        }
        return -1;
    }
}

这个代码中这个扩容部分我们要仔细的说一说,但负载因子大于0.75的时候,我们需要降低哈希冲突,因为我们的元素个数是一定的,所以我们就需要增大散列表的大小,但是当我们扩容的时候,我们会打乱原来的散列表,比如原来4和14都是在节点4的位置,当我们二倍扩容的时候我们就需要将14放到节点14的位置。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值