散列表之完美散列

  • 散列表之完美散列
  • 完美散列perfect hashing
  • 两级散列法
  • gperf工具来自动生成完美散列函数
    • gperf的安装
    • gperf关键字文件的书写格式
    • gperf应用举例

注意

本文中的所有代码你都可以在这里:

https://github.com/qeesung/algorithm/tree/master/chapter11/11-5/gperf(这里会及时更新)

或者是这里:

http://download.csdn.net/detail/ii1245712564/8936303

找到

散列表之完美散列

我们都知道散列表的查找和删除操作的平均时间代价为Θ(1),也就是说在一般情况下散列表是可以很快的找到目标元素的,但是散列表也有罢工的时候,比如设计了一个差劲的散列函数导致所有的键值都具有相同的散列值,那么查找和删除元素的代价就与操作链表差不多了

那有没有什么散列表武功秘籍之类的,遵循一定的章法就可以设计出一个最坏运行时间都是O(1)的散列函数呢?

答案是肯定的,如果关键字集合是动态的话,那么设计出适应关键字任意变化的散列函数是很难做到的,换句话说,如果我们在不知道所有关键字或者关键字变化的情况下想设计一个最坏运行时间是O(1)的散列函数是很艰难的。按照这种说法,如果在知道全部关键字,且关键字不再变化的情况下我们就可以设计这样一个接近完美的散列函数了?是的,但是设计的过程还是需要借助一些工具,后面我们将慢慢介绍

完美散列(perfect hashing)

完美散列的定义

在关键字集不再变化的情况下,运用某种散列技术,将所有的关键字存入散列表中,可以在最坏运行时间为O(1)的情况下完成对散列表的查找工作,这种散列方法就是完美散列

下面我们将介绍一种手工设计的完美散列的方法,再介绍一种工具设计完美散列的方法。

两级散列法

何为两级散列法?就是将关键字散列两次来确定目标元素的位置,且两次散列的散列方案都是采用全域散列(不懂全域散列的小伙伴看这里:全域散列法)

两级散列法大致分为下面两步:

  1. 第一级散列从全域散列函数簇中选取一个散列函数h1,将m个元素散列到n个槽中
  2. 第二级的散列有点特殊,在《散列表值链接法》里面我们知道一旦元素发生冲突,就用链表来存储所有散列值相同的元素。但是这里我们采用的是一个新的散列表,也就是二级散列表来解决一级散列表元素冲突的问题,将映射到槽j的元素再从二级散列表Sj的全域散列函数簇选取出一个散列函数h2对元素进行再次散列,最终确定目标位置。

但是为了确保二级散列表上不发生冲突,需要将散列表Sj的大小mj为散列到槽j中元素的平方,即mj=n2j

举个栗子:

利用完全散列基础存储关键字集合K={10,22,37,40,52,60,70,72,75}。外层散列函数为h(k)=((a?k+b)mod p)mod m(a=3,b=42,p=101,m=9),将散列到槽j中的元素用散列函数hj(k)=((aj?k+bj) modp) modmj

gperf工具来自动生成完美散列函数

gperf是一个完美散列函数的生成器,对于一些给定的字符串,gperf生成一个C/C++版本的散列函数和一个散列表,可以通过一个字符串在生成的散列表里面进行查找操作。散列函数是完美的,也就是说散列表四没有冲突的,在查找操作中最多只需要一次字符串的比较操作。

使用gperf一般分为下面三个步骤:

  • 首先将所有的关键字按照一定的格式写入到一个任意的文件中,后缀最好是*.gperf
  • gperf编译该写入关键字的目标文件,最终会生成一个*.hpp文件
  • 将生成的*.hpp文件用#include包含到自己的源文件中编译即可

gperf的安装

