算法--查找--散列表查找

来源:互联网 发布:乐视1s移动数据开关 编辑:程序博客网 时间:2024/04/30 02:23

查找除了线性表的查找(顺序、二分、分块),树上查找(BST、B-树),还有一种散列查找。

相比于前两种查找,散列表的查找效率惊人的高O(1),它采用的直接寻址技术,基本上就是实现了精准打击,达到一击而中的效果。

所谓的直接寻址技术,说白了,就是存储记录时,通过散列函数计算出一个散列地址;读取记录时,通过同样的散列函数计算出地址,然后按照地址访问记录。整个过程有点类似于数学中的映射,一个或者多个key通过映射对应到一个地址,同样也不会存在多个地址对应一个key的情况。

这里提到了一个关键:散列函数

散列函数,其实就是一种映射关系。它的选取,一般会遵循两个原则:从实现上说要求简单,从效果上说要求均匀。认真想一下就不难明白,我们的散列查找本来就要求高效,如果在这个计算地址的过程山一重水一重,那岂不是南辕北辙了?所以越简单越好。那为什么要求均匀呢?这就涉及到另外一个问题,冲突。不妨先假想一下,如果采用的散列函数计算出来的地址个个都一样,再不做特殊处理的话,取值访问的时候,那不是目瞪口呆无从下手了。

那到底该如何选取所谓的散列函数呢,在前辈们的探索中,总结出来一些具有代表性的方法,这里就直接贴出来了。

散列函数的构造

  • 直接定址
    名字听起来很高大上,其实是最简单的。
    举个例子来说,现在要统计一下班级同学的年龄分布,我们看一下数据,1990年的有2人,1991年的有8人,1992年的有10人…,1999年的有1人。不难发现,所有的时间(key)都是199开头的,我们存储的时候,完全可以用0,1,2…来表示相应的年份,即f(key) = key -1990。

    特点:实现简单,但存储前必须了解数据分布。

  • 数字分析
    既然能分析的,必然是有规律的,不然再分析也没什么卵用。
    看个例子,比如要登记公司的员工信息,用手机号做关键字。我们发现公司员工竟然用的都是形如135XXXX1234的手机号,顺便普及一下,135代表号段,XXXX代表地区识别号,1234才是真正的用户编号。存储的时候,何不用直接用最后四位来表示用户的散列地址呢?

    特点:实现简单,存储前必须了解数据分布,数据必须有规律。

  • 平方取中
    通俗易懂,如名所述。将关键字平方之后,取中间的若干位最为散列地址

    特点:可以没规律,适合关键字较小的数据

  • 折叠
    这个比较特别。
    当关键字比较大的时候,平方运算就比较吃力了。这个时候可以考虑折叠。折叠的核心在于将关键字从左到右平均拆成若干份,位数不够的补0,然后把这几部分求和,最后的结果作为散列地址。

    特点:可以没规律,适合关键字较大的数据

  • 随机数法
    比较简单,如名所述。
    把关键字当成种子,生成随机数作为散列地址。值得说明的是,这里的随机只是一种伪随机,不然访问数据的时候找不到地址,可就尴尬了。

    特点:适合关键字长度不等

  • 除数留余
    这个应该是用的比较多的。
    核心思想一般是选取表长m作为除数,用关键字key除以m,所得的余数作为散列地址。

    特点:除数的选取至关重要

冲突处理

前面已经说过,不管采用哪种方法构造出来的散列函数,最终生成的散列地址都有可能重复,也就是所谓的冲突,只是概率的大小问题。

