1 简介
哈希表:又称散列表,是一种根据给定的关键字来计算关键字在表中地址的数据结构,即散列表建立了关键字
和存储地址
之间的一种直接映射关系。
哈希函数:又称散列函数,将给定关键字映射为该关键字对应的地址的函数,记为Hash(key)=Addr。
哈希冲突:散列函数将两个以上的不同关键字映射到同一个地址,这种情况成为哈希冲突
,这些冲突的不同关键字称为同义词
。
2 哈希函数和冲突处理方法
2.1 构造哈希函数的要点
- 哈希函数定义域必须包含
需要存储的全部关键字
,值域的范围依赖于哈希表的大小或者地址范围。 - 哈希函数计算出来的地址应该能等概率,均匀地分布在整个地址空间,减少冲突的发生。
- 哈希函数应该尽量简单,计算时间短
2.2 常用的Hash函数的构造方法
2.2.1.直接定址法
直接取关键字的某个线性函数值
为散列地址,散列函数为Hash(key)=a*key+b,其中,a和b是常数。这种方法计算最简单,不会产生冲突。
2.2.2.取模法
假定哈希表表长为m,取一个不大于m但最接近或等于m的质数p
,用取模运算把关键字转换成哈希地址。散列函数为Hash(key)=key%p。该方法的关键是选好p,使得每一个关键字通过该函数转换后等概率的映射到散列空间上任一地址,从而尽可能的减少冲突的可能性。
2.2.3.数字分析法
设关键字是r进制数(如十进制),而r个数码在各位上出现的概率不一定相同,可能在某些位上分布均匀些,每种数码出现的几率均等;而在某些位上分布不均匀,只有集中数码经常出现,则应选取数码分布较为均匀的若干位做为散列地址。这种方法是用于已知的关键字集合
。
2.2.4.平方取中法
取关键字的平方值的中间几位作为散列地址。具体取多少位要看实际情况而定。这种方法得到的散列地址与关键字的每一位都有关系,是的散列分布比较均匀
。
2.2.5.折叠法
将关键字拆分为相同的几个部分(最后一部分位数可以短一些),然后取这部分的叠加和
作为散列地址,这种方法称为折叠法。这种方法适合用于关键字位数较多且关键字中每一位数字分布大致均匀时。
### 2.3 常用Hash函数冲突处理方法
2.3.1.开放定址法
将产生冲突的Hash地址作为自变量,通过某种冲突解决函数得到一个新的空闲的Hash地址。
2.3.1.1 线性探测法
冲突发生时,顺序查看表中的下一单元(循环),直到出现一个空闲单元或者查遍全表。(表不为空的时候一定可以找到)
缺点:会造成大量元素在相邻散列地址上聚集
起来,大大降低了查找效率。
2.3.1.2 平方探测法
设发生冲突的地址为d,平方探测法得到新的地址序列为d+1²,d-1²,d+2²,d-2²......平方探测法是一种比较好的探测方法,可以避免出现聚集
问题。
缺点:不能探测到哈希表上的所有单元,但是至少能探测一半单元。
2.3.1.3 再哈希法
又称双哈希法。需要使用两个散列函数,当通过第一个散列函数Hash(key)得到的地址发生冲突时,则利用第二个散列函数Hash2(key)计算该关键字的地址增量。哈希函数为Hi=(H(key)+i*Hash2(key))%m,其中m是表长,i是冲突次数。
2.3.1.4 伪随机序列法
发生哈希冲突时,地址增量为随机数序列,称为伪随机序列法。
2.3.2 拉链法
对于不同的关键字可能会通过哈希函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一表示。拉链法是用于经常进行插入和删除的情况。
3 散列表的查找
给定一个关键字key
,根据哈希函数计算其哈希地址,然后检查哈希地址有没有关键字。
- 如果没有,表明该关键字不存在,返回查找失败;
- 如果有,则检查该记录是否等于关键字,1)如果等于,返回改字,查找成功。2)如果不等,则按照给定的冲突解决办法计算悬疑散列地址,再执行上述过程。
4 散列表查找性能
散列表查找性能跟装填因子
有关,一般记为α,定义为一个表的装满程度
。计算方法为 α=表中记录数n/表长m
散列表的平均查找长度依赖于散列表的装填因子α,而不直接依赖于n或者m。α越大,表示装天的纪录越满,发生冲突的可能性就大,反之发生冲突的可能性越小`。
5 实例
5.1 问题描述
设计散列表,实现电话号码查找系统。设电话号码簿长度为n(0≤n≤10000),系统应该实现如下工作:
⑴ 电话号码簿保存在磁盘文件中,每一条电话号码记录包含数据项:编号(唯一),用户名,通信地址,电话号码(手机号)
⑵ 创建散列表:系统运行时,读取磁盘文件的电话号码,构建散列表,用于查询。要求:自选散列函数(至少2种),自选解决冲突的方法(至少2种),分别以电话号码和用户名为关键字,建立散列表。
⑶ 查询:根据输入的用户名,查找并显示给定用户的信息。
⑷ 性能分析:
① 计算并输出不同散列函数、不同解决冲突方法的平均查找长度。
② 通过改变装填因子、改变哈希函数等方式,改善平均查找长度:通过数据表、柱形图、折线图等方式,记录实验数据的变化情况,对影响平均查找长度变化的原因进行分析。
5.2 问题分析
以电话号码为关键字建立哈希表,散列函数选用折叠法,将手机号的后八位拆分成两个四位,然后相加再模哈希表长度得到地址。,如图一
图一
其中,x,y分别是输入手机号后八位的前四位与后四位,10000是哈希表的长度。
以姓名为关键字建立哈希表,哈希函数选用平方取中法,由于我生成的随机数据中姓名是由4个字符组成,因此计算四个字符与’a’的差值再乘10/27得到一个4位10进制数字,即是散列地址。具体操作如图二
初始散列因子为0.7,哈希表长度是10000,数据量是7000条。
解决冲突办法分别是1.线性探测法,即出现冲突线性寻找下一个非空位置放数据,缺点是容易产生数据堆积。2.其次是平方探测法,平方探测法得到新的地址序列为d+1²,d-1²,d+2²,d-2²……计算如图三
5.3 实验结果及分析
(1)实验数据描述
电话簿初始为7000条数据,哈希表长固定为10000,每一条记录包含四个字段,分别是id(唯一),四位随机字符串姓名,20位随机字符串地址,150开始,后八位随机数字表示电话号码。以txt文件存储在磁盘中,每一行数据就是一条记录,读取/写入一次操作一行。
(2)实验结果
某一次程序输入结果如图四
α为装填因子,AVL为平均查找长度。
- 电话号码为关键字,哈希方法为折叠法,解决冲突方法线性探测法。
- 电话号码为关键字,哈希方法为折叠法,解决冲突平方探测法。
- 姓名为关键字,哈希方法为平方取中法,解决冲突方法线性探测法。
- 姓名为关键字,哈希方法为平方取中法,解决冲突平方探测法。
5.4 结论
结论:
根据与图五可知,由于哈希函数和冲突处理方法不同,以及关键字不同,建立哈希表的查找性能也不同,α越大,即表填的越‘满’,查找性能越低,反之查找性能越高。
6 源代码
代码写的渣渣,能测试就行。
实体类:
package com.sufu.data.structure.experiments;
import java.io.Serializable;
/**
* @author sufu
* @version 1.0.0
* @date 2020/5/17 3:27
* @description 电话号码实体类
*/
public class PhoneNumber implements Serializable {
private int id;
private String username;
private String stress;
private String phoneNumber;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getStress() {
return stress;
}
public void setStress(String stress) {
this.stress = stress;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String num) {
this.phoneNumber = num;
}
public PhoneNumber(int id, String username, String stress, String phoneNumber) {
this.id = id;
this.username = username;
this.stress = stress;
this.phoneNumber = phoneNumber;
}
public PhoneNumber(String s){
String[] info = s.split(",");
this.id = Integer.parseInt(info[0]);
this.username = info[1];
this.stress = info[2];
this.phoneNumber = info[3];
}
@Override
public String toString() {
return id+","+username+","+stress+","+phoneNumber;
}
}
主函数:
package com.sufu.data.structure.experiments;
import java.io.*;
import java.util.Date;
/**
* @author sufu
* @version 1.0.0
* @date 2020/5/17 3:29
* @description
*/
public class Main {
static char[] CHARS = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N', 'O','P','Q','R','S','T','U','V','W','X','Y','Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
static char[] NUMS = {'0','1','2','3','4','5','6','7','8','9'};
static String FILE_PATH = "C:UsersDELLDesktop数据结构";
static int DATA_SIZE = 8000;//数据大小
static int HASH_TABLE_LENGTH = 10000;//哈希表长
static int HASH_TYPE = 1;//哈希方法选用类型 1或非1
static int DEAL_TYPE = 2;//冲突处理方法选用类型 1或非1
static double CPMPARETIMES = 0;
static int ADDR = 1;//解决冲突方法2中的地址增量
static boolean ISADD = false;//结局冲突方法2中是否是加号
public static void main(String[] args) {
System.out.println("创建随机数据.....");
getRandomData();//创建DATA_SIZE条测试数据,输出创建时间
System.out.println("初始化哈希表中..............");
PhoneNumber[] hashTable = init();//创建并返回哈希表
System.out.println("初始化完成!");
// int op;
// while(true){
// System.out.println("欢迎使用! 输入编号执行响应操作:n1.根据电话号码查找 2.根据姓名查找 3.性能分析n输入-1结束");
// Scanner scanner = new Scanner(System.in);
// op = scanner.nextInt();
// if(op == 1){
// System.out.println("请输入电话号码:");
// String s = scanner.next();
// PhoneNumber result = search(s, hashTable);
// if(result!=null){
// System.out.println(result);
// }else
// System.out.println("查找失败,没有该用户!");
// System.out.println("比较次数:"+compareTimes);
// }else if(op == 2){
// System.out.println("2");
// }else if(op == -1)
// break;
// else
// System.out.println("输入有误");
// }
PhoneNumber[] original = readFromDisk();//源数据
for(int i =0;i<DATA_SIZE;i++){
if(search(original[i].getPhoneNumber(),hashTable)==null){
System.out.println("false");
};
}
// for(int i =0;i<DATA_SIZE;i++){
// if(search(original[i].getUsername(),hashTable)==null){
// System.out.println("false");
// };
// }
System.out.println(CPMPARETIMES /DATA_SIZE);
}
static PhoneNumber search(String key,PhoneNumber[] hashTable){
int index;
if(HASH_TYPE == 1){
index = hash1(key);
}else {
index = hash2(key);
}
while(true){
PhoneNumber re = hashTable[index];
CPMPARETIMES++;
if(re == null){
return null;
}
else if(re.getPhoneNumber().equals(key)){
return re;
}else {
if(DEAL_TYPE == 1){
index = deal1(index);
}else {
index = deal2(index);
}
}
}
}
public static PhoneNumber[] init(){
PhoneNumber[] data = readFromDisk();
PhoneNumber[] hashTable = new PhoneNumber[HASH_TABLE_LENGTH];
String num;
int index,count = 0;
for(int i =0;i<data.length;i++){
num = data[i].getPhoneNumber();
if(HASH_TYPE==1){
//选择第一个哈希函数
index = hash1(num);
}else {
//否则是第二个哈希函数
index = hash2(num);
}
while(true){
if(hashTable[index]==null){
//无冲突 直接存放数据
hashTable[index] = data[i];
break;
}else {
if(DEAL_TYPE == 1){
index = deal1(index);
}else {
index = deal2(index);
}
}
}
}
saveItemsToDisk(FILE_PATH+"test.txt",hashTable);
return hashTable;
}
public static String getRandomString(Integer len,char[] chars){
char[] chrs = new char[len];
int index = 0;
for(int i=0;i<len;i++){
index = (char) (Math.random()*chars.length);
chrs[i] = chars[index];
}
return String.valueOf(chrs);
}
public static void saveItemsToDisk(String path,PhoneNumber[] items){
File file = new File(path);
if(file.exists()){
file.delete();
}
try {
file.createNewFile();
FileWriter fileWriter = new FileWriter(file);
PrintWriter printWriter = new PrintWriter(fileWriter);
for (PhoneNumber item : items) {
printWriter.println(item);
}
printWriter.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void getRandomData(){
Date date1 = new Date();
int index = 0;
PhoneNumber[] items = new PhoneNumber[DATA_SIZE];
for (int i=0;i<items.length;i++) {
items[i] = new PhoneNumber(index+","+getRandomString(4,CHARS)+","+getRandomString(20,CHARS)+","+"150"+getRandomString(8, NUMS));
index++;
}
saveItemsToDisk(FILE_PATH+"data.txt", items);
Date date2 = new Date();
System.out.println(date2.getTime()-date1.getTime()+"ms");
}
public static PhoneNumber[] readFromDisk(){
PhoneNumber[] phoneNumber = new PhoneNumber[DATA_SIZE];
File filePath = new File(FILE_PATH+"data.txt");
if (filePath.exists()){
try {
FileReader fileReader = new FileReader(filePath);
BufferedReader reader = new BufferedReader(fileReader);
for (int i=0;i<DATA_SIZE;i++){
phoneNumber[i] = new PhoneNumber(reader.readLine());
}
return phoneNumber;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
static int hash1(String input){
//折叠法,输入电话号码后八位,得到索引,此法只针对输入电话号码查找
//input = input.substring(3, 11);//取后八位
int x = Integer.parseInt(input.substring(0, 4));//去后八位中的前四位
int y = Integer.parseInt(input.substring(4, 8));//取后八位中的后四位
return (x+y)%HASH_TABLE_LENGTH;
}
static int hash2(String input){
//平方取中法,输入四个字符,计算每个字符与字符a的差值做平方再取中间四位
int index = 0;
for(int i =0;i<input.length();i++){
index = index + (int)((input.charAt(i)-'a')*10/27*Math.pow(10,3-i));
}
index*=index;
return (index/100)%HASH_TABLE_LENGTH;
}
static int deal1(int input){
//线性探测法解决冲突
return ++input%HASH_TABLE_LENGTH;
}
static int deal2(int input){
//平方探测法解决哈希冲突
if(ISADD){
input = input+ADDR*ADDR;
ADDR++;
}else
input = (HASH_TABLE_LENGTH+input-ADDR*ADDR)%HASH_TABLE_LENGTH;
return input;
}
}