哈希表
介绍
1.哈希表,又称散列表,是根据关键码值而直接进行访问的数据结构
2.它通过把关键码值映射到表中一个位置来访问记录, 以加快查找的速度
3.这个映射函数叫做散列函数, 存放记录的数组叫做散列表
两种实现方式
1.数组+链表
2.数组+红黑树
代码实现(数组+单向链表)
public class HashTable {
private SingleLinkedList[] hashTable;//LinkedList类型的数组管理链表
private int size;//表示有多少条链表
public HashTable(int size) {
hashTable = new SingleLinkedList[size];//这里是初始化数组
for (int i = 0; i < size; i++) {//分别初始化每个链表
hashTable[i] = new SingleLinkedList();
}
this.size = size;
}
//添加节点
public void add(Node node) {
int no = hash(node.id);//根据节点的id,得到该员工应当添加到哪条链条
hashTable[no].add(node);//将node添加到对应的链表中
}
//根据id,删除节点
public void delete(int id) {
int no = hash(id);//根据节点的id,得到该员工应当添加到哪条链条
hashTable[no].delete(id);
}
//更新节点
public void update(Node node) {
int no = hash(node.id);//根据节点的id,得到该员工应当更新哪条链条
hashTable[no].update(node);
}
// 遍历所有的链表
public void list() {
for (int i = 0; i < size; i++) {
hashTable[i].list(i);
}
}
//根据id查找
public void findById(int id) {
int no = hash(id);//先判断在哪条链表
Node emp = hashTable[no].findById(id);
if (emp != null) {
System.out.println("第" + (no + 1) + "链表中找到id为" + id + "节点");
} else {
System.out.println("哈希表中没有找到");
}
}
// 编写散列函数,使用取模法
public int hash(int id) {
return id % size;
}
}
//表示节点,存储数据
class Node {
public int id;
public String name;//视作data
public Node next;//下一个节点
public Node(int id, String name) {
this.id = id;
this.name = name;
}
}
//表示单向链表,管理节点
class SingleLinkedList {
private Node head;// 头指针,指向第一个节点
//尾插法,添加节点到链表
public void add(Node node) {
//如果添加第一个节点
if (head == null) {
head = node;
return;
}
Node cur = head;
while (true) {
if (cur.next == null) { //说明链表到最后
break;
}
cur = cur.next;
}
cur.next = node;
}
//根据id,删除节点
public void delete(int id) {
if (head == null) {
return;//链表为空
}
Node cur = head;
if (id == head.id) {//删除头节点
head = cur.next;
return;
}
while (true) {
if (cur.next == null) {//该编号节点不存在
return;
}
if (cur.next.id == id) {
cur.next = cur.next.next;//被跳过的节点没有引用,被回收
return;
}
cur = cur.next;
}
}
//更新节点
public void update(Node node) {
if (head == null) {
return;//链表为空
}
Node cur = head;
if (node.id == head.id) {//更新头节点
node.next = cur.next;
head = node;
return;
}
while (true) {
if (cur.next == null) {//该编号节点不存在
return;
} else if (cur.next.id == node.id) {//找到该编号节点
node.next = cur.next.next;
cur.next = node;
return;
}
cur = cur.next;
}
}
//遍历链表的节点data
public void list(int no) {
if (head == null) {
System.out.println("第" + (no + 1) + "条链表为空");
return;
}
System.out.print("第" + (no + 1) + "条链表的信息为");
Node cur = head;
while (true) {
System.out.printf("=> id=%d name=%s\t", cur.id, cur.name);
if (cur.next == null) { //说明链表到最后
break;
}
cur = cur.next;
}
System.out.println();
}
//根据id查找节点
public Node findById(int id) {
if (head == null) {
System.out.println("链表为空");
return null;
}
Node cur = head;
while (true) {
if (cur.id == id) {
break;//这时cur就指向要查找的节点
}
if (cur.next == null) {//说明遍历当前链表没有找到该节点
cur = null;
break;
}
cur = cur.next;//以后
}
return cur;
}
}
多种数据类型的散列函数
1、假设
(1)有一个能够保存 M 个键值对的数组,那么就需要一个能够将任意键转化为该数组范围内的索引([0, M-1] 范围内的整数)的散列函数
(2)散列函数应该易于计算并且能够均匀分布所有的键,即对于任意键,0 到 M-1 之间的每个整数都有相等的可能性与之对应(与键无关)
2、正整数
(1)将整数散列最常用方法是除留余数法
(2)选择大小为素数 M 的数组,对于任意正整数 k,计算 k 除以 M 的余数
(3)这个函数的计算非常容易(在 Java 中为 k % M)并能够有效地 将键散布在 0 到 M-1 的范围内
(4)如果 M 不是素数,则可能无法利用键中包含的所有信息,这可能导致无法均匀地散列散列值
3、浮点数
(1)如果键是 0 到 1 之间的实数,可以将它乘以 M 并四舍五入得到一个 0 至 M-1 之间的索引值
(2)尽管这个方法很容易理解,但它是有缺陷的,因为这种情况下键的高位起的作用更大,最低位对散列的结果没有影响
(3)修正这个问题的办法是将键表示为二进制数然后再使用除留余数法(Java 就是这么做的)
4、字符串
(1)除留余数法也可以处理较长的键,例如字符串,只需将它们当作大整数即可
(2)例如
int hash = 0;
for (int i = 0; i < s.length(); i++)
hash = (R * hash + s.charAt(i)) % M;
(3)Java 的 charAt() 函数能够返回一个 char 值,即一个非负 16 位整数。如果 R 比任何字符的值都大,这种计算相当于将字符串当作一个 N 位的 R 进制值,将它除以 M 并取余
(4)一种叫 Horner 方法的经典算法用 N 次乘法、加法和取余来计算一个字符串的散列值。只要 R 足够小,不造成溢出,那么结果就能够落在 0 至 M-1 之内,使用一个较小的素数,例如 31,可以保证字符串中的所有字符都能发挥作用
(5)Java 的 String 的默认实现使用了一个类似(4)方法
5、组合键
(1)如果键的类型含有多个整型变量,可以和 String 类型一样将它们混合起来
(2)例如:Date,其中含有几个整型的域:day(2 个数字表示的日),month(2 个数字表示的月)、 year(4 个数字表示的年)
int M = 31;
int hash = (((day * R + month) % M ) * R + year) % M;
基于拉链法的散列表:数组 + 链表
1、假设:使用的散列函数能够均匀并独立地将所有的键散布于 0 到 M - 1 之间
2、在一张含有 M 条链表和 N 个键的的散列表中,在假设成立的前提下,任意一条链表中的键的数量均在 N / M 的常数因子范围内的概率无限趋向于 1
基于线性探测法的散列表
1、开放地址散列表:用大小为 M 的数组保存 N 个键值对,其中 M>N,需要依靠数组中的空位解决碰撞冲突
2、开放地址散列表中最简单的方法叫做线性探测法:当碰撞发生时(当一个键的散列值已经被另一个不同的键占用),直接检查散列表中的下一个位置(将索引值加 1)。这样的线性探测可能会产生三种结果
(1)命中,该位置的键和被查找的键相同
(2)未命中,键为空(该位置没有键)
(3)继续查找,该位置的键和被查找的键不同
3、用散列函数找到键在数组中的索引,检查其中的键和被查找的键是否相同。如果不同则继续查找(将索引增大,到达数组结尾时折回数组的开头),直到找到该键或者遇到一个空元素
4、探测:检查一个数组位置是否含有被查找的键的操作,等价于一直使用的比较,不过有些探测实际上是在测试键是否为空
5、开放地址类的散列表的核心思想是与其将内存用作链表,不如将它们作为在散列表的空元素,这些空元素可以作为查找结束的标志
渐进性能的总结
算法(数据结构) | 最坏情况下的运行时间的增长数量级(N 次插入之后) | 平均情况下的运行时间的增长数量级(N 次插入之后) | 内存使用(字节) | ||
查找 | 插入 | 查找 | 插入 | ||
顺序查询(无序链表) | N | N | N / 2 | N | 48 * N |
二分查找(有序数组) | lgN | N | lgN | N / 2 | 16 * N |
二叉树查找(BST) | N | N | 1.39 * lgN | 1.39 * lgN | 64 * N |
2-3 树查找(红黑树) | 2 * lgN | 2 * lgN | 1.00 * lgN | 1.00 * lgN | 64 * N |
拉链法*(链表数组) | < lgN | < lgN | N / (2 * M) | N / M | 48 * N + 32 * M |
线性探测法*(并行数组) | c * lgN | c * lgN | < 1.5 | < 2.5 | 32 * N ~ 128 * N |
1、M:数组长度
2、*:需要均匀并独立的散列函数
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战