コンテンツ
1.ハッシュテーブルの概要
ハッシュテーブルはハッシュテーブルとも呼ばれます
最下層は、配列+単一リンクリスト+赤黒木によって実装されます
追加、検索、削除プロセス
(1)ハッシュ関数を使用して、キーO(1)に対応する配列インデックスインデックスを生成します。
(2)インデックス演算O(1)に従って配列要素を配置します。
ハッシュテーブルは時間のためのスペースのアイデアを採用しています
1.ハッシュ衝突
2つの異なるキー、同じ結果がハッシュ関数によって計算されます
解決:
(1)オープンアドレス法
空になるまで、特定のルールに従って他のアドレスにプローブします
-
線形検出
-
正方形の検出
(2)再ハッシュ
複数のハッシュ関数を設計する
(3)チェーンアドレス方式
たとえば、リンクリストを介して同じインデックスの要素をリンクします
(4)折りたたみ方法:キーワードを同じ桁数の複数の部分に分割し、最後の部分の桁数が異なる場合があります。次に、これらの部分の重ね合わせ(キャリーを削除)をハッシュアドレスとします。デジタル重ね合わせには、シフト重ね合わせと境界重ね合わせの2つの方法があります。シフトスタッキングとは、分割された各部分の最下位ビットを整列させてから追加することです。境界スタッキングとは、分割境界に沿って一方の端からもう一方の端まで前後に折りたたんでから、整列して追加することです。
(5)乱数法:乱数関数を選択し、キーワードのランダム値をハッシュアドレスとして使用します。つまり、H(key)= random(key)ここで、randomはランダム関数であり、通常、キーワードの長さが等しくありません。
(6)剰余の除算法:キーワードをハッシュテーブルテーブルの長さm以下の数pで除算した後の剰余をハッシュアドレスとします。つまり、H(key)= key MOD p、p<=mです。キーワードは直接モジュロにするだけでなく、折りたたみ、二乗、その他の操作の後にモジュロにすることもできます。pの選択は非常に重要です。一般に、素数またはmが使用されます。pが適切に選択されていない場合、同義語を生成するのは簡単です。
2.jdk1.8のハッシュ競合ソリューション
-
デフォルトでは、単一リンクリストを使用して要素をつなぎ合わせます
-
要素を追加する場合、要素を格納するために、単一リンクリストから赤黒木に変換される場合があります
-
たとえば、ハッシュテーブルの容量が64以上で、単一リンクリスト内のノード数が8より大きい場合
-
-
赤黒木のノード数がある程度少ない場合は、単一リンクリストに変換されます。
-
JDK1.8のハッシュテーブルは、リンクリストと赤黒木を使用してハッシュの衝突を解決します
なぜ単一リンクリストを使用するのですか?
(1)ハッシュ関数を使用してキーに対応するインデックスを計算した後、インデックスに従ってすべてのキーをトラバースします。キーが同じである場合、キーに対応する値が上書きされるため、新しい要素が追加されるたびに、リンクリストは最初からトラバースして配置する必要があります。リンクリストの最後に新しい要素が挿入されます。これは、単一リンクリストを使用して解決できます。
(2)単一リンクリストは、二重リンクリストよりもポインタが1つ少ないため、メモリスペースを節約できます。
2.ハッシュ関数
1.ハッシュテーブルのハッシュ関数の実装手順:
(1)最初にキーのハッシュ値を生成します(整数である必要があります)
(2)キーのハッシュ値を配列のサイズと相関させて、インデックス値を生成します。
public int hash(Object key){
return hash_code(key) % table.length;
}
効率を向上させるために、&ビット演算を使用して%演算を置き換えることができます[配列の長さを2 2^nの累乗になるように設計します]
public int hash(Object key){
return hash_code(key) & (table.length - 1);
}
&操作を実行すると、キーのハッシュ値が、配列の添え字の長さであるtable.length-1よりも小さくなる可能性があり、添え字が範囲外になることはありません。
良いハッシュ関数
ハッシュ値をより均等に分散させる->ハッシュ衝突の数を減らす->ハッシュテーブルのパフォーマンスを向上させる-
2.キーのハッシュ値を生成するにはどうすればよいですか?
一般的なタイプのキーには、次のものがあります。
整数、浮動小数点、文字列、カスタムオブジェクト
さまざまな種類のキー、ハッシュ値はさまざまな方法で生成されますが、目的は同じです
-
各キーのハッシュ値を一意にするようにしてください
-
キーのすべての情報を操作に参加させるようにしてください(キーのハッシュ値は異なる可能性が高くなります)
Javaのハッシュ値はint型である必要があります
(1)整数整数自体がハッシュ値として使用されます
(2)浮動小数点数
保存されたバイナリ形式を整数値に変換します
public static int hashCode(float value){
return floatToIntBits(value);
}
(3)ロングタイプ
public static int hashCode(long value){
return (int) (value ^ (value >>> 32));
}
(4)ダブルタイプ
public static int hashCode(double value){
long bits = doubleToLongBits(value);
return (int) (bits ^ (bit >>> 32));
}
>>>および^役割
上位32ビットと下位32ビットを混合して、32ビットハッシュ値を計算します
すべての情報を最大限に活用して、ハッシュ値を計算します
(5)文字列のハッシュ値
文字列は複数の文字で構成されます
たとえば、文字列ハッシュは4文字のh、a、s、およびhで構成されます(文字列の本質は整数です)
したがって、ハッシュのハッシュ値は、h * n ^ 3 + a * n ^ 2 + s * n + hとして表すことができます(n * n計算などの計算が繰り返され、n * n*nのときに計算されます。計算されます)は([h * n + a] * a + s)* n+hと同等です
JDKでは、乗数nは31です。
31は奇数の素数であり、JAMは31 * iを(i << 5)に最適化します-i
(6)カスタムオブジェクト
新しいPersonクラスを作成する
class Person{
//姓名
private String name;
//身高
private float height;
//年龄
private int age;
public Person() {
}
public Person(String name, float height, int age) {
this.name = name;
this.height = height;
this.age = age;
}
}
public class Demo01 {
public static void main(String[] args) {
Person p1 = new Person("张三",1.7f,20);
Person p2 = new Person("张三",1.7f,20);
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
Map<Object,Object> map = new HashMap<>();
map.put(p1,1);
map.put(p2,2);
System.out.println(map.size());
}
}
/**
*运行结果: 2129789493
* 668386784
* 2
*/
p1オブジェクトとp2オブジェクトの属性値はまったく同じです。このとき、属性値を同じに指定すると、それらは同じキーになりますが、hashCodeを直接呼び出した結果がわかりました。 ()メソッドが異なります。マップには2つのキーと値のペアがあります。
これはどのように解決できますか?
通常、クラスのhashCode()メソッドをオーバーライドします
@Override
public int hashCode() {
int hashCode = Integer.hashCode(age);
hashCode = hashCode * 31 + Float.hashCode(height);
hashCode = hashCode * 31 + (name != null ? name.hashCode(): 0);
return hashCode;
}
。
このように、p1オブジェクトとp2オブジェクトのハッシュ値は同じです。実際、hashCode()メソッドを書き換えると、jdkが自動的に書き換えます。
@Override
public int hashCode() {
return Objects.hash(name, height, age);
}
hash()メソッド-> hashCode()メソッドをクリックすると、jdkソースコードもこのように処理されていることがわかります
//将传入的值都放在object类型的数组a中
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
//循环遍历数组元素,计算每个元素的hashCode值 再计算整个数组a的哈希值
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
Javaでは、HashMapのキーはhashCodeとequalsメソッドを実装する必要があり、キーもnullにすることができます
なぜequalsメソッドを実装する必要があるのですか?
以前はhashCode()メソッドを書き直して、カスタムのPersonオブジェクトがまったく同じ属性を持ち、それらのハッシュ値が同じになるようにしましたが、現時点では、マップセットにp1とp2を追加して、 p1をカバーするためにp2を達成することは、いわゆるデヘビーですか?
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
Map<Object,Object> map = new HashMap<>();
map.put(p1,1);
map.put(p2,2);
System.out.println(map.size());
//运行结果:-407057726
// -407057726
// 2
実行結果では、マップセットのサイズが2であることがわかりました。これは、重複排除p1とp2が追加されていないことを示しています。
どうしてこれなの?
実際、多くの人は、ハッシュテーブルについて最初に知ったときに誤解しています。つまり、同じハッシュ値は同じ要素を意味します。これは、先ほどハッシュの衝突について説明したため、キーが異なるためです。計算されたハッシュ値ハッシュメソッドが同じである可能性がある場合、同じキーのハッシュ値は同じである必要があります。つまり、hashCode()メソッドを書き直して、キーのハッシュ値のみを計算し、同じキーが同じである場合、対応する配列インデックスも同じである必要があります。次に、ハッシュの競合が発生した場合、単一リンクリストを使用してそれを解決するときに、最初から最後までそれを比較して、キーが同じであるかどうかを判断します。簡単に言えば、ハッシュ値を比較して同じキーであるかどうかを判断することは信頼できません。
キーを比較する方法は?
==または等しいと考えるのは簡単です
==の場合、メモリアドレスを比較しています。オブジェクトを比較する場合、2つの新しい新しいオブジェクトのアドレスは同じではありませんが、属性はまったく同じであるため、比較のために==を選択することはありません。比較にはequalsメソッドを使用します。
//在Person类中重写equals方法
@Override
public boolean equals(Object obj) {
//如果内存地址相等则两个元素相等就是自己本身
if (this == obj) return true;
//如果obj为空 或者obj与本类Person不是同一个类,那么也必定不同
if (obj == null || getClass() != obj.getClass()) return false;
//传来的obj对象是Person类的实例
Person person = (Person) obj;
//比较height
if (Float.compare(person.height, height) != 0) return false;
//比较年龄
if (age != person.age) return false;
// 比较name
return name != null ? name.equals(person.name) : person.name == null;
}
この時点で、マップセットにp1とp2を追加し、重複排除を実現できます。!!
3.要約:
hashCode()メソッドは、ハッシュ値を計算し、同じ要素のハッシュ値が同じであることを確認してから、配列インデックスを見つけることです。
equals()メソッドは、ハッシュの衝突が発生したときに2つのキーが等しいかどうかを判断するためのものです
注:ハッシュ値を計算するときに配列インデックスを見つけるには、同じハッシュ値のインデックスが同じである必要があり、異なるハッシュ値のインデックスも同じである可能性があります。
(ハッシュ値を計算した後、配列のサイズで操作する必要があるため)
hashCodeとequalsメソッドを書き直した後、mapはp1と同じハッシュ値を持つp1、key1、p23つのキーと値のペアを追加します。
のプロセス
(1)キーと値のペア(p1、123)をマップセットに追加すると、ハッシュ関数はp1のハッシュ値を計算し、インデックスが1であると仮定して、配列の長さで&演算を実行します。インデックスデータが空であることがわかり、データを追加します
(2)マップセットは引き続きキーと値のペア( "key1"、456)を追加し、ハッシュ関数はハッシュ値を計算して&操作を実行し、"key1"に対応する配列インデックスも1であることを確認します。この時点でこのインデックスにデータがあることをインデックスから確認し、最初から最後までトラバースして比較し、「key1」がp1と同じではないことを確認し、キーと値のペア(「key1」、 456)
(3)マップセットは引き続きキーと値のペア(p2、789)を追加し、ハッシュ関数はハッシュ値を計算して&操作を実行し、p2に対応する配列インデックスが1であることを確認し、インデックスを通じて次のことを認識します。この時点で、このインデックスにはデータがあります。最初から最後までトラバースと比較を行い、p2をp1と同じにし、上書きします。
上記は、hashCodeとequalsメソッドを書き換えた後、ハッシュテーブルがハッシュ衝突と同じキーを解決するプロセスです。