一、哈希表基本介绍
哈希表(Hash table,又称散列表),是根据关键码值(Key value)而直接进行访问的数据结构。其通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
实现哈希表的两种方法:
(1)数组+链表
(2)数组+红黑二叉树
二、哈希函数
哈希函数:建立起数据元素的存放位置与数据元素的关键字之间的对应关系的函数。即使用哈希函数可将被查找的键转换为数组的索引。理想情况下它应该运算简单并且保证任何两个不同的关键字映射到不同的单元(索引值)。但是很多时候我们都需要处理多个键被哈希到同一个索引值的情况,即哈希冲突。
以下是哈希函数的构造方法:
1、直接定址法
取关键字或关键字的某个线性函数值为哈希地址。即H(key)=key 或 H(key)=a*key+b (a,b为常数)。
2、数字分析法
若关键字是以r为基的数(如:以10为基的十进制数),并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
3、平方取中法
取关键字平方后的中间几位为哈希地址。
4、折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。适用于关键字位数比较多,且关键字中每一位上数字分布大致均匀时。
5、除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址(p为素数),即H(key)=key MOD p,p<=m (最简单,最常用)p的选取很重要。一般情况,p可以选取为质数或者不包含小于20的质因数的合数(合数指自然数中除了能被1和本身整除外,还能被其他数(0除外)整除的数)。
6、随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址。即H(key)=random(key),其中random为随机函数。适用于关键字长度不等时。
三、哈希冲突解决
1、开放地址法
开放地址法:通过系统的方法找到系统的空位(三种:线性探测、二次探测、再哈希法),并将待插入的元素填入,而不再使用用hash函数得到数字作为数组的下标。
(1)线性探测:假若当前要插入的位置已经被占用了之后,沿数组下标递增方向查找,直到找到空位为止。
(2)二次探测:二次探测和线性探测的区别在于二次探测的步长是,若计算的原始下标是x则二次探测的过程是x+12,x+22,x+32,x+42,x+52随着探测次数的增加,探测的步长是探测次数的二次方(因此名为二次探测)。二次探测会产生二次聚集:即当插入的几个数经过hash后的下标相同的话,那么这一串数字插入的探测步长会增加很快。
(3)再hash法:为了消除原始聚集和二次聚集,把关键字用不同的hash函数再做一遍hash化,用过这个结果作为探测的步长,这样对于特定的关键字在整个探测中步长不变,但是不同的关键字会使用不同的步长。stepSize = constant - (key % constant) 这个hash函数求步长比较实用,constant是小于数组容量的质数。(注意:第二个hash函数必须和第一个hash函数不同,步长hash函数输出的结果值不能为0)。
2、链地址法
链地址法 :创建一个存放单词链表的数组,数组内不直接存放元素,而是存储元素的链表。发生冲突的时候,数据项直接接到这个数组下标所指的链表中即可。
优势:填入过程允许重复,所有关键值相同的项放在同一链表中,找到所有项就需要查找整个是链表,稍微有点影响性能。删除只需要找到正确的链表,从链表中删除对应的数据即可。表容量是质数的要求不像在二次探测和再hash法中那么重要,由于没有探测的操作,所以无需担心容量被步长整除,从而陷入无限循环中。
四、使用哈希表管理公司员工信息
1、题目
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id, 性别, 年龄, 电话),当输入该员工的 id 时,要求查找到该员工的所有信息。
要求:
(1)不使用数据库,速度越快越好
(2)添加时,保证按照id从低到高插入
(3)使用链表来实现哈希表, 且链表不带表头
2、代码实现
package Hash;
import java.util.Scanner;
public class Hash {
public static void main(String[] args) {
hashtable hash=new hashtable(7);
int n;
Scanner sc=new Scanner(System.in);
while(true) {
System.out.println("1: 添加雇员");
System.out.println("2: 显示雇员");
System.out.println("3: 查找雇员");
System.out.println("4: 退出系统");
n = sc.nextInt();
switch (n) {
case 1:
System.out.println("输入id");
int id = sc.nextInt();
System.out.println("输入名字");
String name = sc.next();
System.out.println("输入性别");
String sex = sc.next();
System.out.println("输入电话");
String phone = sc.next();
Employee emp=new Employee(id,name,sex,phone);//创建员工
hash.add(emp);
break;
case 2:
hash.show();
break;
case 3:
System.out.println("请输入要查找的员工id");
id = sc.nextInt();
hash.find(id);
break;
case 4:
sc.close();
System.exit(0);
default:
break;
}
}
}
}
//员工类
class Employee{
public int id;//员工id
public String name;//员工名字
public String sex;//员工性别
public String phone;//员工电话
public Employee next;//指向下一个员工指针,默认为null
public Employee(int id, String name,String sex,String phone) {//Employee的构造函数
super();
this.id = id;
this.name = name;
this.sex = sex;
this.phone = phone;
}
}
//员工信息链表
class EmployeeList{
private Employee head;//头指针,指向第一个Employee
//添加员工
public void add(Employee emp) {
if(head==null) {//添加第一个员工
head=emp;
return;
}
Employee cur =head; //辅助指针
while(true) {
if(cur.next==null) {//链表已经到尾部
break;
}
cur=cur.next;//cur后移继续遍历
}
cur.next=emp;//退出while循环时表示已经到链表尾,直接在链表尾部添加上员工
}
//遍历员工信息
public void show(int no) {
if(head==null) {
System.out.println("第 "+(no+1)+" 条链表为空");
return;
}
System.out.print("第 "+(no+1)+" 条链表的信息为");
Employee cur =head; //辅助指针
while(true) {
System.out.printf(" => id=%d name=%s sex=%s phone=%s\t", cur.id, cur.name,cur.sex,cur.phone);//遍历一次输出信息一次
if(cur.next == null) {//链表已经到尾部
break;
}
cur=cur.next; //cur后移继续遍历
}
System.out.println();
}
//根据id查找员工
public Employee find(int id) {
if(head==null) {//空链表
System.out.println("链表为空");
return null;
}
Employee cur =head; //辅助指针
while(true) {
if(cur.id==id) {//找到id即退出while循环
break;
}
if(cur.next==null) {//遍历完当前链表没有找到该员工
cur=null;
break;
}
cur=cur.next; //cur后移继续遍历
}
return cur;
}
}
//HashTable管理多条链表
class hashtable{
private EmployeeList[] employeelist;
private int size;//链表总数
//hashtable的构造函数
public hashtable(int size) {
this.size=size;
employeelist=new EmployeeList[size];//初始化employeelist
for(int i=0;i<size;i++) {
employeelist[i]=new EmployeeList();
}
}
//添加员工
public void add(Employee emp) {
int employeeNo=hashFun(emp.id);//根据员工的id,得到该员工应当添加到哪条链表
employeelist[employeeNo].add(emp);//将emp 添加到对应的链表中
}
//遍历hashtable
public void show() {
for(int i=0;i<size;i++) {
employeelist[i].show(i);
}
}
//根据id查找员工
public void find(int id) {
//使用散列函数确定到哪条链表查找
int employeeNo = hashFun(id);
Employee emp = employeelist[employeeNo].find(id);
if(emp!= null) {//找到
System.out.printf("在第%d条链表中找到 雇员 id = %d name = %s sex = %s phone = %s", (employeeNo + 1), id,emp.name,emp.sex,emp.phone);
System.out.println();
}else{
System.out.println("在哈希表中,没有找到该雇员~");
}
}
//散列函数--除留余数法
private int hashFun(int id) {
return id%size;
}
}
运行结果展示:
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1001
输入名字
Jack
输入性别
男
输入电话
111
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1002
输入名字
Tom
输入性别
男
输入电话
222
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1003
输入名字
Lucy
输入性别
女
输入电话
333
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1008
输入名字
Simth
输入性别
男
输入电话
444
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
2
第 1 条链表的信息为 => id=1001 name=Jack sex=男 phone=111 => id=1008 name=Simth sex=男 phone=444
第 2 条链表的信息为 => id=1002 name=Tom sex=男 phone=222
第 3 条链表的信息为 => id=1003 name=Lucy sex=女 phone=333
第 4 条链表为空
第 5 条链表为空
第 6 条链表为空
第 7 条链表为空
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
3
请输入要查找的员工id
1002
在第2条链表中找到 雇员 id = 1002 name = Tom sex = 男 phone = 222
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
4