当冲突不可避免时,就不得不考虑解决方案了。这里介绍常见的四种。

  • 开放定址法
    不知道谁起的名字,简直无语之极。说穿了,就是一旦发生了冲突,就去寻找下一个地址,只要散列表足够大,总能找到适合的地址。这模式,跟丑旦我找女朋友一毛一样,好不容易喜欢上一个姑娘,可人家不愿意,我又不想等,那怎么办,赶紧换下一家呗。

    用数学的语言描述一下这个过程,就是

    fi(key) = (f(key)+di) MOD m (di=1,2,…,m-1)di的选取,可以线性,也可以选择平方或者随机数,来提升均匀性。
  • 链地址法
    把它俩放一块说,可以比较着来看。

    还说前面的找女朋友吧,人家不愿意,但是丑旦我脑子被偶像剧安利多了,决定要等等看,说不定女神有一天就能够回眸一笑回心转意,我等啊等,女神没有垂青,我擦嘞反倒是等来了两个情敌…最让我气愤的是,这两个情敌竟然也是个痴情的种子,那咋办嘞?不如组成个联盟,一块等吧。我先来的我是资深备胎,你后来的你是一般备胎,他最后来的他是实习备胎…

    假设关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},使用除留余数法求散列表。

    链地址法
    图片比较丑,但能说明情况,其中*表示初始化的默认值,一般是key不可能取到的值。

  • 再哈希
    再哈希的核心思想就是,如果一个哈希函数不能保证不重复,那就用两个。经过两个不同的哈希函数计算出来的结果重复的概率就大大降低,所以再哈希的性能是比较好的。

    用数学语言描述一下就是:

    hi=( h(key)+i*h1(key) )%m    0≤i≤m-1 结果显然是由h(key)和h1(key)共同决定的。
  • 公共溢出区
    这个就更直白了,开辟两张表:基本表和溢出表,将所有冲突的“情敌”单独存放到溢出表中,可以参考下面的例子。

    例:假设关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},同样使用除留余数法求散列表。
    公共溢出区
    依然比较丑,*表示初始化的默认值,一般是key不可能取到的值。

代码实现

下面使用“除数留余”和“开放地址”来模拟一下哈希查找。
要插入的数据是{ 12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34 };
老规矩,能上代码就不多说话。

public class HashSearch {    public static int defaultValue = -1111;    /**     * <p>name: main</p>     * <p>description: </p>     * <p>return: void</p>     */    public static void main(String[] args) {        // TODO Auto-generated method stub        Hash hash = initTable(12);        int[] array = { 12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34 };        for (int i = 0; i < array.length; i++) {            insert(array[i], hash);        }        for (int i = 0; i < array.length; i++) {            System.out.println("" + search(array[i], hash));        }    }    public static class Hash {        int length;        int[] address;        public Hash(int length) {            this.length = length;        }    }    //初始化    public static Hash initTable(int size) {        Hash hash = new Hash(size);        hash.address = new int[size];        for (int i = 0; i < hash.address.length; i++) {            hash.address[i] = defaultValue;        }        return hash;    }    //计算哈希值    public static int computeHash(int key,Hash hash) {        if (hash.length > 0) {            return key % hash.length;        }        return defaultValue;    }    //插入    public static void insert(int key, Hash hash) {        int index = computeHash(key, hash);        while (hash.address[index] != defaultValue) {// 不是默认值,说明冲突            index = (index + 1) % hash.length;// 开放定址法的线性探测,每次加1,往后寻找        }        hash.address[index] = key;    }    //查找    public static boolean search(int key, Hash hash) {        int index = computeHash(key, hash);        while (hash.address[index] != key) {            index = (index + 1) % hash.length;            /**             * di取值是(0,length-1].             * index==computeHash(key,hash),说明已经加够一个周期了,依然不存在,即可返回;             * hash.address[index] == defaultValue,说明出现了空位依然没有找到key,即可返回.             */            if (hash.address[index] == defaultValue                    || index == computeHash(key, hash)) {                return false;            }        }        System.out.println("找到" + key + "了,它在表中的第" + index + "个位置");        return true;    }}

程序的输出:

找到12了,它在表中的第0个位置true找到67了,它在表中的第7个位置true找到56了,它在表中的第8个位置true找到16了,它在表中的第4个位置true找到25了,它在表中的第1个位置true找到37了,它在表中的第2个位置true找到22了,它在表中的第10个位置true找到29了,它在表中的第5个位置true找到15了,它在表中的第3个位置true找到47了,它在表中的第11个位置true找到48了,它在表中的第6个位置true找到34了,它在表中的第9个位置true

手动计算的结果:
除数取余
good,两下吻合。
哈希查找就先研究到这里,欢迎留言拍砖嘞。

1 0
原创粉丝点击