对散列冲突的处理——分离链接法

对散列冲突的处理——分离链接法

承接上一篇博文哈,上次因为去拯救中国电竞,没有写完。只说了说散列的思想和散列函数的一些讨论,这次详细的分析下对散列冲突的消除方式,会用到一些上一篇博文的一些东西,上个链接吧:这是上一篇博文的地址。
散列冲突就是两个不同的元素被散列到同一位置,那么就需要消除冲突,消除冲突的常用两种方式:分离链接法和开放定址法。开放定址法又有线性探测,平方探测,双散列三种方法。
先主要聊聊分离链接法。

分离链接法

分离链接法会将散列到同一个位置的元素保存到一个表中,可以使用标准库提供的一些方法来实现。但是,假如空间较小,正确的做法应该是避免使用这种方法。举个栗子,对0-9的完全平方数进行散列,设散列函数是hash(x)=x mod 10。tablesize不是素数是为了计算方便。看图更明白一点
这里写图片描述
进行完插入,我们现在进行一些其他操作的分析。为进行一次查找,我们用散列函数来确定要查找元素所在链表,然后在被确定链表中进行一次查找。为执行一次insert(插入),我们应该先检查元素是否存在于散列中,如果元素是新的,就将它插入链表的前端,不仅是因为方便,还因为一个常发生的情况:新插入的元素可能不久之后又被访问。对于已存在的元素,假设允许插入的话,应留出额外的域,每插入一个重复元,相应的域+1。
分离链接法实现的架构大概是这样,上个代码,我进行了详细的注释,实现了对像我这种还在成长的小白的友好23333333

