各位读者,晚上好。
这里分享下基于拉链法、线性探测法实现的散列表。
本博客代码示例均来自:算法 Algorithmes Forth Edition
[美] Robert Sedgewick Kevin Wayne 著 谢路云译
一、拉链法
package com.cmh.algorithm;
import edu.princeton.cs.algs4.SequentialSearchST;
import java.util.Arrays;
/**
* 基于拉链法的散列表
*
* @author Author:起舞的日子
* Date: 2020/4/16 下午11:04
* <p>
*
*/
public class SeparateChainingHashST<Key, Value> {
/**
* 键值对总数
*/
private int N;
/**
* 散列表的大小
*/
private int M;
/**
* 存放链表对象的数组
*/
private SequentialSearchST<Key, Value>[] st;
public SeparateChainingHashST() {
this(997);
}
public SeparateChainingHashST(int M) {
/*
创建M条链表
*/
this.M = M;
st = new SequentialSearchST[M];
for (int i = 0; i < M; i++) {
st[i] = new SequentialSearchST<>();
}
}
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % M;
}
public Value get(Key key) {
return st[hash(key)].get(key);
}
public void put(Key key, Value val) {
st[hash(key)].put(key, val);
}
public Iterable<Key> keys() {
return (Iterable<Key>) Arrays.stream(st)
.map(e -> e.keys());
}
/**
* 总结:
* 1、散列表是算法在时间和空间上做出权衡的经典例子;散列函数能够将键转化为数组的一个索引;
* 2、散列查找需要处理碰撞冲突,这里介绍2种解决碰撞的办法:拉链法、线性探测法
* 3、可将各种数据类型散列:
* 整数-除留余数法
* 浮点数-把键表示为二进制再使用除留余数法
* 字符串-
* 组合键
* java.hashCode()
* 自定义hashCode()方法
* 4、软缓存:每次计算散列值很耗时,就把计算的值缓存起来,下次直接用;
* 5、为避免链表过长,动态调整链表数组大小,无论符号表中有多少键值对都能保证链表较短:
* 二项分布
* 泊松分布
*
*/
}
二、线性探测法
package com.cmh.algorithm;
/**
* 基于线性探测法的散列表
* <p>
* Author:起舞的日子
* Date: 2020/4/16 下午11:29
*/
public class LinearProbingHashST<Key, Value> {
/**
* 符号表中的键值对总数
*/
private int N;
/**
* 线性探测表的大小
*/
private int M = 16;
/**
* 并行数组
*/
private Key[] keys;
private Value[] vals;
public LinearProbingHashST() {
keys = (Key[]) new Object[M];
vals = (Value[]) new Object[M];
}
public LinearProbingHashST(int cap) {
M = cap;
}
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % M;
}
private void resize() {
}
private void resize(int cap) {
LinearProbingHashST<Key, Value> t;
t = new LinearProbingHashST<>(cap);
for (int i = 0; i < M; i++) {
if (keys[i] != null) {
t.put(keys[i], vals[i]);
}
keys = t.keys;
vals = t.vals;
M = t.M;
}
}
public void put(Key key, Value val) {
/**
* 扩容,为什么扩2倍?
* 因为这样可以避免键簇形成
*/
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) {
for (int i = hash(key); keys[i] != null; i = (i + 1) % M) {
if (keys[i].equals(key)) {
return vals[i];
}
}
return null;
}
public boolean contains(Key key) {
for (int i = hash(key); keys[i] != null; i = (i + 1) % M) {
if (keys[i].equals(key)) {
return true;
}
}
return false;
}
public void delete(Key key) {
if (!contains(key)) {
return;
}
int i = hash(key);
while (!key.equals(keys[i])) {
i = (i + 1) % M;
}
keys[i] = null;
vals[i] = null;
i = (i + 1) % M;
while (keys[i] != null) {
Key keyToRedo = keys[i];
Value valToRedo = vals[i];
keys[i] = null;
vals[i] = null;
N--;
put(keyToRedo, valToRedo);
i = (i + 1) % M;
}
N--;
if (N > 0 && N == M / 8) {
resize(M / 2);
}
}
/**
* 总结:
* 1、 用大小为M的数组保存N个键值对,其中M>N.依靠数组中空位解决碰撞冲突。——开放地址散列表
* 2、开放地址散列表中最简单的方法是:线性探测法
* 3、用散列函数找到键所在的索引,检查其中的键和被查找的键是否相同,如果不同继续查找;
* 4、探测:检查一个数组位置是否含有被查找的键的操作
* 5、键簇-元素插入数组后形成一组连续的条目,如何避免?调整数组大小
* 6、均摊分析 动态调整数组大小使散列表长度加倍的插入操作需要大量探测
*
*/
}
三、源码地址
https://github.com/cmhhcm/my2020.git