散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。(哈希表本质上就是个数组,哈希表的底层是数组)
实现哈希表的两种方法:
1、数组+链表
2、数组+红黑二叉树
推荐对话简图理解概念一文彻底搞定哈希表!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
哈希函数:建立起数据元素的存放位置与数据元素的关键字之间的对应关系的函数。即使用哈希函数可将被查找的键转换为数组的索引。理想情况下它应该运算简单并且保证任何两个不同的关键字映射到不同的单元(索引值)。但是,这是不可能的,很多时候我们都需要处理多个键被哈希到同一个索引值的情况,即哈希碰撞冲突
构造哈希函数的目标:使得到的哈希地址尽可能均匀地分布在m个连续内存单元地址上,同时使计算过程尽可能简单以达到尽可能高的时间效率。
根据关键字的结构和分布的不同,有多种构造哈希函数的方法。
取关键字或关键字的某个线性函数值为哈希地址。即H(key)=key 或 H(key)=a*key+b (a,b为常数)。
举例1:统计1-100岁的人口,其中年龄作为关键字,哈希函数取关键字自身。查找年龄25岁的人口有多少,则直接查表中第25项。
举例2:统计解放以后出生人口,其中年份作为关键字,哈希函数取关键字自身加一个常数H(key)=key+(-1948).查找1970年出生的人数,则直接查(1970-1948)=22项即可
若关键字是以r为基的数(如:以10为基的十进制数),并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
举例:有80个记录,其关键字为8位十进制数,假设哈希表长1000,则可取两位十进制数组成哈希地址,为了尽量避免冲突,可先分析关键字。
经分析,发现第一位、第二位都是8,1,第三位只可能取3或4,第八位只可能取2,5或7,所以这四位不可取,那么对于第四、五、六、七位可看成是随机的,因此,可取其中任意两位,或取其中两位与另外两位的叠加求和舍去进位作为哈希地址。
取关键字平方后的中间几位为哈希地址。(较常用的一种)
举例:为BASIC源程序中的标识符键一个哈希表(假设BASIC语言允许的标识符为一个字母或者一个字母和一个数字两种情况,在计算机内可用两位八进制数表示字母和数字),假设表长为512=2^{9},则可取关键字平方后的中间9位二进制数为哈希地址。(每3个二进制位可表示1位八进制位,即3个八进制位为9个二进制位)
A :01 (A的ASCII码值为65,65的八进制为101,取后两位表示关键字)
B:02 (B的ASCII码值为66,66的八进制为102,取后两位表示关键字)
…
Z:32(Z的ASCII码值为90,90的八进制为132,取后两位表示关键字)
…
0:60(0的ASCII码值为48,48的八进制为60,取后两位表示关键字)
…
9:71(9的ASCII码值为57,57的八进制为71,取后两位表示关键字)
将关键字分割成位数相同的几部分(最后一部分的位数可不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。适用于关键字位数比较多,且关键字中每一位上数字分布大致均匀时。
举例:根据国际标准图书编号(ISBN)建立一个哈希表。如一个国际标准图书编号 0-442-20586-4的哈希地址为:
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址(p为素数)
H(key)=key MOD p,p<=m (最简单,最常用)p的选取很重要
一般情况,p可以选取为质数或者不包含小于20的质因数的合数(合数指自然数中除了能被1和本身整除外,还能被其他数(0除外)整除的数)。
选择一个随机函数,取关键字的随机函数值为它的哈希地址。即H(key)=random(key),其中random为随机函数。适用于关键字长度不等时。
总结:实际工作中根据情况不同选用的哈希函数不同,通常,考虑因素如下:
(1)计算哈希函数所需时间(包括硬件指令的因素)
(2)关键字的长度
(3)哈希表的大小
(4)关键字的分布情况
(5)记录的查找频率
在哈希表中,虽然冲突很难避免,但发生冲突的可能性却有大有小。这主要与三个因素有关:
- 与装填因子有关。所谓装填因子α是指哈希表中已存入的元素数n与哈希地址空间大小m的比值,即α=n/m。α越小,冲突的可能性就越小;但α越小,存储空间的利用率就越低。
- 与所采用的哈希函数有关。
- 与解决冲突的哈希冲突函数有关。
开放地址法:通过系统的方法找到系统的空位(三种:线性探测、二次探测、再哈希法),并将待插入的元素填入,而不再使用用hash函数得到数字作为数组的下标。
即简单理解:发生冲突时查找周围一个空位置存放记录。设置一个查找周围一个空位置的函数。
平方探测法可以避免出现堆积问题。
缺点是不能探测到哈希表上的所有单元,但至少能探测到一半单元。
哈希表的扩容:
关于开放寻址,如果一直找不到空的位置怎么办?
这种情况不存在,为啥嘞?你这样想,是因为你考虑了一个前提,那就是位置已经被占光了,没有空位置了,但是实际情况是位置不会被占光的,因为有一定量的位置被占了的时候就会发生扩容。当哈希表被占的位置比较多的时候,出现哈希冲突的概率也就变高了,所以很有必要进行扩容。
那么这个扩容是怎么扩的呢?
这里一般会有一个增长因子的概念,也叫作负载因子,简单点说就是已经被占的位置与总位置的一个百分比,比如一共十个位置,现在已经占了七个位置,就触发了扩容机制,因为它的增长因子是0.7,也就是达到了总位置的百分之七十就需要扩容。拿HashMap来说,当它当前的容量占总容量的百分之七十五的时候就需要扩容了。而且这个扩容也不是简单的把数组扩大,而是新创建一个数组是原来的2倍,然后把原数组的所有元素都重新Hash一遍放到新的数组。
链地址法 :创建一个存放单词链表的数组,数组内不直接存放元素,而是存储元素的链表。发生冲突的时候,数据项直接接到这个数组下标所指的链表中即可。
优势:填入过程允许重复,所有关键值相同的项放在同一链表中,找到所有项就需要查找整个是链表,稍微有点影响性能。删除只需要找到正确的链表,从链表中删除对应的数据即可。表容量是质数的要求不像在二次探测和再hash法中那么重要,由于没有探测的操作,所以无需担心容量被步长整除,从而陷入无限循环中。
如果冲突的很多,那这个增加的链表岂不是很长?
如果冲突过多的话,这个key对应的链表会变得比较长,怎么处理呢?这里举个例子吧,拿java集合类中的HashMap来说吧,如果这里的链表长度大于等于8的话,链表就会转换成红黑树结构,当然如果长度小于等于6的话,就会还原链表。以此来解决链表过长导致的性能问题。这样设计是因为中间有个7作为一个差值,来避免频繁的进行树和链表的转换,因为转换频繁也是影响性能的啊。
题目:有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id、姓名,性别,电话),当输入该员工的id时,要求查找到该员工的所有信息.
要求:
思路分析:
添加雇员信息
1,创建一个节点类存储雇员的信息(id,name,sex,phone)
2,创建一个定长数组为哈希表,哈希表的每个数组元素储存一条链表头节点
3,根据散列函数将要添加的雇员id进行散列(比如:散列函数构造采用简单的4,取模法:H(k)=id % size 假如id=1001 数组长度为7则取余后的key为0 对应的数组下标为0)
5,根据id散列后的key值将对应id的雇员节点链接到对应的数组下标下的链表后面
查找对应id的雇员信息:
1,根据id散列得到key值
2,到key值对应的数组下标的链表中进行查询
import java.util.Scanner;
public class HashTabDemo {
public static void main(String[] args) {
//创建哈希表
HashTab hashTab = new HashTab(7);
//写一个简单的菜单
int n;
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("1: 添加雇员");
System.out.println("2: 显示雇员");
System.out.println("3: 查找雇员");
System.out.println("4: 退出系统");
n = scanner.nextInt();
switch (n) {
case 1:
System.out.println("输入id");
int id = scanner.nextInt();
System.out.println("输入名字");
String name = scanner.next();
System.out.println("输入性别");
String sex = scanner.next();
System.out.println("输入电话");
String phone = scanner.next();
//创建 雇员
Emp emp = new Emp(id, name,sex,phone);
hashTab.add(emp);
break;
case 2:
hashTab.list();
break;
case 3:
System.out.println("请输入要查找的id");
id = scanner.nextInt();
hashTab.findEmpById(id);
break;
case 4:
scanner.close();
System.exit(0);
default:
break;
}
}
}
}
//创建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) {
//根据员工的id ,得到该员工应当添加到哪条链表
int empLinkedListNO = hashFun(emp.id);
//将emp 添加到对应的链表中
empLinkedListArray[empLinkedListNO].add(emp);
}
//遍历所有的链表,遍历hashtab
public void list() {
for(int i = 0; i < size; i++) {
empLinkedListArray[i].list(i);
}
}
//根据输入的id,查找雇员
public void findEmpById(int id) {
//使用散列函数确定到哪条链表查找
int empLinkedListNO = hashFun(id);
Emp emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
if(emp != null) {//找到
System.out.printf("在第%d条链表中找到 雇员 id = %d name = %s sex = %s phone = %s", (empLinkedListNO + 1), id,emp.name,emp.sex,emp.phone);
}else{
System.out.println("在哈希表中,没有找到该雇员~");
}
}
//编写散列函数, 使用一个简单取模法
public int hashFun(int id) {
return id % size;
}
}
//表示一个雇员
class Emp {
public int id;
public String name;
public String sex;
public String phone;
public Emp next; //next 默认为 null
public Emp(int id, String name,String sex,String phone) {
super();
this.id = id;
this.name = name;
this.sex = sex;
this.phone = phone;
}
}
//创建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(" => id=%d name=%s sex=%s phone=%s\t", curEmp.id, curEmp.name,curEmp.sex,curEmp.phone);
if(curEmp.next == null) {//说明curEmp已经是最后结点
break;
}
curEmp = curEmp.next; //后移,遍历
}
System.out.println();
}
//根据id查找雇员
//如果查找到,就返回Emp, 如果没有找到,就返回null
public Emp findEmpById(int id) {
//判断链表是否为空
if(head == null) {
System.out.println("链表为空");
return null;
}
//辅助指针
Emp curEmp = head;
while(true) {
if(curEmp.id == id) {//找到
break;//这时curEmp就指向要查找的雇员
}
//退出
if(curEmp.next == null) {//说明遍历当前链表没有找到该雇员
curEmp = null;
break;
}
curEmp = curEmp.next;//以后
}
return curEmp;
}
}
测试输出
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1001
输入名字
张三
输入性别
男
输入电话
12345674512
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1002
输入名字
李强
输入性别
男
输入电话
14578214573
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
1
输入id
1009
输入名字
王文
输入性别
女
输入电话
14789654233
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
2
第 1 链表的信息为 => id=1001 name=张三 sex=男 phone=12345674512
第 2 链表的信息为 => id=1002 name=李强 sex=男 phone=14578214573 => id=1009 name=王文 sex=女 phone=14789654233
第 3 链表为空
第 4 链表为空
第 5 链表为空
第 6 链表为空
第 7 链表为空
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
3
请输入要查找的id
1009
在第2条链表中找到 雇员 id = 1009 name = 王文 sex = 女 phone = 14789654233
1: 添加雇员
2: 显示雇员
3: 查找雇员
4: 退出系统
本文转自数据结构(Java实现)-详谈哈希表(Hash Table)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。