拉链法和线性探测法
散列函数
-
正整数
除留余数法,选择大小为素数M的数组,对于任意正整数k ,计算k除以M的余数。
如果M不是素数,我们可能无法利用键中包含的所有信息,这可能导致我们无法均匀地散列散列值 -
浮点数
第一,如果键是0-1的实数,我们可以将它乘 M 并四舍五入得到一个0~M-1 之间的索引,有缺陷,高位起的作用更大,最低位对散列值得结果没影响。
第二,将键表示为二进制,然后试用版除留余数法。 -
字符串
基本原理也是除留余数法,Horner方法
int hash = 0;
for(int i = 0 ; i<s.length(); i++){
hash = (R * hash +s.charAt(i)) % M;
}
//R代表进制 -
组合键
eg ,年月日
int hash = (((day * R +month) % M) * R +year) % M; -
Java约定
第一,所有数据类型都集成了一个能返回一个32bit整数的hashCode()方法。
第二,每一种数据类型的hashCode()都必须和equals()一致
第三,a.equals(b) 为true a.hashCode()= b.hashCode(); a,b的hashCode相同,并不一样是一个对象,需要用equal判断是否为同一对象。 -
hashCode()的返回值转化为索引值
hashCode()返回32位整数,而我们需要的是 0~M-1索引。转化方法如下
private int hash(Key x){
return (x.hashCode() & 0x7fffffff) % M;
}
//x.hashCode() & 0xfffffff将符号位屏蔽,然后除留余数法计算。
- hashCode()
public class Transaction{
private final String who;
private final Date when;
private final double amount;
public int hashCode(){
int hash = 17;
hash = 31 * hash + who.hashCode();
hash = 31 * hash + when.hashCode();
hash = 31 * hash + ammount.hashCode();
return hash;
}
}
- 软缓存
如果散列值得计算很耗时,那么我们或许可以将每个键的散列值保存起来,即在每个键中使用一个hash变量保存它的hashCode()返回值 - 优秀的散列方法的三个条件
第一,一致性 等价的键必然产生相等的散列值
第二,高效性,计算简便
第三,均匀地散列所有键
基于拉链法的散列表
定义:碰撞处理是指处理两个或多个键的散列值相同的情况。一种直接的办法是将大小为M的数组中的每个元素指向一条链表,链表中每个结点都存储了
散列值为该元素索引的键值对。这种方法称为拉链法。
让M足够大,这样每个链表的长度就越短。
查找分两步:根据散列值找到链表,然后沿着链表查找相应的键。
在一张含有M条链表 和N个键的散列表中,未命中查找和插入操作所需比较的次数为 ~N/M
基于线性探测法的散列表
另外一种,用大小M的数组保存N个键值对,其中 M > N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法统称为 开放地址散列表
开放地址散列表最简单的方法叫做 线性探测法
定义:当碰撞发生时,我们直接检查散列表中的下一个位置。这样的线性探测有三种结果
第一,命中,该位置的键和被查找的键相同
第二,未命中,键为空(该位置没有键)
第三,继续查找,该位置的键和被查找的键不同
基于线性探测法的散列表:
插入思路:
第一:计算key的散列表,找到相应的位置
第二:查看这个位置的值是否为要插入的key。不是的话比较下一个位置 ,直到当前位置的键值为空。
第三:如果在遇到空键值前遇到相等的键,则进行更新操作;如果直到遇到空值也没遇到相同的键,在空键值位置插入相应键值。
public class LinearProbingHashST<Key, Value> {
private static final int INIT_CAPACITY = 4;
private int n; // number of key-value pairs in the symbol table
private int m; // size of linear probing table
private Key[] keys; // the keys
private Value[] vals; // the values
public LinearProbingHashST() {
this(INIT_CAPACITY);
}
public LinearProbingHashST(int capacity) {
m = capacity;
n = 0;
keys = (Key[]) new Object[m];
vals = (Value[]) new Object[m];
}
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % m;
}
// resizes the hash table to the given capacity by re-hashing all of the keys
private void resize(int capacity) {
LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
for (int i = 0; i < m; i++) {
if (keys[i] != null) {
temp.put(keys[i], vals[i]);
}
}
keys = temp.keys;
vals = temp.vals;
m = temp.m;
}
public void put(Key key, Value val) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (val == null) {
delete(key);
return;
}
// double table size if 50% full
if (n >= m/2) resize(2*m);
int i;
for (i = hash(key); keys[i] != null; i = (i + 1) % m) {
if (keys[i].equals(key)) {
vals[i] = val;
return;
}
}
keys[i] = key;
vals[i] = val;
n++;
}
public Value get(Key key) {
if (key == null) throw new IllegalArgumentException("argument to get() is null");
for (int i = hash(key); keys[i] != null; i = (i + 1) % m)
if (keys[i].equals(key))
return vals[i];
return null;
}
}
删除操作
键簇: 元素在插入数组后集聚而成的一组连续的条目。
我们需要将簇中被删除键的右侧的所有键重新插入到散列表
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
if (!contains(key)) return;
// find position i of key
int i = hash(key);
while (!key.equals(keys[i])) {
i = (i + 1) % m;
}
// delete key and associated value
keys[i] = null;
vals[i] = null;
// rehash all keys in same cluster
i = (i + 1) % m;
while (keys[i] != null) {
// delete keys[i] an vals[i] and reinsert
Key keyToRehash = keys[i];
Value valToRehash = vals[i];
keys[i] = null;
vals[i] = null;
n--;
put(keyToRehash, valToRehash);
i = (i + 1) % m;
}
n--;
// halves size of array if it's 12.5% full or less
if (n > 0 && n <= m/8) resize(m/2);
assert check();
}
当 散列表快满的时候查找所需要的探测次数是巨大的,但当使用率 a小于 1/2 是探测的预计次数在1.5~2.5之间。
调整数组的大小
第一,基于线性探测法的数组大小调整
把原表中所有的键重新散列并插入到新表
private void resize(int capacity) {
LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
for (int i = 0; i < m; i++) {
if (keys[i] != null) {
temp.put(keys[i], vals[i]);
}
}
keys = temp.keys;
vals = temp.vals;
m = temp.m;
}
第二,拉链法的数组大小调整
内存使用
除了存储键和值所需要的空间外,我们实现拉链法 SeparateChainingHashST 保存了M个SeparateChainingHashST
对象和它们的引用。每个对象需要16字节,每个引用需要8字节。另外还有N个对象,每个对象需要24字节以及三个引用。
在使用率 1/81/2情况下,线性探测使用4N16N个引用。
方法 N个元素所需的内存
拉链法 48N + 32M
线性探测法 32N ~ 128N
二叉查找树 56N
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
· 用 C# 插值字符串处理器写一个 sscanf