一.概述
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列(哈希)函数, 映射过程叫做哈希化,存放记录的数组叫做散列表。使用哈希函数向数组插入数据后,这个数组就是哈希表。
哈希函数和散列值(哈希值), 散列值(哈希值)就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
二.为什么需要哈希函数
1.问题与解决方案分析
现有一批员工(5000) , 员工信息包括(姓名,性别,年龄,地址等信息) ,要存入内存中, 如果我们将每一个员工作为数组的一个单元存放,那么数组的大小是5000,同时可以用数组下标存取单词,那么姓名和下标要怎样建立联系呢?
于是想到了Object的hashCode()方法可以将字符串转化为数字,而hashcode的取值范围是Integer.MIN_VALUE ---- Integer.MAX_VALUE之间约正负21亿, 这个结果是很巨大的,在实际内存中,根本不可能为一个数组分配这么大的空间。
于是需要将我们就需要将这正负21亿的范围,压缩到从0到5000的范围。
于是想到了取余这种方法,任何数对5000取余那么得到的结果必定在0-5000范围内,取余后的余数就是数组的下标, 即arrayIndex = largerNumber % smallRange,这也就是哈希函数。它把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就是哈希表。
问题好像解决了, 然而,这样做产生了另一个问题,就是这些hashcode值对5000取余,必定会产生若干重复余数,这样会导致下标重复,这就是哈希冲突
2.解决哈希冲突的方法
解决哈希冲突的方法很多,包括: 开放地址法、链地址法、再哈希、建立公共溢出区等方法, 具体可以看这篇博客和这篇. 我们这里采用的是链地址法,也是java中hashMap解决hash冲突的方法
开放地址法解决hash冲突: 不能直接存放在由哈希函数所计算出来的数组下标时(存在重复下标时),通过探测方法找到空位置放入,这种方法要求初始的数组长度要大于实际存放数组长度足够多
链地址解决hash冲突 : 在哈希表每个单元中设置链表(即链地址法),某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。
三. 哈希表实现(链地址法解决hash冲突)
1.问题场景
有一个公司,当有新的员工来报道时,要求将该员工的信息加入 (性别,年龄,名字,住址..),当输入该员工的姓名时,要求查找到该员工的所有信息.
2.实现模型图
3.具体代码实现(数组+链表实现hash表)
(1) 员工类(这里只写了姓名方便操作)
//表示一个雇员
class Emp {
public int hash;
public String name;
public Emp next; //next 默认为 null
public Emp( String name) {
super();
this.name = name;
this.hash= name.hashCode();
}
}
(2)链表类(每个链表对应数组的一个下标)
//创建EmpLinkedList ,表示链表
class EmpLinkedList {
//头指针,执行第一个Emp,因此我们这个链表的head 是直接指向第一个Emp
private Emp head; //默认null
//添加雇员到链表
//说明
//1. 假定,当添加雇员时,id 是自增长,即id的分配总是从小到大
// 因此我们将该雇员直接加入到本链表的最后即可
public void add(Emp emp) {
//如果是添加第一个雇员
if(head == null) {
head = emp;
return;
}
//如果不是第一个雇员,则使用一个辅助的指针,帮助定位到最后
Emp curEmp = head;
while(true) {
if(curEmp.next == null) {//说明到链表最后
break;
}
curEmp = curEmp.next; //后移
}
//退出时直接将emp 加入链表
curEmp.next = emp;
}
//遍历链表的雇员信息
public void list(int no) {
if(head == null) { //说明链表为空
System.out.println("第 "+(no+1)+" 链表为空");
return;
}
System.out.print("第 "+(no+1)+" 链表的信息为");
Emp curEmp = head; //辅助指针
while(true) {
System.out.printf(" => name=%s\t", curEmp.name);
if(curEmp.next == null) {//说明curEmp已经是最后结点
break;
}
curEmp = curEmp.next; //后移,遍历
}
System.out.println();
}
public Emp findEmpByName(int hash) {
//判断链表是否为空
if(head == null) {
System.out.println("链表为空");
return null;
}
//辅助指针
Emp curEmp = head;
while(true) {
if(curEmp.hash == hash) {//找到
break;//这时curEmp就指向要查找的雇员
}
//退出
if(curEmp.next == null) {//说明遍历当前链表没有找到该雇员
curEmp = null;
break;
}
curEmp = curEmp.next;//以后
}
return curEmp;
}
(3) 哈希表(管理多个链表)
//创建HashTab 管理多条链表
class HashTab {
private EmpLinkedList[] empLinkedListArray;
private int size; //表示有多少条链表
//构造器
public HashTab(int size) {
this.size = size;
//初始化empLinkedListArray
empLinkedListArray = new EmpLinkedList[size];
//?留一个坑, 这时不要分别初始化每个链表
for(int i = 0; i < size; i++) {
empLinkedListArray[i] = new EmpLinkedList();
}
}
//添加雇员
public void add(Emp emp) {
//根据员工的name的hashcode ,得到该员工应当添加到哪条链表
int empLinkedListNO = hashFun(emp.name.hashCode());
//将emp 添加到对应的链表中
empLinkedListArray[empLinkedListNO].add(emp);
}
//遍历所有的链表,遍历hashtab
public void list() {
for(int i = 0; i < size; i++) {
empLinkedListArray[i].list(i);
}
}
//根据输入的name,查找雇员
public void findEmpByName(String name) {
//使用散列函数确定到哪条链表查找
int empLinkedListNO = hashFun(name.hashCode());
Emp emp = empLinkedListArray[empLinkedListNO].findEmpByName(name.hashCode());
if(emp != null) {//找到
System.out.printf("在第%d条链表中找到 雇员 name = %d\n", (empLinkedListNO + 1), name);
}else{
System.out.println("在哈希表中,没有找到该雇员~");
}
}
//编写散列函数, 使用一个简单取模法
public int hashFun(int hash) {
return hash % size;
}
}
(4) 测试类
public class EmpHashTab {
public static void main(String[] args) {
//创建哈希表
HashTab hashTab = new HashTab(7);
//写一个简单的菜单
String key = "";
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("add: 添加雇员");
System.out.println("list: 显示雇员");
System.out.println("find: 查找雇员");
System.out.println("exit: 退出系统");
key = scanner.next();
switch (key) {
case "add":
System.out.println("输入名字");
String name = scanner.next();
//创建 雇员
Emp emp = new Emp( name);
hashTab.add(emp);
break;
case "list":
hashTab.list();
break;
case "find":
System.out.println("请输入要查找的姓名");
name = scanner.next();
hashTab.findEmpByName(name);
break;
case "exit":
scanner.close();
System.exit(0);
default:
break;
}
}
}
}
(5)测试结果