C# でハッシュ テーブルのデータ構造を実装するコレクション クラスは次のとおりです。
(1) System.Collections.Hashtable
(2) System.Collections.Generic.Dictionary<TKey,TValue>
前者は一般的なタイプのハッシュ テーブル、後者はハッシュ テーブルの一般的なタイプです。ハッシュ テーブルの一般的な型のバージョンです。Dictionary と Hashtable の違いは、ジェネリックと非ジェネリックの単純な違いではなく、まったく異なるハッシュ競合解決方法を使用します。
8.3 ハッシュ競合の解決
ハッシュ関数の目的は競合を最小限に抑えることですが、実際のアプリケーションでは競合は避けられないため、競合が発生した場合には、対応する解決策が必要です。競合の可能性は、次の 2 つの要因に関連しています。
(1) 充填率 α: いわゆる充填率とは、ハッシュ アドレス空間のサイズ m に対するハッシュ テーブルに格納されるレコード数 n の比率、つまり α = n / m を指します。 αが小さいほど競合が発生する可能性が低く、αが大きいほど競合が発生する可能性が高くなります(最大値は1)。これは、α が小さいほどハッシュ テーブル内の空きユニットの割合が多くなり、挿入されるレコードと挿入されるレコードが競合する可能性が低くなり、逆に α が大きいほど、ハッシュテーブルの空きユニットの割合が小さくなるほど、挿入されるレコードと挿入されるレコードが競合する可能性が高くなり、一方、αが小さいほど、ストレージスペースの利用率が低いほど、ストレージスペースの利用率が低くなり、逆にストレージスペースの利用率が低くなります。競合の発生を減らし、記憶領域の使用率を向上させるために、α は通常 0.6 ~ 0.9 の範囲で制御されますが、C# の HashTable クラスでは α の最大値が 0.72 に設定されています。
(2) 使用するハッシュ関数に関係します。ハッシュ関数が適切に選択されていれば、ハッシュ アドレスがハッシュ アドレス空間内でできるだけ均等に分散されるため、競合の発生が減少しますが、そうでない場合は、ハッシュ アドレスが特定の領域に集中し、競合の発生が増加する可能性があります。可能性。
競合解決技術は、オープン ハッシュ法 (チェーン アドレス法とも呼ばれます) とクローズド ハッシュ法 (オープン アドレス法とも呼ばれます) の 2 つの主なカテゴリに分類できます。ハッシュ テーブルは、配列を使用して実装された連続的なアドレス空間です。2 つの競合解決手法の違いは、競合する要素が配列の空間の外に格納されるか、空間内に格納されるかです。
(1) ハッシュ法と競合する要素は配列空間の外に格納されます。「オープン」という言葉は、競合する要素を格納するために追加のスペースを「開く」必要があると理解できます。
(2) クローズドハッシュ法と競合する要素は配列空間に格納されます。「閉じた」という言葉は、競合するかどうかに関係なく、すべての要素が配列内で「閉じられている」ことを意味すると理解できます。オープン アドレス法とも呼ばれるクローズド ハッシュは、競合に関係なく、配列空間がすべての要素に対してオープンであることを意味します。
8.3.1 クローズドハッシュ方式(オープンアドレス方式)
クローズド ハッシュでは、すべての要素がハッシュ テーブル配列に保存されます。競合が発生すると、競合箇所の近くでレコードを保存できる空のユニットが検索されます。「次の」開口部を見つけるプロセスはサウンディングと呼ばれます。上記の方法は次の式で表すことができます。
hi=(h(キー)+di)%m i=1,2,...,k (k≤m-1)
このうち、h (key) はハッシュ関数、m はハッシュ テーブルの長さ、di はインクリメントのシーケンスです。di の値の違いに応じていくつかの検出方法に分けることができますが、ここでは Hashtable で使用されるダブルハッシュ方法のみを紹介します。
-
ダブルハッシュ
ダブル ハッシュ (2 次ハッシュとも呼ばれる) は、クローズド ハッシュ方式の中でも優れた方式であり、キーワードの別のハッシュ関数値を増分として使用します。2 つのハッシュ関数が h1 と h2 であると仮定すると、取得される検出シーケンスは次のようになります。
(h1(キー)+h2(キー))%m,(h1(キー)+2h2(キー))%m,(h1(キー)+3h2(キー))%m,…
このうち、mはハッシュテーブル長です。次のオープン アドレスを検出するためのダブル ハッシュの式は次のとおりであることがわかります。
(h1(キー) + i * h2(キー)) % m (1≤i≤m-1)
h2 を定義する方法はたくさんありますが、どのような方法を使用する場合でも、h2 (キー) の値は m と互いに素になる必要があります (共素とも呼ばれ、2 つの数値の最大公約数が 1 であることを意味します)。 2 つの数値には共通因数がありません。1 を除いて、競合するシノニム アドレスをハッシュ テーブル全体に均等に分散させることができます。そうでない場合は、シノニム アドレスの循環計算が発生する可能性があります。m が素数の場合、h2 が 1 から m-1 までの数値は m と互いに素であるため、h2 は次のように単純に定義できます。
h2(キー) = キー% (m - 2) + 1
The Mother of All Things オブジェクト クラスは GetHashCode() メソッドを定義します。このメソッドのデフォルトの実装では、オブジェクトの存続期間中に変更されないように、一意の整数値を返します。すべての型はオブジェクトから直接的または間接的に派生するため、すべてのオブジェクトがこのメソッドにアクセスできます。当然のことながら、文字列やその他の型は一意の数値で表すことができます。つまり、GetHashCode() メソッドは、すべてのオブジェクトのハッシュ関数の構築方法を統一します。もちろん、GetHashCode() メソッドは仮想メソッドであるため、このメソッドをオーバーライドして独自のハッシュ関数を構築することもできます。
8.4.1 ハッシュテーブルの実装原理
ハッシュ テーブルは、クローズド ハッシュ メソッドを使用して競合を解決します。構造バケットを通じてハッシュ テーブル内の 1 つの要素を表します。この構造には、次の 3 つのメンバーがあります。
(1) key: キー、つまりハッシュテーブル内のキーワードを示します。
(2)val:値、すなわちキーワードに対応する値を表す。
(3) hash_coll: キーに対応するハッシュコードを表すint型です。
int 型は 32 ビットの記憶領域を占有し、その最上位ビットは符号ビットで、「0」の場合は正の整数、「1」の場合は負の整数を意味します。Hash_coll は、現在位置で競合が発生しているかどうかを最上位ビットで表し、「0」、つまり正の数値の場合は競合なし、「1」の場合は競合なしを意味します。現在の位置に競合があることを示します。ハッシュ コードを格納し、競合が発生したかどうかをマークするためにビットが特別に使用される理由は、主にハッシュ テーブルの操作効率を向上させるためです。これについては後述します。
Hashtable は競合を解決するために二重ハッシュを使用しますが、上記の二重ハッシュ方法とは少し異なります。アドレスを検出する方法は次のとおりです。
h(キー, i) = h1(キー) + i * h2(キー)
ハッシュ関数 h1 と h2 の式は次のとおりです。
h1(キー) = キー.GetHashCode()
h2(キー) = 1 + (((h1(キー) >> 5) + 1) % (ハッシュサイズ - 1))
2 次元ハッシュを使用しているため、h(key, i) の最終値はハッシュサイズより大きくなる可能性があるため、h(key, i) に対してモジュロ演算を実行する必要があります。最終的に計算されたハッシュ アドレスは次のとおりです。
ハッシュアドレス = h(key, i) % ハッシュサイズ
[注意]: バケット構造体の hash_coll フィールドには、ハッシュアドレスの代わりに h(key, i) の値が格納されます。
ハッシュ テーブルのすべての要素は、buckets という名前のバケット配列 (データ バケットとも呼ばれます) に格納されます。以下は、データ要素が (キー、値、ハッシュ コード) を使用する、ハッシュ テーブルへのデータの挿入および削除のプロセスを示しています。表現します。この例では、ハッシュテーブルの長さが 11、つまり hashsize = 11 であると仮定していることに注意してください。ここでは最初の 5 要素のみが表示されます。
(1) 要素(k1, v1, 1)と(k2, v2, 2)を挿入します。
挿入された 2 つの要素間に競合がないため、h1(key) % hashsize の値がハッシュ コードとして直接使用され、h2(key) は無視されます。効果を図 8.6 に示します。
(2) 要素の挿入(k3、v3、12)
新しく挿入された要素のハッシュ コードは 12 です。ハッシュ テーブルの長さは 11、12 % 11 = 1 であるため、新しい要素はインデックス 1 に挿入される必要がありますが、インデックス 1 はすでに k1 によって占有されているため、h2 を使用する必要があります。 ( キー) ハッシュ コードを再計算します。
h2(キー) = 1 + (((h1(キー) >> 5) + 1) % (ハッシュサイズ - 1))
h2(キー) = 1 + ((12 >> 5) + 1) % (11 - 1)) = 2
- 新しいハッシュ アドレスは h1(key) + i * h2(key) = 1 + 1 * 2 = 3 なので、k3 はインデックス 3 に挿入されます。インデックス 1 で競合があるため、最上位ビットを「1」に設定する必要があります。
(10000000000000000000000000000001)2 = (-2147483647)10
最終的な効果を図 8.7 に示します。
( 3) 要素の挿入 (k4、v4、14)
k4 のハッシュ コードは 14 (14 % 11 = 3) で、インデックス 3 はすでに k3 によって占有されているため、アドレスは 2 次ハッシュを使用して再計算され、新しいアドレスは 14 になります。インデックス 3 に競合があるため、上位ビットを「1」に設定する必要があります。
(12)10 = (00000000000000000000000000001100)2 上位「1」以降
(10000000000000000000000000001100)2 = (-2147483636)10
最終的な効果を図 8.8 に示します。
(4) 要素k1、k2を削除する
Hashtable が競合する要素 (hash_coll は負の数) を削除すると、要素のキーが配列バケットを指すようにし、同時に要素の hash_coll の下位 31 ビットをすべて「0」に設定し、最上位を保持します。元の hash_coll が負の数であるため、最上位ビットは「1」になります。
(1000000000000000000000000000000)2 = (-2147483648)10
hash_coll 値が -2147483648 であるかどうかを単純に判断するだけでは、インデックスが空であるかどうかを判断することはできません。インデックス 0 で競合がある場合、その hash_coll 値も -2147483648 となり、キーがバケットを指すことになるためです。ここで、キーがバケットを指し、hash_coll 値が -2147483648 である欠員は、「競合欠員」と呼ばれます。図 8.8 に示すように、k1 が削除されると、インデックス 1 のギャップは競合ギャップになります。
Hashtable が競合せずに要素を削除すると (hash_coll は正の数)、キーと値の両方が null に設定され、hash_coll の値は 0 に設定されます。このような競合のない空きを「競合のない空き」と呼びます。図 8.9 に示すように、k2 が削除された後、インデックス 2 は競合のない空きです。ハッシュテーブルが初期化されると、バケット配列内のすべての位置が競合します。 -無料、空席あり。
ハッシュ テーブルがキーワードで要素を検索する場合、まずキーのハッシュ アドレスを計算し、次にこのハッシュ アドレスを通じて配列の対応する位置に直接アクセスし、2 つのキーの値を比較し、それらが同じであれば検索します。成功して返され、それらが異なる場合、次のステップは hash_coll の値に基づいて決定されます。hash_coll が 0 または正の数の場合は、競合がなくこの時点では検索が失敗することを示し、hash_coll が負の数の場合は、競合があることを示します。このとき、計算を続ける必要があります。ハッシュ アドレスから 2 次ハッシュを介して検索するなど、対応するものが見つかるまで続きます。キーの値は、検索が成功したことを示します。検索プロセス中に hash_coll が正の場合、または 2 次ハッシュの回数が正の場合計算された値がハッシュ テーブルの長さと等しい場合、検索は失敗します。hash_coll の上位ビットを競合ビットとして設定する主な目的は、検索速度を向上させ、無意味な 2 次ハッシュの複数回の計算を避けることであることがわかります。
8.4.2 ハッシュテーブルのコード実装
ハッシュテーブルの実装は比較的複雑ですが、コードを簡略化するため、この例では一部のエラー判定を無視していますので、テスト時にキー値を空に設定しないでください。
2 public class Hashtable
3 {
4 private structbucket 5 { 6 public Object key; //キー7 public Object val; //値8 public int hash_coll; //ハッシュ コード9 } 10 privatebucket []buckets ; //ハッシュテーブルのデータを格納する配列(データバケット)11 private int count; //要素数12 private int
loadsize; //現在保存できる要素の数は
13 です private float loadFactor; //フィルファクタ
14 //デフォルトの構築メソッド
15 public Hashtable() : this ( 0 , 1.0f ) { }
16 //構築メソッド容量を指定する
17 public Hashtable( int Capacity, floatloadFactor )
18 {
19 if ( ! (loadFactor >= 0.1f && loadFactor <= 1.0f ))
20 throw new ArgumentOutOfRangeException(
21 "フィル ファクターは 0.1 から 1 の間でなければなりません" );
22 this .loadFactor = loadFactor > 0.72f ? 0.72f :loadFactor;
23 //容量に応じてテーブルの長さを計算します
24 double rawsize = Capacity / this .loadFactor;
25 int hashsize = (rawsize > 11 ) ? //テーブルの長さは 11 より大きい素数です
26 HashHelpers.GetPrime(( int )rawsize) : 11 ;
27buckets = newbucket [hashsize]; //コンテナを初期化する28loadsize = ( int )( this .loadFactor * hashsize); 29 } 30 public virtual void Add (Object key, Object value) // Add 31 { 32 Insert ( key , value, true ); 33 } 34 //ハッシュコードの初期化35 private uint InitHash(Object key, int hashsize, 36
out uint seed, out uint incr)
37 {
38 uint hashcode = ( uint )GetHash(key) & 0x7FFFFFFF ; //絶対値を取得します
39 seed = ( uint )hashcode; // h1
40 incr = ( uint )( 1 + ( ((シード >> 5 ) + 1 ) % (( uint )hashsize - 1 )));// h2
41 return hashcode; //ハッシュ コードを返す
42 }
43 public virtual Object this [オブジェクト キー] //インデクサー
44 {
45 get
46 {
47 uint seed; // h1
48 uint incr; // h2
49 uint hashcode = InitHash(key,buckets.Length,
50 out シード、 out incr);
51 int ntry = 0 ; // h(key,i) 52bucket b; 53 int bn = ( int )(seed % ( uint )buckets.Length); // h(key,0) 54 do 55の i 値を表すために使用されます { 56 b = buckets[bn]; 57 if (b.key == null ) // b は競合のないスロット58 { //対応するキーが見つからない、return empty 59 return null ; 60 }
61 if (((b.hash_coll & 0x7FFFFFFF ) == ハッシュコード) &&
62 KeyEquals(b.key, key))
63 { //查找成功
64 return b.val;
65 }
66 bn = ( int )((( long )bn + incr) %
67 ( uint )buckets.Length); // h(key+i)
68 } while (b.hash_coll < 0 && ++ ntry < buckets.Length);
69 return null ;
70 }
71 set
72 {
73 Insert(key, value, false );
74 }
75 }
76 private void Expand() //展開
77 { //新規作成容量は
以前 の 容量 の 約 2倍 です 。
80 }
81 private void rehash( int newsize) //按新容量扩容
82 {
83 Bucket[] newBuckets = 新しい バケット[newsize];
84 for ( int nb = 0 ; nb < buckets.Length; nb ++ )
85 {
86 バケット oldb = バケット[nb];
87 if ((oldb.key != null ) && (oldb.key != バケット))
88 {
89 putEntry(newBuckets, oldb.key, oldb.val,
90 oldb.hash_coll & 0x7FFFFFFF );
91 }
92 }
93 バケット = newBuckets;
94 ロードサイズ = ( int )(loadFactor * newsize);
95 return ;
96 }
97 //古い配列の要素を新しい配列に追加します
98 private void putEntry(bucket[] newBuckets, Object key,
99 オブジェクト nvalue, int ハッシュコード)
100 {
101 uint シード = ( uint ) ハッシュコード; // h1
102 uint incr = ( uint )( 1 + (((seed >> 5 ) + 1 ) %
103 (( uint )newBuckets.Length - 1 ))); // h2
104 int bn = ( int )(seed % (uint )newBuckets.Length); //ハッシュ アドレス
105 do
106 { //現在の位置が競合空席または競合のない空席の場合、新しい要素
107を追加できます if ((newBuckets[bn].key == null ) ||
108 (newBuckets[bn].key == バケット))
109 { //値を割り当てる
110 newBuckets[bn].val = nvalue;
111 newBuckets[bn].key = key;
112 newBuckets[bn].hash_coll |= ハッシュコード;
113 リターン;
114 }
115 //現在の位置に他の要素が既に存在する場合
116 if (newBuckets[bn].hash_coll >= 0 )
117 { // hash_coll の上位ビットを 1 に設定
118 newBuckets[bn].hash_coll |=
119 unchecked (( int ) 0x80000000 );
120 }
121 //セカンダリ ハッシュ h1(key)+h2(key)
122 bn = ( int )((( long )bn + incr) % ( uint)newBuckets.Length);
123 } while ( true );
124 }
125 protected virtual int GetHash(Object key)
126 { //ハッシュコードを取得する
127 return key.GetHashCode();
128 }
129 protected virtual bool KeyEquals(Object item , オブジェクト key)
130 { // 2 つのキーが等しいかどうかを判断するために使用されます
131 return item == null ? false : item.Equals(key);
132 }
133 // add が true の場合は要素の追加に使用され、add が false の場合は要素の値の変更に使用されます
134 private void Insert(Object key, Object nvalue, bool add)
135 { //上限の場合保存できる要素数を超えた場合、拡張
136 if (count >= loadsize)
137 {
138 Expand();
139 }
140 uint seed; // h1
141 uint incr; // h2
142 uint hashcode = InitHash (キー、バケット.長さ、アウト シード、 アウト 増分) ;
143 int ntry = 0 ; // h(key,i) の i 値を表すために使用されます
144 int emptySlotNumber = - 1 ; //空のスロットを記録するために使用されます
145 int bn = ( int )(seed % ( uint )buckets. Length ); //インデックス番号
146 do
147 { //競合する空きがある場合は、同じキーが存在するかどうかを判断するために後方検索を続ける必要があります
148 if (emptySlotNumber == - 1 && (buckets[bn].key = = バケット) &&
149 (buckets[bn].hash_coll < 0 ))
150 {
151 emptySlotNumber = bn;
152 }
153 if (buckets[bn].key == null ) //追加する前に重複キーがないことを確認してください
154 {
155 if ( emptySlotNumber != - 1 ) //前の空のスロットを使用
156 bn = emptySlotNumber;
157buckets [bn].val = nvalue;
158 Buckets[bn].key = key;
159 Buckets[bn].hash_coll |= ( int )hashcode;
160 count ++ ;
161 return ;
162 }
163 //重複キーが見つかりました
164 if (((buckets[bn].hash_coll & 0x7FFFFFFF ) == hashcode) &&
165 KeyEquals(buckets[bn].key, key))
166 { //要素を追加している状態の場合、キーが重複しているためエラー167が報告されますif (add) 168
{
169 throw new ArgumentException( "重複したキー値が追加されました! " );
170 }
171buckets [bn].val = nvalue; //バッチキーの要素を変更します
172 return ;
173 }
174 //競合がある場合、 set hash_coll 最上位ビットは 1
175 if (emptySlotNumber == - 1 )
176 {
177 if (buckets[bn].hash_coll >= 0 )
178 {
179buckets [bn].hash_coll |= unchecked (( int ) 0x80000000 );
180 }
181 }
182 bn = ( int )((( long )bn + incr) % ( uint )buckets.Length); //第 2 度 ha 183 } while ( ++ ntry < Buckets.Length); 184 throw
new InvalidOperationException ( "追加に失敗しました! "
);
185 }
186 public virtual void Remove(Object key) //要素を削除します
187 {
188 uint seed; // h1
189 uint incr; // h2
190 uint hashcode = InitHash(key,buckets.Length, out seed, out incr);
191 int ntry = 0 ; // i in h(key,i)
192 バケット b;
193 int bn = ( int)(seed % ( uint )buckets.Length); //ハッシュアドレス
194 do
195 {
196 b = buckets[bn];
197 if (((b.hash_coll & 0x7FFFFFFF ) == ハッシュコード) &&
198 KeyEquals(b. key , key)) //対応するキー値が見つかった場合
199 { //最上位ビットを保持し、残りを 0 にクリア
200buckets [bn].hash_coll &= unchecked (( int ) 0x80000000 );
201 if (buckets[bn].hash_coll != 0 ) //もともと競合があった場合
202 { //キーポイントをバケットに設定する
203 Buckets[bn].key = buckets;
204 }
205 else //もともと競合はなかった
206 { //キーを空に設定します
207 Buckets[bn].key = null ;
208 }
209 Buckets[bn].val = null ; //対応する「値」を解放します。
210 カウント-- ;
211 リターン;
212 } //二度哈希
213 bn = ( int )((( long )bn + incr) % ( uint )buckets.Length);
214 } while (b.hash_coll < 0 && ++ ntry < buckets.Length);
215 }
216 パブリック オーバーライド 文字列 ToString()
217 {
文字 列219 for ( int i = 0 ; i < buckets.Length; i ++ ) 220 { 221 if ( buckets [ i ] .key ! = null && buckets [ i].key != バケット) 222 { //空でない場合はインデックス、キー、値、hash_coll を出力します223 s += string .Format( " {0,-5}{1,-8}{2,-8}{3,-8}\r \ n " 、224 i.ToString()、buckets[i].key.ToString()、
225buckets [i].val.ToString(),
226buckets [i].hash_coll.ToString());
227 }
228 else
229 { //空の場合、インデックスと hash_coll を出力します
230 s += string .Format ( " {0,-21}{1,-8}\r\n " , i.ToString(),
231 Buckets[i].hash_coll.ToString());
232 }
233 }
234 return s;
235 }
236 パブリック 仮想 整数の 数 //属性
237 { //要素数を取得
238 get { return count; }
239 }
240 }
HashtableとArrayListの実装は似ており、たとえば、どちらも配列に基づいてさらに抽象化されており、容量を自動的に指数関数的に拡張できます。