Hash算法
在其他各种结构线性表、树等数据结构中。记录在结构中的位置是随机的,和记录keyword之间不存在确定的关系,因此。在结构中查找记录时需进行一系列和keyword的“比較”的基础上。在顺序查找时。比較的结果为“==”与“!=”两种可能;在折半查找、二叉排序树查找和B-树查找时,比較的结果为“<”、"="和“>”3种可能。查找的效率依赖于查找过程中所进行的比較次数。
理想的情况是希望不经过比較。一次存取便能得到所查记录,那就必须在记录的存储位置和它的keyword之间建立一个确定的相应关系f。使每一个keyword和结构中一个唯一的存储位置相相应。因而在查找时,仅仅要依据这个相应关系f找到给定值K的像f(k)。
若结构中存在keyword和K相等的记录,则必然在f(k)的存储位置上,由此。不须要进行比較便可直接取得所查记录。
在此,我们称这个相应的关系f为哈希(Hash)函数。按这个思想建立的表为哈希表。
哈希表是种数据结构。它能够提供高速的插入操作和查找操作。
第一次接触哈希表时。它的长处多得让人难以置信。不论哈希表中有多少数据,插入和删除(有时包含側除)仅仅须要接近常量的时间即0(1)的时间级。
实际上。这仅仅须要几条机器指令。对哈希表的使用者一一人来说。这是一瞬间的事。哈希表运算得很快,在计算机程序中,假设须要在一秒种内查找上千条记录通常使用哈希表(比如拼写检查器)哈希表的速度明显比树快,树的操作通常须要O(N)的时间级。哈希表不仅速度快,编程实现也相对easy。
哈希表也有一些缺点它是基与数组的。数组创建后难于扩展某些哈希表被基本填满时。性能下降得很严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中。这是个费时的过程)。
并且。也没有一种简便的方法能够以不论什么一种顺序〔比如从小到大)遍历表中的数据项。假设须要这样的能力,就仅仅能选择其它数据结构。
然而假设不须要有序遍历数据。井且能够提前预測数据量的大小。
那么哈希表在速度和易用性方面是无与伦比的。
散列函数能使对一个数据序列的訪问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:
我们能够举一个哈希表的最简单的样例。如果要建立一张全国34个地区的各民族人口统计表,每一个地区为一个记录,记录的各数据项为:
编号 |
地区名 |
总人口 |
汉族 |
回族 |
... |
显然,能够用一个一维数组C(1...30)来存放这张表,当中C[i]是编号为i的地区的人口情况。编号i便为记录的keyword,由它唯一确定记录的存储位置c[i]。比如:如果北京市的编号为1,则若要查看北京市的各族人口,仅仅要取出c[1]的记录就可以。
假如把这个数组看成是哈希表,则哈希函数f(key) = key。
然而,非常多情况下的哈希函数并不如此简单。
可仍以此为例。为了查看方便以地区名作为keyword。
如果。非常多情况下的哈希函数并不如此简单。
可仍以此为例。为了查看方便应以地区名作为keyword。如果地区名以汉语拼音的字符表示,则不能简单地取哈希函数f(key)=key,而是首先要将它们转化为数字,有时候还要作些简单的处理。
比如我们能够有这种哈希函数:(1)取keyword中第一个字母在字母表中的序号作为哈希函数。
比如:BEIJING的哈希函数值为字母“B”在字母表中的序号,等于02。或者(2)先求keyword的第一个和最后一个字母在字母表中的序号之和,然后推断这个和值,若比30(表长)大,则减去30.比如:TIANJIN的两个字母“T”和“N”的序号之和为34,故取04为哈希函数值;或(3)先求每一个汉字的第一个拼音字母的ASCxx码(英文字母同样)之和的八进制形式,然后将这个八进制数看成是十进制数再除以30取余数,若余数为零则加上30而为哈希函数值。
比如:HENAN的头两个拼音字母为“H”和“N”。它们的ASCxx码之和为(226)8,以(226)8除以(30)10得余数为16,则16为HENAN的哈希函数值,即记录在数组中的下标值。上述人口统计表中部分keyword在这3种不同的哈希函数情况下的哈希函数值如表下标所列:
简单的哈希函数演示样例
Key |
BEIJING(北京) |
TIANJING(天津) |
HEBEI(河北) |
SHANXI(山西) |
SHANHAI(上海) |
SHANGDONG(山东) |
HENAN(河南) |
SICHUAN(四川) |
f1(key) |
02 |
20 |
08 |
19 |
19 |
19 |
08 |
19 |
f2(key) |
09 |
04 |
17 |
28 |
28 |
26 |
22 |
03 |
f3(key) |
04 |
26 |
02 |
13 |
23 |
17 |
16 |
16 |
从这个样例可见:
(1)哈希函数是一个映像,因此哈希函数的设定非常灵活。仅仅要使得不论什么keyword由此所得的哈希函数值都落在表长同意范围之内就可以;
(2)对不同的keyword可能得到同一哈希地址,即key1。=key2,而f(key1)=f(key2),这样的现象称为冲突(collision)。
具有同样函数值的keyword对该哈希函数来说称做同义词(synonym)。
比如:keywordHEBEI和HENAN不等,但f1(HEBEI)=f1(HENAN),又如:f2(SHANXI)=f2(SHANGHAI);f3(HENAN)=f3(SICHUAN)。
这样的现象给建表造成困难,如在第一种哈希函数情况下。由于山西、上海、山东和四川这4个记录的哈希地址造成困难。如在第一种哈希函数的情况下。由于山西、上海、山东和四川这4个记录的哈希地址均为19,而C[19]仅仅能存放一个记录,那么其它3个记录存放在表中什么位置呢?而且。从上表3个不同的哈希函数的情况能够看出,哈希函数选的合适能够降低这样的冲突现象。特别是在这个样例中。仅仅可能有30个记录,能够细致分析者30个keyword的特性,选择一个切当的哈希函数来避免冲突的发生。
然而,在普通情况下,冲突仅仅能尽可能地少。而不能全然避免。由于,哈希函数是从keyword集合到地址集合的映像。通常。keyword集合集合比較大。它的元素包括全部可能的keyword。而地址集合元素仅为哈希表中的地址值。如果表长为n,则地址为0到n-1。
比如。在C语言的编译程序中可对源程序中的标识符建立一张哈希表。
在设定哈希函数时考虑的keyword集合应包括全部可能产生的keyword;如果标识符定义为以字母为首的8位字母或数字,则keyword(标识符)的集合大小(-----PS:数字大,打印不了····) 。而在一个源程序中出现的标识符合是有限的,设表长为1000足矣。地址集合中的元素为0~999。因此,在普通情况下,哈希函数是一个压缩映像,这就不可避免产生冲突。
因此。在建造哈希表时不仅要设定一个”好“的哈希函数。并且要设定一种处理冲突的方法。
综上所述,可例如以下描写叙述哈希表:依据设定的哈希函数H(key)和处理冲突的方法将一组keyword映像到一个有限的连续地址集上。并以keyword在地址集中的”像“作为记录在表中存储位置,这样的表便称为哈希表,这一映像过程称为哈希造表或散列,所得存储位置称哈希地址或者散列地址。
哈希表算法-哈希表的构造方法
1、直接定址法
比如:有一个从1到100岁的人口数字统计表,当中。年龄作为keyword,哈希函数取keyword自身。
但这样的方法效率不高,时间复杂度是O(1),空间复杂度是O(n),n是keyword的个数
2、数字分析法
有学生的生日数据例如以下:
年.月.日
75.10.03
75.11.23
76.03.02
76.07.12
75.04.21
76.02.15
...
经分析,第一位,第二位,第三位反复的可能性大。取这三位造成冲突的机会添加,所以尽量不取前三位,取后三位比較好。
3、平方取中法
取keyword平方后的中间几位为哈希地址。
4、折叠法
将keyword切割成位数同样的几部分(最后一部分的位数能够不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。
比如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字。若要以它作keyword建立一个哈希表,当馆藏书种类不到10,000时,可採用此法构造一个四位数的哈希函数。假设一本书的编号为0-442-20586-4,则:
5、除留余数法
取keyword被某个不大于哈希表表长m的数p除后所得余数为哈希地址。
H(key)=key MOD p (p<=m)
例: p =21
6、随机数法
选择一个随机函数,取keyword的随机函数值为它的哈希地址。即
H(key)=random(key) ,当中random为随机函数。通经常使用于keyword长度不等时採用此法。
5、除留余数法
取keyword被某个不大于哈希表表长m的数p除后所得余数为哈希地址。
H(key)=key MOD p (p<=m)
6、随机数法
选择一个随机函数。取keyword的随机函数值为它的哈希地址,即
H(key)=random(key) ,当中random为随机函数。通经常使用于keyword长度不等时採用此法。
5、除留余数法
取keyword被某个不大于哈希表表长m的数p除后所得余数为哈希地址。
H(key)=key MOD p (p<=m)
6、随机数法
选择一个随机函数,取keyword的随机函数值为它的哈希地址,即
H(key)=random(key) ,当中random为随机函数。
通经常使用于keyword长度不等时採用此法。
哈希表算法-处理冲突的方法
假设两个同学分别叫 刘丽 刘兰。当增加刘兰时。地址24发生了冲突,我们能够以某种规律使用其他的存储位置。假设选择的一个其他位置仍有冲突,则再选下一个,直到找到没有冲突的位置。
选择其他位置的方法有:
1、开放定址法
Hi=(H(key)+di) MOD m i=1,2,...,k(k<=m-1)
当中m为表长。di为增量序列
假设di值可能为1,2,3,...m-1。称线性探測再散列。
假设di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,...k*k,-k*k(k<=m/2)称二次探測再散列。
假设di取值可能为伪随机数列。
称伪随机探測再散列。
例:在长度为11的哈希表中已填有keyword分别为17,60,29的记录。现有第四个记录,其keyword为38。由哈希函数得到地址为5,若用线性探測再散列。例如以下:
2、再哈希法
当发生冲突时。使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间添加。
3、链地址法
将全部keyword为同义词的记录存储在同一线性链表中。
4、建立一个公共溢出区
如果哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。
/* * 题目:给定一个全部由字符串组成的字典,字符串全部由大写字母构成。当中为每一个字符串编写password,编写的 * 方式是对于 n 位字符串,给定一个 n 位数,大写字母与数字的相应方式依照电话键盘的方式: * 2: A,B,C 5: J,K,L 8: T,U,V * 3: D,E,F 6: M,N,O 9: W,X,Y,Z * 4: G,H,I 7: P,Q,R,S * 题目给出一个1--12位的数,找出在字典中出现且password是这个数的全部字符串。
字典中字符串的个数不超过5000。 * * 思路:1.回溯法找出全部可能的字符串 * 2.在字典中查找此字符串是否存在。(字典存储採用哈希表存储) * */ #include<stdio.h> #include<stdlib.h> #include<string.h> #define HASHTABLE_LENGTH 5001 //哈希表长度 #define STRING_LENGTH 13 //单词最大长度 //字符串 typedef struct { char str[STRING_LENGTH]; int length; } HString; HString string= {'\0',0}; //暂存可能的字符串 HString hashTable[HASHTABLE_LENGTH]; //哈希表 //hash函数,构造哈希表 void createHashTable(char *str) { int i,key,step=1; i=key=0; while(str[i]) { key+=str[i++]-'A'; } key%=HASHTABLE_LENGTH; while(1) { if(hashTable[key].length==0) { hashTable[key].length=strlen(str); strcpy(hashTable[key].str,str); break; } key=(key+step+HASHTABLE_LENGTH)%HASHTABLE_LENGTH; //处理冲突。线性探測再散列 if(step>0) step=-step; else { step=-step; step++; } } } //从文件里读字典 void readString() { int i; char str[STRING_LENGTH]; char ch; FILE *fp; if((fp=fopen("document/dictionary.txt","r"))==NULL) { printf("can not open file!\n"); exit(0); } i=0; while((ch=getc(fp))!=EOF) { if(ch=='\n') //读完一个字符串 { str[i]='\0'; createHashTable(str); i=0; continue; } str[i++]=ch; } if(fclose(fp)) { printf("can not close file!\n"); exit(0); } } //在哈希表中查找是否存在该字符串,存在返回1,不存在返回0 int search(char *str) { int i,key,step=1; i=key=0; while(str[i]) { key+=str[i++]-'A'; } key%=HASHTABLE_LENGTH; while(1) { if(hashTable[key].length==0) return 0; if(strcmp(hashTable[key].str,str)==0) { return 1; } key=(key+step+HASHTABLE_LENGTH)%HASHTABLE_LENGTH; //处理冲突,线性探測再散列,二次探測 if(step>0) step=-step; else { step=-step; step++; } } return 0; } //求全部可能的字符串 void getString(char* num) { int i,digit,max; if(*num==0) //递归出口,字符串已到末尾 { string.str[string.length]='\0'; if(search(string.str))//这个字符串存在于字典中。输出 puts(string.str); return; } digit=*num-'0';//取第一位字符,转成数字 if(digit>=2&&digit<=6) { i=(digit-2)*3+'A'; max=(digit-2)*3+'A'+3; } else if(digit==7) { i='P'; max='P'+4; } else if(digit==8) { i='T'; max='T'+3; } else if(digit==9) { i='W'; max='W'+4; } for(i; i<max; i++) { string.str[string.length++]=i; getString(num+1); //递归 string.length--; } } void main() { char num[STRING_LENGTH]; //因为输入的数字超出了unsigned long的范围,所以用字符串来存储 readString(); //把字典从文件里读入内存 printf("please inputer an number(1--12位,不能有0或1)\n"); scanf("%s",num); getString(num); }
版权声明:本文博客原创文章,博客,未经同意,不得转载。