まず、ハッシュ関数
ハッシュ関数は、キーを配列のインデックスに変換します。ハッシュ関数は高速で、すべてのキーを均等に分散する必要があります。たとえば、サイズMのハッシュテーブルの場合、ハッシュ関数は任意のキーを0からM-1の整数に変換できる必要があります。キーごとに異なるハッシュ関数が必要です。Javaでよく使用されるクラスの多くは、さまざまなデータ型に対してさまざまなハッシュ関数を使用するようにhashCodeメソッドを書き直しています。
第二に、ジッパー方式に基づくハッシュテーブル
ハッシュアルゴリズムの理想的な状態は、異なるキーを異なるインデックス値に変換することですが、これは明らかに不可能であり、競合が発生するため、競合に対処する必要があります。
直接的な方法は、サイズMの配列の各インデックスをリンクリストにポイントすることです。リンクリストの各ノードは、ハッシュ値がリンクリストのインデックス値であるキー値のペアを格納します。このメソッドはジッパーメソッドです。 。
以下は、基本的なデータ構造です。
public class SeparateChainingHashST<Key,Value> {
/**
* 键值对总数
*/
private int N;
/**
* 散列表大小
*/
private int M;
private SequentialSearchST<Key,Value>[] st;
public SeparateChainingHashST() {
this(997);
}
public SeparateChainingHashST(int M) {
this.M=M;
st=new SequentialSearchST[M];
for (int i = 0; i < M; i++) {
//数组中每个索引值都初始化一个链表
st[i]=new SequentialSearchST<>();
}
}
}
SequentialSearchST
これは、前の順次検索で実装された順序付けされていないリンクリストです。
ublic class SequentialSearchST<Key, Value> {
/**
* 首节点
*/
private Node first;
private int size;
private class Node {
private Key key;
private Value value;
private Node next;
public Node(Key key, Value value, Node next) {
this.key = key;
this.value = value;
this.next = next;
}
}
/**
* 根据key查询对应的值,一个个往下遍历直到找到相等的key,返回对应的值,否则返回null
* @param key
* @return
*/
public Value get(Key key) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
return x.value;
}
}
return null;
}
/**
* 加入一个元素
* @param key
* @param value
*/
public void put(Key key, Value value) {
for (Node x = first; x != null; x = x.next) {
//key已存在,更新对应的值
if (key.equals(x.key)) {
x.value = value;
return;
}
}
//key不存在,新添加一个节点
first = new Node(key, value, first);
size++;
}
public boolean isEmpty() {
return size == 0;
}
private int size() {
return size;
}
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("argument to contains() is null");
return get(key) != null;
}
/**
* 删除key对应的节点
* @param key
*/
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
first = delete(first, key);
}
/**
* 递归查找,直到找到相等的key,正常删除链表节点
* @param x
* @param key
* @return
*/
private Node delete(Node x, Key key) {
if (x == null) return null;
if (key.equals(x.key)) {
size--;
return x.next;
}
x.next = delete(x.next, key);
return x;
}
public Iterable<Key> keys() {
Queue<Key> queue=new Queue<>();
while (first!=null){
queue.enqueue(first.key);
first=first.next;
}
return queue;
}
}
ハッシュ計算:
前述のように、Javaのすべてのデータ型に対してhashCode()メソッドが書き直されました。このメソッドは32ビットの整数を返しますが、必要なのは配列のインデックスなので、デフォルトのhashCodeメソッドと残りのメソッドを組み合わせて0からM-1の整数を生成します。hashCodeによって返される値は符号付きであるため、計算結果が負の場合でも0x7fffffff
、31ビットの非負の整数に渡す必要があります、それから余りの除算法を使ってそれをさせる%M
、Mはより大きな素数である。
private int hash(Key key){
return (key.hashCode() & 0x7fffffff) % M;
}
このようにして、データを挿入、削除、および取得できます。
挿入の実装:
次のように、最初にキーのハッシュ値を計算して配列インデックスを取得し、次にキーと値のペアをインデックスに対応するリンクリストに挿入します。
public void put(Key key,Value value){
if (key==null){
throw new NoSuchElementException("key为空");
}
if (value==null){
delete(key);
}
//保证链表的长度在2到8之间
if (N>=8*M){
resize(M*2);
}
st[hash(key)].put(key,value);
}
削除の実装:
最初にキーのハッシュ値を計算し、それが配置されているリンクリストを見つけ、リンクリストにキーが存在する場合はそれを削除します。
/**
* 删除指定键值对
* @param key
*/
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
int i = hash(key);
if (st[i].contains(key)){
N--;
}
st[i].delete(key);
//保证链表平均长度在2到8间 此为下界2
if (N>0 && N<=M*2){
resize(M/2);
}
}
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("key为空");
return get(key) != null;
}
値を取得:
キーのハッシュ値を計算し、対応するリンクリストの値を返します
public Value get(Key key){
if (key == null) return null;
return st[hash(key)].get(key);
}
M個のリンクリストを使用してN個のキーを格納します。キーがテーブルでどのように分散されていても、平均の長さはでなければなりませんN/M
。
ジッパー方式を使用する利点の1つは、予想よりも多くのキーがある場合、検索時間はより大きな配列を選択するよりも長くなることです。それが予想よりも低い場合、スペースが少し無駄になるが、検索は高速です。
したがって、メモリが十分な場合は、検索の使用を一定にするのに十分な大きさのMを選択できます。メモリが少ない場合は、最大のMを選択しても、パフォーマンスがM倍向上します。
配列を動的に調整し
ます。削除後、平均の長さN / Mが2未満の場合、配列は2倍になります:M / 2;
データを追加した後、N / Mが8より大きい場合、配列は2倍になります。 M * 2
private void resize(int capacity){
SeparateChainingHashST<Key,Value>hashST=new SeparateChainingHashST<>(capacity);
for (int i = 0; i < M; i++) {
for (Key key:st[i].keys()){
if (key!=null){
hashST.put(key,st[i].get(key));
}
}
}
this.M=hashST.M;
this.N=hashST.N;
this.st=hashST.st;
}
3、線形検出法に基づくハッシュテーブル
ハッシュテーブルを実装するもう1つの方法は、サイズMの配列を使用してN個のキーと値のペア(M> N)を格納し、衝突を使用して衝突の衝突を解決することです。この戦略に基づくすべてのメソッドは、オープンアドレスハッシュテーブルになります。
オープンアドレスハッシュテーブルの最も簡単な方法は線形検出方法です。つまり、競合が発生した場合(あるキーのハッシュ値が別のキーですでに占有されている場合)、ハッシュテーブルの次の位置(インデックス+1)を直接チェックします。それでも競合がある場合は、空の位置が検出されてキーと値のペアが挿入されるまで、逆方向の検出が続けられます。
データ構造は次のとおりです。
ここでは、キー[]を使用してキーを保存し、値[]を使用してキーに対応する値を保存します。
public class LinearProbingHashST<Key, Value> {
private Key[] keys;
private Value[] values;
/**
* 键值对数
*/
private int N;
/**
* 线性表大小
*/
private int M;
public LinearProbingHashST(int M) {
this.M = M;
keys = (Key[]) new Object[this.M];
values = (Value[]) new Object[this.M];
}
}
挿入操作:挿入
するキーのハッシュ値を計算し、現在のインデックスが他のキーで占められているかどうかを判断する必要があります。占有されているキーが挿入するキーでもある場合は、対応する値を変更します。それ以外の場合は、空の位置が見つかるまで繰り返します。 。
public void put(Key key, Value value) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (value==null){
delete(key);
}
//保证使用率 N/M 小于等于 1/2 ,当使用率趋近于1时,探测的次数会变得很大
if (N>=M*2){
resize(M*2);
}
int i;
for (i = hash(key); keys[i] != null; i = (i + 1) % M) {
if (keys[i].equals(key)) {
//待插入的key存在,修改对应的值并返回
values[i] = value;
return;
}
}
//找到空位置,插入键值对
keys[i] = key;
values[i] = value;
N++;
}
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % M;
}
クエリ操作:
キーのハッシュ値を計算します。現在の位置が競合している場合(他のキーによって占められている場合)、逆方向へのトラバースを続行します。トラバーサルの終了条件は、ハッシュ(キー)から次のnull位置までです。存在する場合は、対応するの値。存在しない場合はnull
public Value get(Key key) {
for (int i = hash(key); keys[i] != null; i = (i + 1) % M) {
if (keys[i].equals(key)) {
return values[i];
}
}
return null;
}
次のようなハッシュテーブルの場合:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
P M A C S H L E R X
10 9 8 4 0 5 11 12 3 7
Aのハッシュ値は4で、Hのハッシュ値も4ですが、4の競合のため、Hを挿入すると線形に7の位置に移動するため、Hに対応する値を検索すると、インデックス4から開始します。 、一致せず、順番に調べます。空の位置9の前に見つからない場合、Hがないことを意味しますが、Hは9の前の7の位置にあるため、インデックスは対応する値5
を返します。削除操作:
削除操作の場合、対応するキーを直接nullに設定することはできません。
上記のハッシュテーブルも同じです。Cが削除され、Hのハッシュ値が4の場合、線形検出のプロセスでは、インデックス5が空であり、Hがハッシュテーブルに存在しないと「誤って」考えて、nullを返します。しかし、実際にはHが存在し、インデックス7にあります。
したがって、特定のキーを削除した後、上記のエラーを回避するには、キー+ 1から次の空の位置にすべてのキーを再挿入する必要があります。
public void delete(Key key) {
if (!contains(key)) {
return;
}
int i=hash(key);
//线性探测找到待删除的key的索引
while (!keys[i].equals(key)){
i=(i+1)%M;
}
//置空
keys[i]=null;
values[i]=null;
//将i+1的位置到下一个空位置前的所有key重新插入到散列表中
i=(i+1)%M;
while (keys[i]!=null){
Key oldKey=keys[i];
Value oldValue=values[i];
keys[i]=null;
values[i]=null;
N--;
put(oldKey,oldValue);
i=(i+1)%M;
}
N--;
if (N>0 && N <= M/8){
resize(M/2);
}
}
またα= N/M
、ハッシュテーブルの使用率をαと呼び
、「アルゴリズム4」では以下の結論が得られています。
サイズMの線形検出に基づいており、N個のキーを含むハッシュテーブルで、ハッシュ関数が0とM-1の間のすべてのキーを均等かつ独立して分散できる場合、ヒットとミス検索に必要なプローブの数は、
1/2(1 + 1 /1-α)および1/2(1 + 1 /(1-α)^ 2)です。
つまり、ハッシュテーブルがほぼいっぱいになると、検索に必要な検出の数は膨大になります(αは1に近づきます)が、使用率α < 1/2
に達すると、検出の推定数は1.5から2.5の間だけなので、値は1/2以下です。
これに基づいて、
挿入前と削除後に配列を動的に調整する必要があります。
挿入する前に裁判官:
//保证使用率 N/M 不能超过1/2 ,当使用率趋近于1时,探测的次数会变得很大
if (N>=M*2){
resize(M*2);
}
削除後の判断:
メモリ使用量とキー値と表の数値の比が常に一定範囲内であることを確認してください。
//数组减小一半,如果N/M 为12.5% 或更少
if (N>0 && N <= M/8){
resize(M/2);
}