首先到gperf官网(http://www.gnu.org/software/gperf/)载gperf的源码,下载以后将是一个压缩包,对压缩包进行解压安装

cd ~/download
tar -xzvf gperf-3.0.4.tar.gz -C /tmp/gperf_source_code
cd /tmp/gperf_source_code
./configure
make
make install

现在我们来看看是否安装正确,在命令行中输入gperf -v,如果显示一下信息,说明我们已经安装成功了

GNU gperf 3.0.4

Copyright (C) 1989-1998, 2000-2004, 2006-2009 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.

Written by Douglas C. Schmidt and Bruno Haible.

gperf关键字文件的书写格式

我们可以将我们所有的关键字输入到一个文件中,然后用gperf再将该文件编译成我们需要的C/C++源码,下面我们就先来看一下关键字文件的书写格式

declarations

%%

keywords

%%

functions

文件分为三个部分,分别是declarations,keywords,functions,这三部分分别代表什么意思呢?

declarations

这部分主要是用来声明的,声明的内容主要包括一些头文件,以及keywords的结构。

那什么是keywords的结构啊?简单来说,gperf的关键字是字符串类型的,我们要通过关键字来搜寻关键字对应的数据,比如我们要在散列表里面存储一些人的信息,主要包括姓名年龄性别,我们需要通过姓名来得到额外的两个信息,于是我们可以这样定义keywords的结构

%{
enum SEX{MALE=0,FEMALE=1};
%}
struct Person_Data{char * name ; unsigned int age ; SEX sex };

其中%{...%}之间可以插入一段C代码,在gperf编译该文件的时候就可以将这段代码插入到生成的源文件中去,还有需要注意的就是结构的第一个元素必须是chat *或者是const char *类型的



keywords

在这一段里面我们主要写入我们的关键字,按照上面的栗子,我们写入的keyword结构必须要和之前在declarations中声明的结构一样

qeesung,23,MALE
tom,18,MALE
lucy,22,FEMALE


functions

functions里面主要是可以写一些C代码,在gperf编译该文件的时候,就可以将这些函数插入到生成的C/C++文件中取

对于gperf更加详细的说明以参见gperf官方文档以及文章使用gperf 实现高效的 C/C++ 命令行处理

gperf应用举例

还是上面的栗子,现在需要将人的一些信息记录到散列表中,并且需要在单位时间内就可以完成对表的查询,那么我们首先写一个关键字文件

person_data.gperf

%{
#include "person_data_type.h"
#include <cstring>
%}
struct person_data_type;
%%
qeesung,23,MALE
tom,18,MALE
beryl,20,FEMALE
cat,12,MALE
adolph,78,MALE
lucy,22,FEMALE
william,45,MALE
linus,46,MALE
jack,7,MALE
alice,20,FEMALE
%%  

gperf编译一下这个文件

gperf -t  -L C++ -N query_person -G  person_data.gperf > person_data.hpp
#-t 选项说明自定义了keywords结构体
#-L 指定输出的语言类型这里选定了C++
#-N 指定查找散列表的函数名字
#-G 将散列表设为全局可见的

最后生成一个person_data.hpp文件

最后在我们的源文件中包含person_data.hpp文件

main.cc

/**************************************************
 *       Filename:  main.cc
 *    Description:  主文件测试gperf
 *
 *        Version:  1.0
 *        Created:  2015年07月27日 19时12分13秒
 *       Revision:  none
 *       Compiler:  gcc
 *
 *         Author:  qeesung (season), [email protected]
 *   Organization:
 **************************************************/
#include <stdlib.h>
#include <iostream>
#include <string>
#include "./person_data.hpp"
#include "person_data_type.h"

using namespace std;

/**
 * +++  FUNCTION  +++
 *         Name:  main
 *  Description:  主函数
 */
int main ( int argc, char *argv[] )
{
    while(1)
    {
        string query_name;
        cout<<"Enter name >";
        cin>>query_name;
        if (query_name == ".")
            break;
        cout<<endl;
        person_data_type * result = Perfect_Hash::query_person(query_name.c_str(), query_name.length());
        if (result == NULL)
            cout<<"Can‘t find "<<query_name<<endl<<endl;
        else
        {
            cout<<"+++++++++++ "<<query_name<<" +++++++++++"<<endl;
            cout<<"age : "<<result->age<<endl;
            string sex_name = "male";
            if(result->sex)
                sex_name = "female";
            cout<<"sex : "<<sex_name<<endl<<endl;;
        }
    }
    return EXIT_SUCCESS;
}               /** ----------  end of function main  ---------- */

编译运行:

编译

g++ main.cc -o query_person



运行

./query_person



运行结果

Enter name >qeesung

+++++++++++ qeesung +++++++++++

age : 23

sex : male

Enter name >lucy

+++++++++++ lucy +++++++++++

age : 22

sex : female

Enter name >tomcat

Can’t find tomcat

Enter name >cat

+++++++++++ cat +++++++++++

age : 12

sex : male

Enter name >

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 05:08:15

散列表之完美散列的相关文章

数据结构---哈希表(散列表)

我们在Java容器中谈到:有哈希表(也称为散列表)支持的HashMap.LinkedHashSet等都具有非常高的查询效率.这其中就是Hash起的作用.顺序查找的时间复杂度为O(N) ,二分查找和查找树的时间复杂度为O(logN),而 哈希表的时间复杂度为O(1) .不过这只是理想状态,实际并不那么完美. 1.哈希表的概念和思想 哈希表是唯一的专用于集合的数据结构.可以以常量的平均时间实现插入.删除和查找. 哈希表的思想是:用一个与集合规模差不多大的数组来存储这个集合,将数据元素的关键字映射到数

查找算法总结(二分查找/二叉查找树/红黑树/散列表)

1.二分查找 二分查找时,先将被查找的键和子数组的中间键比较.如果被查找的键小于中间键,就在左子数组继续查找,如果大于中间键,就在右子数组中查找,否则中间键就是要找的元素. /** * 二分查找 */ public class BinarySearch { public static int find(int[] array, int key) { int left = 0; int right = array.length - 1; // while (left <= right)不能改为<

数据结构之散列表总结

散列表的概念 注意:    ①由同一个散列函数.不同的解决冲突方法构造的散列表,其平均查找长度是不相同的.     ②散列表的平均查找长度不是结点个数n的函数,而是装填因子α(填入表中的记录个数/散列表的槽数    n/m).因此在设计散列表时可选择α以控制散列表的平均查找长度.(平均查找长度=总查找(插入)/记录个数)          通过链接法解决冲突:成功查找的期望查找长度O(1+a), 不成功查找的平均查找长度也为O(1+a).         开放寻址解决冲突:引入探查序列,对于a<

初识java集合——散列表(HashTable)

[散列表]为每个对象计算一个整数,称为散列码(是由对象的实例域产生的一个整数)更确切的说 * 不同实例域的对象产生不同的散列码 * * 如果自定义类,就要负责实现这个类的hashcode,注意:自己实现的hashcode方法应该与equals方法兼容 * 即如果a.equals(b) 为true a和b必须具有相同的散列码 * * 在java中,[散列表]用链表数组实现.每个列表被称为桶.想要查找表中对象,先计算散列码,然后与桶的总数取余 * 所的余数就是桶的索引.当桶中没有其他元素时,很幸运可

数据结构与算法——散列表类的C++实现(分离链接散列表)

散列表简介: 散列表的实现常被称为散列.散列是一种用于以常数平均时间执行插入.删除和查找的技术. 散列的基本思想: 理想的散列表数据结构只不过是一个包含一些项的具有固定大小的数组.(表的大小一般为素数) 设该数组的大小为TbaleSize,我们向该散列表中插入数据,首先我们将该数据用一个函数(散列函数)映射一个数值x(位于0到TbaleSize1-1之间):然后将该数据插入到散列表的第x的单元.(如果有多个数据映射到同一个数值,这个时候就会发生冲突) 散列函数介绍: 为了避免散列函数生成的值不是

散列表之链接法

散列表之链接法 散列表的定义 散列表的基本操作 散列表的编码实现 散列表的设计 主测试文件 编译运行 结论 注意: 本文中的所有代码你可以在这里 https://github.com/qeesung/algorithm/tree/master/chapter11/11-2/hashTable(这里会及时更新) 或者这里 http://download.csdn.net/detail/ii1245712564/8804203 找到 散列表之链接法 在之前的文章<散列表之直接寻址表>中介绍了散列表

散列表(算法导论笔记)

散列表 直接寻址表 一个数组T[0..m-1]中的每个位置分别对应全域U中的一个关键字,槽k指向集合中一个关键字为k的元素,如果该集合中没有关键字为k的元素,则T[k] = NIL 全域U={0,1,…,9}中的每个关键字都对应于表中的一个下标值,由实际关键字构成的集合K={2,3,5,8}决定表中的一些槽,这些槽包含指向元素的指针,而另一些槽包含NIL 直接寻址的技术缺点非常明显:如果全域U很大,则在一台标准的计算机可用内存容量中,要存储大小为|U|的一张表T也许不太实际,甚至是不可能的.还有

算法导论 第11章 散列表

散列表是主要支持动态集合的插入.搜索和删除等操作,其查找元素的时间在最坏情况下是O(n),但是在是实际情况中,散列表查找的期望是时间是O(1),散列表是普通数组的推广,因为可以通过元素的关键字对数组进行直接定位,所以能够在O(1)时间内访问数组的任意元素. 1.直接寻址表 当关键字的全域较小,即所有可能的关键字的量比较小时,可以建立一个数组,为所有可能的关键字都预留一个空间,这样就可以很快的根据关键字直接找到对应元素,这就是直接寻址表,在直接寻址表的查找.插入和删除操作都是O(1)的时间.. 2

什么是散列表?(正在整理学习中)

什么是散列表?为什么要用散列表?数组的特点是:寻址容易,插入和删除困难:链表的特点是:寻址困难,插入和删除容易:那么能不能综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构?答案是肯定的,这就是我们要了解的散列表,也叫哈希表 HashMap具有优秀的查找性能.根据key找到value,性能最好的算法!(没有之一). Map(HashMap) 具有优秀的查找性能. 是根据key找到value,性能最好的算法!(没有之一).无论数据多少,查找方法(get)的性能始终如一!而散列表:为了实现