public class SeparateChainingHashTable<T>
{
    //tablesize的默认大小,101是个素数
    private static final int DEFAULT_TABLE_SIZE=101;
    //已散列元素个数
    private int currentSize;
    //散列表使用一个链表数组
    private List<T>[] theLists;
    public SeparateChainingHashTable()
    {
        this(DEFAULT_TABLE_SIZE);
    }
    //带参构造,自定义大小,并进行素数修正
    public SeparateChainingHashTable(int size)
    {
        //对数组中的List进行初始化,长度为素数
        theLists=new LinkedList[nextPrime(size)];
        for(int i=0;i<theLists.length;i++)
        {
            //为数组的每个位置建立链表以解决冲突
            theLists[i]=new LinkedList<T>();
        }
    }
    //清空
    public void makeEmpty()
    {
        for (List<T> list : theLists)
        {
            list.clear();
        }
        currentSize=0;
    }
    //插入方法,对于几乎所有的插入,都有这么一个事
    //每次插入动作决定是否开始之前,都会有一次成功或不成功的查找(这不是一个概念,只是简单的一个思想)
    //也就是说当插入动作花费常数时间时,我们可以用查找的时间开销充当插入的时间开销
    //它也有一些别的用处,之后还会提到一种。
    public void insert(T t)
    {
        List<T> whichList=theLists[myHash(t)];
        if(!contains(t))
        {
            whichList.add(t);
            currentSize++;
            if(currentSize>theLists.length)
                reHash();
        }
    }
    //查找
    public boolean contains(T t)
    {
        //通过散列位置找出是哪一个链表
        List<T> whichList=theLists[myHash(t)];
        return whichList.contains(t);
    }
    //删除
    public void remove(T t)
    {
        //通过散列位置找出是哪一个链表
        List<T> whichList=theLists[myHash(t)];
        if(contains(t))
        {
            whichList.remove(t);
            currentSize--;
        }
    }
    //获得散列位置
    private int myHash(T t)
    {
        int hash=t.hashCode();
        //对每个t得到数组的序号 大小从0-theLists.length-1 进行分配
        hash %= theLists.length;
        //附加测试,防止hash值为负数,这个问题也在上一篇博文介绍过,
        if(hash<0)
            hash+=theLists.length;
        return hash;
    }
    //有一种情况是currentSize>theLists.length 须要对数组进行扩容 即再散列
    //由于列表装的太满 那么操作时间将会变得更长。且插入操作可能失败  此时的方法时新建另外一个两倍大的表
    //并且使用一个新的相关的散列函数(由于计算时tableSize变了),扫描整个原始散列表,计算每个元素的新散列值   并将它们插入到新表中
    //这种方式的开销是非常大的,贼鸡儿大
    private void reHash()
    {
        List<T>[] oldLists=theLists;//复制一下一会要用    theLists在又一次new一个
        //对在散列的表同样进行素数修正
        theLists=new LinkedList[nextPrime(2*theLists.length)];
        for(int i=0;i<theLists.length;i++)
        {
            theLists[i]=new LinkedList<T>();
        }
        //把原来的元素拷贝到新的数组中  注意是把集合中的元素复制进去
        for(int i=0;i<oldLists.length;i++)
        {
            for (T t : oldLists[i])
            {
                insert(t);
            }
        }

    }
    //表的大小是一个素数,能够保证一个非常好的分布
        private static boolean isPrime(int num)
    {
        int i=1;
        while((num%(i+1))!=0)
        {
            i++;
        }
        if(i==num-1)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    //素数修正
    private static int nextPrime(int num)
    {
        while(!isPrime(num))
        {
            num++;
        }
        return num;
    }

}

创建person类可以测试用,就事分析下此类对象的哈希值的生成原理。

public class Person {
    private String name;
    public Person(String name)
    {
        this.name=name;
    }

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

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

}

重写的hashCode是自动生成的,正好分析下源码生成hashCode的方式,跟踪一下他的调用过程。重写的hashCode是调用Objects类的静态方法hash(Object… values)这里写图片描述
这里写图片描述
Objects是jdk1.7中出现的,它被final修饰,无法被继承,构造器私有,不能new。它的方法全部被实现了,而且都被static修饰。Object是jdk1.0就有了,它俩的关系就和雷锋跟雷峰塔的关系差不多。可以把Objects理解为一个工具类。Objects中的hsah方法是这样的
这里写图片描述
它调用Arrays的hashCode()方法,传入了一个Object类型的数组。
这里写图片描述
看下这个方法,这个方法就会得出最终的哈希值。我写的测试类只传入了一个String类型的参数,在红框标注的地方发生了一个向上转型,调用了String类型的hsahCode()方法。也就是说我建立的对象哈希值的计算方式是利用name属性的哈希值计算一个关于31的多项式(因为我写的测试类比较简单,只有一个name属性,其实是应该利用Objects.hash()方法传入的参数数组,也可以说是类的属性数组)。这么说可能有些复杂,那就在看下String类型的hsahCode()方法,两个hashCode的计算方式很相似,可以类比。
这里写图片描述
源码中对hash的定义是Default to 0,value是对应字符串的字符数组。通过观察源码可以得知,string类型计算哈希值的方式是利用相关字符计算一个关于31的多项式。
至于为啥要用31呢,最主要的就是,用31计算哈希值可以在散列时有效地减少散列冲突。
对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行哈希值运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值在散列时冲突次数都小于7。在上一篇博文分析字符串类型的散列函数时,使用的就是37。源码中使用31还有一个原因是31好算,31可以被 JVM 优化,31 * i = (i << 5) - i。
把话题拉回来,对于一个散列冲突,有许许多多的方式可以解决,但是我们希望如果散列表是大的且散列函数是好的,那么链表应该是短的并且分布相对均匀。所以任何复杂的尝试都不值得考虑了,实现的越简单越好。
介绍一个概念。填装因子,用符号emmmm,就是拉姆的,跟入字长得特别像的那个符号表示。它是散列表中元素个数和tablesize的比值。在之前的例子中,就是那个0-9的完全平方数散列到长度为10的散列表中那个,拉姆的=1.0,emmmmmm我不知道怎么打这个符号,就先用‘入’了。入=1.0,也可以说是散列表的平均长度是1,在分离链接法中,这是一个很重要的数值。分离链接法的一般法则就是使表的大小与预料元素个数大致相等(即让入约等于1),如果入>1,我们就会调用rehash方法对散列表扩容,即在散列。

没啦

结束,至于开放定址法的三种方式,以后有机会再聊,代码测试过,写个main就能跑,有兴趣可以试一下,溜了溜了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值