基本介绍
哈希表([Hash table,又称散列表),是根据关键码值(Key Value)而直接进行访问的数据结构。其通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
实现哈希表的两种方法:
数组+链表
数组+红黑二叉树
哈希表能解决什么问题?一般哈希表都是用来快速判断一个元素是否出现集合里。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
散列函数(hash function)
哈希函数:建立起数据元素的存放位置与数据元素的关键字之间的对应关系的函数。即使用哈希函数可将被查找的键转换为数组的索引。理想情况下它应该运算简单并且保证任何两个不同的关键字映射到不同的单元(索引值)。但是很多时候我们都需要处理多个键被哈希到同一个索引值的情况,即哈希冲突。
以下是哈希函数的构造方法:
直接定址法取关键字或关键字的某个线性函数值为哈希地址。即H(key)=key 或 H(key)=a*key+b (a,b为常数)。
数字分析法若关键字是以r为基的数(如:以10为基的十进制数),并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
平方取中法取关键字平方后的中间几位为哈希地址。
折叠法将关键字分割成位数相同的几部分(最后一部分的位数可不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。适用于关键字位数比较多,且关键字中每一位上数字分布大致均匀时。
除留余数法取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址(p为素数),即H(key)=key MOD p,p<=m (最简单,最常用)p的选取很重要。一般情况,p可以选取为质数或者不包含小于20的质因数的合数(合数指自然数中除了能被1和本身整除外,还能被其他数(0除外)整除的数)。
随机数法选择一个随机函数,取关键字的随机函数值为它的哈希地址。即H(key)=random(key),其中random为随机函数。适用于关键字长度不等时。
哈希冲突解决
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞/哈希冲突。
开放地址法开放地址法:通过系统的方法找到系统的空位(三种:线性探测、二次探测、再哈希法),并将待插入的元素填入,而不再使用用hash函数得到数字作为数组的下标。
线性探测:假若当前要插入的位置已经被占用了之后,沿数组下标递增方向查找,直到找到空位为止。
二次探测:二次探测和线性探测的区别在于二次探测的步长是,若计算的原始下标是x则二次探测的过程是x+12,x+22,x+32,x+42,x+52随着探测次数的增加,探测的步长是探测次数的二次方(因此名为二次探测)。二次探测会产生二次聚集:即当插入的几个数经过hash后的下标相同的话,那么这一串数字插入的探测步长会增加很快。
再hash法:为了消除原始聚集和二次聚集,把关键字用不同的hash函数再做一遍hash化,用过这个结果作为探测的步长,这样对于特定的关键字在整个探测中步长不变,但是不同的关键字会使用不同的步长。stepSize = constant - (key % constant) 这个hash函数求步长比较实用,constant是小于数组容量的质数。(注意:第二个hash函数必须和第一个hash函数不同,步长hash函数输出的结果值不能为0)。
链地址法链地址法 :创建一个存放单词链表的数组,数组内不直接存放元素,而是存储元素的链表。发生冲突的时候,数据项直接接到这个数组下标所指的链表中即可。优势:填入过程允许重复,所有关键值相同的项放在同一链表中,找到所有项就需要查找整个是链表,稍微有点影响性能。删除只需要找到正确的链表,从链表中删除对应的数据即可。表容量是质数的要求不像在二次探测和再hash法中那么重要,由于没有探测的操作,所以无需担心容量被步长整除,从而陷入无限循环中。
代码实现
题目:有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id, 性别, 年龄, 电话),当输入该员工的 id 时,要求查找到该员工的所有信息。
要求:(1)不使用数据库,速度越快越好 ==> 哈希表(2)添加时,保证按照id从低到高插入(3)使用链表来实现哈希表, 且链表不带表头
public class HashTableDemo {
public static void main(String[] args) {
//创建 哈希表
HashTab hashTab = new HashTab(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);//创建员工
hashTab.add(emp);
break;
case 2:
hashTab.show();
break;
case 3:
System.out.println("请输入要查找的员工id");
id = sc.nextInt();
hashTab.find(id);
break;
case 4:
sc.close();//关闭输入
System.exit(0);//最好写上
default:
break;
}
}
}
}
/**
* Employee 表示一个雇员
*/
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) {
this.id = id;
this.name = name;
this.sex = sex;
this.phone = phone;
}
}
/**
* 创建 EmpLinkedList,表示链表
*/
class EmpLinkedList {
//头指针,指向第一个Employee,因此这个链表的 head 指向第一个 Employee
private Employee head;
//添加员工
//说明:
//1. 假定当添加雇员时,id 是自增长,即 id的分配总是从小到大,因此添加至本链表的最后即可
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查找员工
//如果找到,就返回 Employee,如果没找到,就返回 null
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;
}
}
/**
* 创建 HashTab 管理多条链表
*/
class HashTab {
private EmpLinkedList[] empLinkedListsArray;
private int size;// size:表示数组的大小,即链表的总数
//构造器
public HashTab(int size) {
this.size = size;
empLinkedListsArray = new EmpLinkedList[size];
//必须进行数组初始化,如果不初始化,则添加的时候找不到 head指针,抛出 空指针异常
for (int i = 0; i < size; i++) {
empLinkedListsArray[i] = new EmpLinkedList();
}
}
//添加 雇员
public void add(Employee emp) {
//根据员工的id,得到该员工应该添加到 哪一条链表
int empLinkedListNO = hashFun(emp.id);
//将 emp 添加到 对应的链表中
empLinkedListsArray[empLinkedListNO].add(emp);
}
//遍历所有的链表,即遍历 哈希表
public void show() {
for (int i = 0; i < size; i++) {
empLinkedListsArray[i].show(i);
}
}
//根据id查找员工
public void find(int id) {
//使用散列函数确定到哪条链表查找
int employeeNo = hashFun(id);
Employee emp = empLinkedListsArray[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;
}
}