1、散列表的基本概念
(1)散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为
H
a
s
h
(
k
e
y
)
=
A
d
d
r
Hash(key)=Addr
Hash(key)=Addr(这里的地址可以是数组下标、索引或内存地址等)。
散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。
(2)散列表:根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
理想情况下,对散列表进行查找的时间复杂度为
O
(
1
)
O(1)
O(1),即与表中元素的个数无关。
2、散列函数的构造方法
(1)构造散列函数的要求:
1、散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
2、散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。
3、散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址。
(2)常用的散列函数:
1、直接定址法
直接取关键字的某个线性函数值为散列地址,散列函数为
H ( k e y ) = k e y H(key) = key H(key)=key 或 H ( k e y ) = a ∗ k e y + b H(key)= a*key +b H(key)=a∗key+b
式中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
2、除留余数法
假定散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换成散列地址。散列函数为
H ( k e y ) = k e p % p H(key) = kep\%p H(key)=kep%p
3、数字分析法
设关键字是
r
r
r进制数(如十进制数),而
r
r
r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
4、平方取中法
取关键字的平方值的中间几位作为散列地址。具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
3、处理冲突的方法
(1)开放定址法
开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为
H = ( H ( k e y ) + d i ) % m H=(H(key)+d_{i})\%m H=(H(key)+di)%m
式中,
H
(
k
e
y
)
H(key)
H(key)为散列函数,
i
=
0
,
1
,
2
,
…
,
k
(
k
≤
m
−
1
)
i=0,1,2,…, k (k≤m- 1)
i=0,1,2,…,k(k≤m−1),
m
m
m表示散列表表长,
d
i
d_{i}
di为增量序列。
增量的取法如下:
1、线性探测法。当 d i = 0 , 1 , 2 , … , m − 1 d_{i}=0,1,2,…, m-1 di=0,1,2,…,m−1时,称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。线性探测法可能使第 i i i个散列地址的同义词存入第 i + 1 i+1 i+1个散列地址,这样本应存入第 i + 1 i+1 i+1个散列地址的元素就争夺第 i + 2 i+2 i+2个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。
2、平方探测法。当
d
i
=
0
2
,
1
2
,
−
1
2
,
2
2
,
−
2
2
,
.
.
.
,
k
2
,
−
k
2
d_{i} = 0^{2},1^{2},-1^{2},2^{2},-2^{2},...,k^{2},-k^{2}
di=02,12,−12,22,−22,...,k2,−k2时,称为平方探测法,其中
k
≤
m
/
2
k≤m/2
k≤m/2,散列表长度m必须是一个可以表示成
4
k
+
3
4k+3
4k+3的素数,又称二次探测法。
平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
3、再散列法。当 d i = H a s h 2 ( k e y ) d_{i}=Hash_{2}(key) di=Hash2(key)时,称为再散列法,又称双散列法。需要使用两个散列函数,当通过第一个散列函数 H ( k e y ) H(key) H(key)得到的地址发生冲突时,则利用第二个散列函数 H a s h 2 ( k e y ) Hash_{2}(key) Hash2(key)计算该关键字的地址增量。它的具体散列函数形式如下:
H = ( H ( k e y ) + i ∗ H a s h 2 ( k e y ) ) % m H=(H(key)+ i*Hash_{2}(key)) \% m H=(H(key)+i∗Hash2(key))%m
初始探测位置 H = H ( k e y ) % m H=H(key) \% m H=H(key)%m。 i i i是冲突的次数,初始为0。在再散列法中,最多经过m-1次探测就会遍历表中所有位置,回到 H H H位置。
4、伪随机序列法。当 d i = d_{i} = di= 伪随机数序列时,称为伪随机序列法。
(2)拉链法(链接法,chaining)
对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。假设散列地址为i的同义词链表的头指针存放在散列表的第
i
i
i个单元中,因而查找、插入和删除操作主要在同义词链中进行。拉链法适用于经常进行插入和删除的情况。
4、散列表代码实现(java):
拉链法实现散列表:
package com.haiyang.datastructure.hashtab;
import java.util.Scanner;
/**
* 以雇员为例测试散列表
* @author haiYang
* @create 2021-12-26 20:24
*/
public class HashTabDemo {
public static void main(String[] args) {
//创建哈希表
HashTab hashTab = new HashTab(7);
//通过简单菜单测试
String key = "";
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("=============================");
System.out.println("add:添加雇员");
System.out.println("list:显示雇员");
System.out.println("find:查找雇员");
System.out.println("delete:删除雇员");
System.out.println("exit:退出系统");
key = scanner.next();
switch (key) {
case "add":
System.out.println("请输入id:");
int id = scanner.nextInt();
System.out.println("请输入名字:");
String name = scanner.next();
Employee employee = new Employee(id, name);
hashTab.add(employee);
break;
case "list":
hashTab.list();
break;
case "find":
System.out.println("请输入要查找的id");
id = scanner.nextInt();
hashTab.findEmpById(id);
break;
case "delete":
System.out.println("请输入要删除的id");
id = scanner.nextInt();
hashTab.DeleteEmpById(id);
break;
case "exit":
scanner.close();
System.exit(0);
default:
break;
}
}
}
}
//管理多条EmpLikedList
class HashTab {
private EmpLikedList[] empLikedLists;
private int size;
//初始化为一个长度为size的哈希表
public HashTab(int size) {
this.size = size;
empLikedLists = new EmpLikedList[size];
for (int i = 0; i < size; i++) {
empLikedLists[i] = new EmpLikedList();
}
}
//添加雇员
public void add(Employee employee) {
//通过散列函数获取该条记录应该加入到第几个链表中
int empLinkedListNo = hashFun(employee.id);
empLikedLists[empLinkedListNo].add(employee);
}
//遍历hashTab中的所有链表
public void list() {
for (int i = 0; i < size; i++) {
empLikedLists[i].list(i);
}
}
//按id查找雇员
public void findEmpById(int id) {
int empLinkedListNO = hashFun(id);
Employee emp = empLikedLists[empLinkedListNO].findEmpById(id);
if (emp != null) {
System.out.println("在第" + empLinkedListNO + "链表中找到雇员" + id);
} else {
System.out.println("在哈希表中,没有找到该雇员");
}
}
//按id删除雇员
public void DeleteEmpById(int id) {
int empLinkedListNO = hashFun(id);
empLikedLists[empLinkedListNO].DeleteEmpById(id);
}
//散列函数
public int hashFun(int id) {
return id % size;
}
}
//EmpLikedList,表示链表
class EmpLikedList {
private Employee head;
//添加雇员信息
public void add(Employee employee) {
if (head == null) {//如果链表为空,将其作为头结点加入
head = employee;
return;
}
//遍历到链表尾,将其加入链表
Employee currentEmployee = head;
while (currentEmployee != null) {
currentEmployee = currentEmployee.next;
}
currentEmployee.next = employee;
}
//打印雇员信息
public void list(int no) {
if (head == null) {
System.out.println("第" + no + "条链表为空!");
return;
}
System.out.print("第" + no + "条链表信息为:");
Employee currentEmployee = head;
while (currentEmployee != null) {
System.out.print("id:" + currentEmployee.id + "用户名:" + currentEmployee.name);
currentEmployee = currentEmployee.next;
}
System.out.println();
}
//按id查找雇员
public Employee findEmpById(int id) {
if (head == null) {
return null;
}
Employee currentEmployee = head;
while (true) {
if (currentEmployee.id == id) {
break;
}
if (currentEmployee.next == null) {
currentEmployee = null;
break;
}
currentEmployee = currentEmployee.next;
}
return currentEmployee;
}
//按id删除雇员
public void DeleteEmpById(int id) {
Employee currentEmployee = head;
while (currentEmployee != null) {
if (currentEmployee.id == id && currentEmployee == head) {
head = currentEmployee.next;
System.out.println("id为" + id + "的雇员已删除");
return;
} else if (currentEmployee.next != null && currentEmployee.next.id == id) {
currentEmployee.next = currentEmployee.next.next;
System.out.println("id为" + id + "的雇员已删除");
return;
}
currentEmployee = currentEmployee.next;
}
System.out.println("雇员不存在");
}
}
//表示一个雇员
class Employee {
public int id;
public String name;
public Employee next;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
}