ハッシュ法の続きです。今回はオープンアドレス法について説明します。
オープンアドレス法の場合、チェイン法とは違ってハッシュ表に直接データをセットするので、衝突が発生したとき別の空き場所を探す手順が必要になります。この手順のことを「再ハッシュ (rehashing)」といいます。
再ハッシュの手順はいくつかの方法がありますが、その中で最も簡単な方法が「線形走査法 (linear probing)」です。ハッシュ関数を h(x)、ハッシュ表の大きさを M とすると、k 回目の再ハッシュ関数 hk(x) は次の式で表すことができます。
最初の再ハッシュ関数 \(h_1(x)\) は (h(x) + 1) % M で、2 回目の再ハッシュ関数 \(h_2(x)\) は (h(x) + 2) % M になります。つまり、線形走査法はハッシュ表の空き場所を順番に調べていく「線形探索」と同じです。本ページでは線形走査法でオープンアドレス法の仕組みを説明することにします。このほかに「二重ハッシュ法 (double hashing)」という方法がありますが、これはあとで取り上げることにします。
最初にデータの挿入から説明します。下図を見てください。
ハッシュ表 ハッシュ表 ┌───┐ ┌───┐ │ / │ │ / │ ├───┤ ├───┤ │ A │ │ A ┼─┐ 衝突 (E) ├───┤ ├───┤ │ │ / │ │ E │←┘ ├───┤ ├───┤ │ / │ │ / │ ├───┤ ├───┤ │ B │ │ B ┼─┐ 衝突 (D, F) ├───┤ ├───┤ │ │ / │ │ D ┼←┤ 衝突 (F) ├───┤ ├───┤ │ │ C │ │ C ┼←┤ 衝突 (F) ├───┤ ├───┤ │ │ / │ │ F │←┘ └───┘ └───┘ A (1), B (4), C (6) を挿入 D (4), E (1), F (4) を挿入 図 : オープンアドレス法(線形走査法)
最初にデータ A, B, C を挿入します。ハッシュ値の場所 (1, 4, 6 番目) にデータをセットします。次に、データ D を挿入します。ハッシュ値は 4 ですが、B と衝突しています。線形走査法の場合、次の場所は (4 + 1) % 8 で 5 になります。そこで、D を 5 番目にセットします。同様に E を挿入しますが A と衝突しているので、その隣の場所 (2) に E をセットします。
最後に F を挿入しますが、B と衝突しています。そこで再ハッシュを行いますが、1 回目は (4 + 1) % 8 = 5 で D と衝突します。2 回目は (4 + 2) % 8 = 6 ですが、C と衝突します。3 回目で (4 + 3) % 8 = 7 になり、この場所に F を挿入します。
データの探索も簡単です。データのハッシュ値 n を求め、ハッシュ表の n 番目に格納されているデータと比較します。等しい場合は探索成功です。そうでなければ、再ハッシュを行って次のデータと比較します。そこが空き場所ならば、探索は失敗となります。
たとえば B を探す場合、ハッシュ値は 4 なので、ハッシュ表の 4 番目に格納されている値と比較します。この場合は等しいので探索成功です。F を探索する場合、最初に B と比較します。次に、再ハッシュを行い 5, 6, 7 番目と順番にデータを比較していきます。そして、7 番目の F で探索成功となります。ハッシュ値が 1 のデータ G を探索する場合は、最初に A と比較し、次に E と比較します。その次に 3 番目のデータと比較しますが、空き場所なので探索は失敗となります。
オープンアドレス法の場合、データの探索と挿入だけならば簡単なのですが、データの削除処理がからむとちょっと複雑になります。次の図を見てください。
ハッシュ表 ハッシュ表 ┌───┐ ┌───┐ │ / │ │ / │ ├───┤ ├───┤ │ A │ │ A │ ├───┤ ├───┤ │ E │ │ E │ ├───┤ ├───┤ │ / │ │ / │ ├───┤ ├───┤ │ B │ │ B ┼─┐Fの探索 ├───┤ ├───┤ │ │ D │ │ D ┼←┤探索継続 ├───┤ ├───┤ │ │ C │─Cを削除→│ / │←┘終了(失敗) ├───┤ ├───┤ │ F │ │ F │ └───┘ └───┘ データを挿入した順番 A (1), B (4), C (6), D (4), E (1), F (4) 図 : データの削除 (1)
ハッシュ表 ハッシュ表 ┌───┐ ┌───┐ │ / │ │ / │ ├───┤ ├───┤ │ A │ │ A │ ├───┤ ├───┤ │ E │ │ E │ ├───┤ ├───┤ │ / │ │ / │ ├───┤ ├───┤ │ B │ │ B ┼─┐Fの探索 ├───┤ ├───┤ │ │ D │ │ D ┼←┤探索継続 ├───┤ ├───┤ │ │ C │─Cを削除→│DEL┼←┤探索継続 ├───┤ ├───┤ │ │ F │ │ F │←┘成功 └───┘ └───┘ データを挿入した順番 A (1), B (4), C (6), D (4), E (1), F (4) 図 : データの削除 (2)
データ C を削除します。単純に考えると、ハッシュ表の 6 番目を空き場所にすればよさそうですが、実はそうはいかないのです。6 番目を空き場所にした状態で、データ F を探索してみましょう。F のハッシュ値は 4 で B と衝突します。そこで、再ハッシュを行うと (4 + 1) % 8 = 5 になりますが、ここでも D と衝突します。そして、再ハッシュを行い (4 + 2) % 8 = 6 になりますが、この場所は空き場所なので探索は失敗となります。
このように、データを単純に削除すると、再ハッシュを行ったデータをたどることができなくなるのです。そこで、削除したことを表すデータ DEL を用意します。そして、データが DEL のときは探索を続けるように手順を変更します。図 (2) を見てください。
データ C を削除する場合、その場所に DEL を書き込みます。そのあと、データ F を探索する場合、D と衝突したあとの再ハッシュで (4 + 2) % 8 = 6 になります。今度は空き場所ではなく DEL になっているので、再ハッシュを行って探索を続けます。今度はデータ F を見つけることができます。
なお、新しいデータを挿入する場合は、空き場所または削除した場所を探して、そこにデータを書き込むだけです。
オープンアドレス法の場合、データの最大数はハッシュ表の大きさに制限されます。また、データ数が多くなるとハッシュ値の衝突が頻発するため、その性能は劣化してしまいます。とくに線形走査法の場合、再ハッシュのたびに連続した領域を使用するため、データがハッシュ表に分散するのではなく、特定の領域に偏って存在するようになります。このような現象を「クラスター (clustering)」といいます。これがハッシュ値の衝突をさらに増やすことになり、線形走査法では性能が急激に悪くなります。参考文献『Cプログラマのためのアルゴリズムとデータ構造』によると、線形走査法の場合ハッシュ表の最大使用率は 80 % を目安にするとよいそうです。
それから、データの削除を行う場合、データだけではなく削除データ (DEL) が増えても性能が劣化することに注意してください。ようするに、オープンアドレス法の場合、ハッシュ表の空き場所が少なくなると性能が劣化するのです。データの挿入と削除を繰り返すと空き場所は減少していくので、データ数が少ない状態でも探索が遅くなる場合もあるのです。このため、オープンアドレス法で削除処理を行うときは、ハッシュ表の再構築を考慮する必要があると思われます。削除処理を行う場合は、チェイン法を使ったほうが簡単かもしれません。
それではプログラムを作りましょう。最初にデータ構造を定義します。次のリストを見てください。
リスト : データ型の定義 #define EMPTY 0 #define DEL 1 #define ANY 2 // ハッシュ表の要素 typedef struct { int kind; char *key; int value; } Item; // ハッシュ表 typedef struct { int size, count; Item *table; } HashTable1;
ハッシュ表の名前は HashTable1 としました。ハッシュ表の本体 table の要素を構造体 Item で表します。table のデータ型は Item * になります。メンバ変数 kind は空き場所 (EMPTY)、削除データ (DEL)、データ有り (ANY) を表します。メンバ変数 key がキーを、value が値を表します。
次はハッシュ表を生成する関数 make_hash_table1 を作ります。
リスト : ハッシュ表の生成と廃棄 // ハッシュ表の生成 HashTable1 *make_hash_table1(int size) { HashTable1 *ht = malloc(sizeof(HashTable1)); if (ht != NULL) { ht->table = malloc(sizeof(Item) * size); if (ht->table == NULL) { free(ht); return NULL; } ht->size = size; ht->count = 0; for (int i = 0; i < size; i++) { ht->table[i].kind = EMPTY; ht->table[i].key = NULL; } } return ht; }
最初に HashTable1 の領域を malloc で取得します。次に、table の本体を取得します。大きさは sizeof(Item) * size になります。そのあと、table を空き場所 (EMPTY) で初期化します。このとき、いっしょに key も NULL に初期化しておきます。
次は作業用の関数として、ハッシュ関数 hash_func1 とキーを探索する関数 search_keyを作ります。
リスト : 作業用関数 // ハッシュ関数 int hash_func1(HashTable1 *ht, const char *key) { unsigned int value = 0; for (; *key != '\0'; key++) value = (value << 3) + *key; return value % ht->size; } // キーの探索 int search_key(HashTable1 *ht, const char *key) { int n = hash_func1(ht, key); int c = 0; Item *table = ht->table; while (c < ht->size) { if (table[n].kind == EMPTY) break; if (table[n].kind == ANY && strcmp(key, table[n].key) == 0) return n; n = (n + 1) % ht->size; c++; } return -1; }
hash_func1 は前回作成したハッシュ関数と同じです。search_key はハッシュ表からキー key を探索し、見つけた場合はその位置 (添字) を返します。見つからない場合は -1 を返します。
最初にハッシュ値を求め変数 n にセットし、while ループでキーを探索します。table[n].kind が EMPTY であれば探索は失敗です。break で while ループを脱出して -1 を返します。kind が ANY で table[n].key と引数 key が等しい場合、探索は成功です。return で n を返します。そうでなければ、再ハッシュ (n + 1) % ht->size を行って探索を続行します。c が ht->size 以上になる場合は探索失敗です。while ループを終了して -1 を返します。
データの探索は search_key を使うと簡単です。
リスト : データの探索 int search_hash1(HashTable1 *ht, const char *key, bool *err) { int n = search_key(ht, key); if (n >= 0) { *err = true; return ht->table[n].value; } *err = false; return 0; }
search_key を呼び出して、返り値 n が 0 以上であれば、その位置に格納されている value を返します。そうでなければ、*err に false をセットして return で 0 を返します。
次はデータを挿入する関数 insert_hash1 を作ります。次のリストを見てください。
リスト : データの挿入 bool insert_hash1(HashTable1 *ht, const char *key, int val) { int n = search_key(ht, key); if (n >= 0) { ht->table[n].value = val; return true; } int c = 0; Item *table = ht->table; n = hash_func1(ht, key); while (c < ht->size) { if (table[n].kind != ANY) { table[n].key = malloc(strlen(key) + 1); if (table[n].key == NULL) break; strcpy(table[n].key, key); table[n].kind = ANY; table[n].value = val; ht->count++; return true; } n = (n + 1) % ht->size; c++; } return false; }
最初に search_key を呼び出して同じキーがないか調べます。同じキーが見つかった場合は、キーに対応する値を引数 val に書き換えます。見つからない場合は、while ループで空き場所または削除した場所を探して、そこにデータを書き込みます。
kind が ANY でなければ、そこにデータを書き込むことができます。キーの領域を malloc で取得して、table[n].key にセットします。strcpy でキーをコピーして、kind に ANY を、value に val をセットします。そして、ht->count を +1 して true を返します。
データがセットされていれば再ハッシュ (n + 1) % ht->size を行って、空き場所または削除した場所を探します。c が ht->size 以上になったら、空き場所がないので while ループを終了して false を返します。
次はデータの削除を行う関数 delete_hash1 を作ります。
リスト : データの削除 bool delete_hash1(HashTable1 *ht, const char *key) { int n = search_key(ht, key); if (n >= 0) { ht->table[n].kind = DEL; free(ht->table[n].key); ht->table[n].key = NULL; ht->count--; return true; } return false; }
メソッド search_key でハッシュ表からキー (key) を探します。見つかった場合は、kind を DEL に書き換えて、key の領域を free で解放します。それから、key を NULL に書き換えて、count を -1 してから return で true を返します。キーが見つからない場合は false を返します。
あとはとくに難しいところはないので説明は割愛いたします。詳細はプログラムリスト1をお読みください。
それでは簡単な例題として、前回と同様のテストを行ってみましょう。
リスト : 簡単なテスト void test1(void) { char buff[8][12]; HashTable1 *ht = make_hash_table1(13); bool err; printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- insert -----\n"); for (int i = 0; i < 8; i++) { sprintf(buff[i], "%d", rand()); printf("%s, %d\n", buff[i], insert_hash1(ht, buff[i], i)); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("------ search ------\n"); for (int i = 0; i < 8; i++) printf("%s, %d\n", buff[i], search_hash1(ht, buff[i], &err)); printf("------ delete ------\n"); for (int i = 0; i < 8; i++) { printf("%s %d\n", buff[i], delete_hash1(ht, buff[i])); int x = search_hash1(ht, buff[i], &err); printf("%s, %d, %d\n", buff[i], x, err); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- delete hash -----\n"); delete_hash_table1(ht); }
$ clang hash1.c $ ./a.out -- 1, 0 -- ----- insert ----- 1804289383, 1 846930886, 1 1681692777, 1 1714636915, 1 1957747793, 1 424238335, 1 719885386, 1 1649760492, 1 -- 0, 8 -- ------ search ------ 1804289383, 0 846930886, 1 1681692777, 2 1714636915, 3 1957747793, 4 424238335, 5 719885386, 6 1649760492, 7 ------ delete ------ 1804289383 1 1804289383, 0, 0 846930886 1 846930886, 0, 0 1681692777 1 1681692777, 0, 0 1714636915 1 1714636915, 0, 0 1957747793 1 1957747793, 0, 0 424238335 1 424238335, 0, 0 719885386 1 719885386, 0, 0 1649760492 1 1649760492, 0, 0 -- 1, 0 -- ----- delete hash -----
次はキーの個数を増やして実行時間を計測してみましょう。
リスト : 簡単なテスト2 void test2(int size) { HashTable1 *ht = make_hash_table1(100003); bool err; char buff[12]; while (length_hash1(ht) < size) { sprintf(buff, "%d", rand()); search_hash1(ht, buff, &err); if (!err) insert_hash1(ht, buff, 1); } delete_hash_table1(ht); } int main(void) { test1(); for (int i = 95000; i <= 100000; i += 250) { clock_t s = clock(); test2(i); printf("%d, %.3f\n", i, (double)(clock() - s) / CLOCKS_PER_SEC); } return 0; }
ハッシュ表の大きさは 100003 (素数) とし、作成するデータ数を 95000 から 100000 まで増やして実行時間を計測します。結果は次のようになりました。
表 : 実行結果 (単位 : 秒) 個数 : OPEN CHAIN 個数 : OPEN CHAIN -------+----------------------+--------------- 95000 : 0.072 0.039 97750 : 0.134 0.048 95250 : 0.075 0.038 98000 : 0.164 0.044 95500 : 0.070 0.046 98250 : 0.181 0.048 95750 : 0.073 0.049 98500 : 0.177 0.049 96000 : 0.078 0.047 98750 : 0.197 0.048 96250 : 0.081 0.048 99000 : 0.177 0.050 96500 : 0.105 0.046 99250 : 0.219 0.045 96750 : 0.106 0.046 99500 : 0.282 0.050 97000 : 0.113 0.047 99750 : 0.927 0.044 97250 : 0.128 0.048 100000 : 0.782 0.049 97500 : 0.135 0.049
実行環境 : clang version 14.0.0, Ubunts 22.04 LTS (WSL2, Windows 10), Intel Core i5-6200U 2.30GHz 最適化オプション : -O2
ハッシュ表の大きさはチェイン法も同じです。オープンアドレス法の場合、ハッシュ表に占めるデータ数が少ないうちはチェイン法と同等以上の性能を発揮しますが、空き場所が少なくなってくると性能はどんどん劣化していきます。そこで、次は「二重ハッシュ法」を試してみましょう。
「二重ハッシュ法 (double hashing)」は二種類のハッシュ関数を使う方法です。最初のハッシュ関数を h(x), 二番目のハッシュ関数を g(x), ハッシュ表の大きさを M とすると、二重ハッシュ法は次のような手順になります。
二重ハッシュ法で g(x) = 1 とすると線形走査法になります。g(x) は簡単な関数にしないと時間がかかってしまいます。実際、g(x) は次に示すような簡単な関数で十分なようです。
g(x) = q - (h(x) % q)
q は M よりも小さな非負整数値が選ばれます。たとえば、q = 7 とすると、g(x) の値は 1 から 7 までの範囲になります。つまり、二重ハッシュ法は n (1 <= n <= q) 個おきにハッシュ表を調べていくことになります。そして、ハッシュ値によって n の値を変えることで、クラスターの発生を回避することができます。
ただし、M と n は互いに素になっていないと、ハッシュ表をすべて調べることはできません。このため、M は素数とするのが普通です。また、g(x) の値が 1 にならないように、値を +1 する方法もあります。
プログラムは次のようになります。
リスト : 二重ハッシュ法 int hash_func2(HashTable1 *ht, const char *key, int *m) { unsigned int value = 0; for (; *key != '\0'; key++) value = (value << 3) + *key; *m = 8 - value % 7; return value % ht->size; } // キーの探索 int search_sub(HashTable1 *ht, const char *key) { int val; int n = hash_func2(ht, key, &val); int c = 0; Item *table = ht->table; while (c < ht->size) { if (table[n].kind == EMPTY) break; if (table[n].kind == ANY && strcmp(key, table[n].key) == 0) return n; n = (n + val) % ht->size; c++; } return -1; }
関数 hash_func2 の返り値は今までと同じハッシュ値ですが、引数 *m に 2 番目のハッシュ関数 1 + (7 - n % 7) の値をセットとします。関数 search_key は、hash_func2 でハッシュ値を求めて、変数 n と val にセットします。val がハッシュ表を探索するときの増分値になります。したがって、再ハッシュの計算は (n + val) % ht->size になります。
再ハッシュの計算が異なるだけで、あとは線形走査法のプログラムと同じです。プログラムの説明は割愛しますので、詳細はプログラムリスト2をお読みください。
それでは実行結果を示します。
表 : 実行結果 (単位 : 秒) 個数 : OPEN CHAIN DOUBLE ----------------------------- 95000 : 0.072 0.039 0.048 95250 : 0.075 0.038 0.048 95500 : 0.070 0.046 0.047 95750 : 0.073 0.049 0.051 96000 : 0.078 0.047 0.054 96250 : 0.081 0.048 0.056 96500 : 0.105 0.046 0.062 96750 : 0.106 0.046 0.050 97000 : 0.113 0.047 0.056 97250 : 0.128 0.048 0.060 97500 : 0.135 0.049 0.062 97750 : 0.134 0.048 0.059 98000 : 0.164 0.044 0.076 98250 : 0.181 0.048 0.074 98500 : 0.177 0.049 0.079 98750 : 0.197 0.048 0.091 99000 : 0.177 0.050 0.081 99250 : 0.219 0.045 0.094 99500 : 0.282 0.050 0.106 99750 : 0.927 0.044 0.283 100000 : 0.782 0.049 0.253
線形走査法よりも二重ハッシュ法の方が速くなりました。二重ハッシュ法は簡単な関数でも大きな効果を発揮するようです。興味のある方はいろいろ試してみてください。
/* * hash1.c : ハッシュ法 (オープンアドレス法) * * Copyright (C) 2015-2023 Makoto Hiroi */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #include <time.h> #define EMPTY 0 #define DEL 1 #define ANY 2 // ハッシュ表の要素 typedef struct { int kind; char *key; int value; } Item; // ハッシュ表 typedef struct { int size, count; Item *table; } HashTable1; // ハッシュ表の生成 HashTable1 *make_hash_table1(int size) { HashTable1 *ht = malloc(sizeof(HashTable1)); if (ht != NULL) { ht->table = malloc(sizeof(Item) * size); if (ht->table == NULL) { free(ht); return NULL; } ht->size = size; ht->count = 0; for (int i = 0; i < size; i++) { ht->table[i].kind = EMPTY; ht->table[i].key = NULL; } } return ht; } // ハッシュ表の廃棄 void delete_hash_table1(HashTable1 *ht) { free(ht->table); free(ht); } // ハッシュ関数 int hash_func1(HashTable1 *ht, const char *key) { unsigned int value = 0; for (; *key != '\0'; key++) value = (value << 3) + *key; return value % ht->size; } // 見つけた位置を返す、未発見は -1 int search_key(HashTable1 *ht, const char *key) { int n = hash_func1(ht, key); int c = 0; Item *table = ht->table; while (c < ht->size) { if (table[n].kind == EMPTY) break; if (table[n].kind == ANY && strcmp(key, table[n].key) == 0) return n; n = (n + 1) % ht->size; c++; } return -1; } // 探索 int search_hash1(HashTable1 *ht, const char *key, bool *err) { int n = search_key(ht, key); if (n >= 0) { *err = true; return ht->table[n].value; } *err = false; return 0; } // 挿入 bool insert_hash1(HashTable1 *ht, const char *key, int val) { int n = search_key(ht, key); if (n >= 0) { ht->table[n].value = val; return true; } int c = 0; Item *table = ht->table; n = hash_func1(ht, key); while (c < ht->size) { if (table[n].kind != ANY) { table[n].key = malloc(strlen(key) + 1); if (table[n].key == NULL) break; strcpy(table[n].key, key); table[n].kind = ANY; table[n].value = val; ht->count++; return true; } n = (n + 1) % ht->size; c++; } return false; } // 削除 bool delete_hash1(HashTable1 *ht, const char *key) { int n = search_key(ht, key); if (n >= 0) { ht->table[n].kind = DEL; free(ht->table[n].key); ht->table[n].key = NULL; ht->count--; return true; } return false; } // ハッシュ表は空か bool is_empty_hash1(HashTable1 *ht) { return ht->count == 0; } // ハッシュ表のデータ数を返す int length_hash1(HashTable1 *ht) { return ht->count; } // 巡回 void foreach_hash1(void (*func)(const char *, int), HashTable1 *ht) { Item *table = ht->table; for (int i = 0; i < ht->size; i++) { if (table[i].kind == ANY) func(table[i].key, table[i].value); } } // ハッシュ表をクリアする void clear_hash1(HashTable1 *ht) { ht->count = 0; for (int i = 0; i < ht->size; i++) { if (ht->table[i].kind == ANY) { free(ht->table[i].key); ht->table[i].key = NULL; } ht->table[i].kind = EMPTY; } } // 簡単なテスト void test1(void) { char buff[8][12]; HashTable1 *ht = make_hash_table1(13); bool err; printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- insert -----\n"); for (int i = 0; i < 8; i++) { sprintf(buff[i], "%d", rand()); printf("%s, %d\n", buff[i], insert_hash1(ht, buff[i], i)); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("------ search ------\n"); for (int i = 0; i < 8; i++) printf("%s, %d\n", buff[i], search_hash1(ht, buff[i], &err)); printf("------ delete ------\n"); for (int i = 0; i < 8; i++) { printf("%s %d\n", buff[i], delete_hash1(ht, buff[i])); int x = search_hash1(ht, buff[i], &err); printf("%s, %d, %d\n", buff[i], x, err); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- delete hash -----\n"); delete_hash_table1(ht); } void test2(int size) { HashTable1 *ht = make_hash_table1(100003); bool err; char buff[12]; while (length_hash1(ht) < size) { sprintf(buff, "%d", rand()); search_hash1(ht, buff, &err); if (!err) insert_hash1(ht, buff, 1); } delete_hash_table1(ht); } int main(void) { test1(); for (int i = 95000; i <= 100000; i += 250) { clock_t s = clock(); test2(i); printf("%d, %.3f\n", i, (double)(clock() - s) / CLOCKS_PER_SEC); } return 0; }
/* * hash2.c : ハッシュ法 (二重ハッシュ法) * * Copyright (C) 2015-2023 Makoto Hiroi */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #include <time.h> #define EMPTY 0 #define DEL 1 #define ANY 2 // ハッシュ表の要素 typedef struct { int kind; char *key; int value; } Item; // ハッシュ表 typedef struct { int size, count; Item *table; } HashTable1; // ハッシュ表の生成 HashTable1 *make_hash_table1(int size) { HashTable1 *ht = malloc(sizeof(HashTable1)); if (ht != NULL) { ht->table = malloc(sizeof(Item) * size); if (ht->table == NULL) { free(ht); return NULL; } ht->size = size; ht->count = 0; for (int i = 0; i < size; i++) { ht->table[i].kind = EMPTY; ht->table[i].key = NULL; } } return ht; } // ハッシュ表の廃棄 void delete_hash_table1(HashTable1 *ht) { free(ht->table); free(ht); } // ハッシュ関数 int hash_func2(HashTable1 *ht, const char *key, int *m) { unsigned int value = 0; for (; *key != '\0'; key++) value = (value << 3) + *key; *m = 8 - value % 7; return value % ht->size; } // キーの探索 int search_sub(HashTable1 *ht, const char *key) { int val; int n = hash_func2(ht, key, &val); int c = 0; Item *table = ht->table; while (c < ht->size) { if (table[n].kind == EMPTY) break; if (table[n].kind == ANY && strcmp(key, table[n].key) == 0) return n; n = (n + val) % ht->size; c++; } return -1; } // 探索 int search_hash1(HashTable1 *ht, const char *key, bool *err) { int n = search_sub(ht, key); if (n >= 0) { *err = true; return ht->table[n].value; } *err = false; return 0; } // 挿入 bool insert_hash1(HashTable1 *ht, const char *key, int val) { int n = search_sub(ht, key); if (n >= 0) { ht->table[n].value = val; return true; } int m, c = 0; Item *table = ht->table; n = hash_func2(ht, key, &m); while (c < ht->size) { if (table[n].kind != ANY) { table[n].key = malloc(strlen(key) + 1); if (table[n].key == NULL) break; strcpy(table[n].key, key); table[n].kind = ANY; table[n].value = val; ht->count++; return true; } n = (n + m) % ht->size; c++; } return false; } // 削除 bool delete_hash1(HashTable1 *ht, const char *key) { int n = search_sub(ht, key); if (n >= 0) { ht->table[n].kind = DEL; free(ht->table[n].key); ht->table[n].key = NULL; ht->count--; return true; } return false; } // ハッシュ表は空か bool is_empty_hash1(HashTable1 *ht) { return ht->count == 0; } // ハッシュ表の要素の個数 int length_hash1(HashTable1 *ht) { return ht->count; } // 巡回 void foreach_hash1(void (*func)(const char *, int), HashTable1 *ht) { Item *table = ht->table; for (int i = 0; i < ht->size; i++) { if (table[i].kind == ANY) func(table[i].key, table[i].value); } } // ハッシュ表をクリアする void clear_hash1(HashTable1 *ht) { ht->count = 0; for (int i = 0; i < ht->size; i++) { if (ht->table[i].kind == ANY) { free(ht->table[i].key); ht->table[i].key = NULL; } ht->table[i].kind = EMPTY; } } // 簡単なテスト void test1(void) { char buff[8][12]; HashTable1 *ht = make_hash_table1(13); bool err; printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- insert -----\n"); for (int i = 0; i < 8; i++) { sprintf(buff[i], "%d", rand()); printf("%s, %d\n", buff[i], insert_hash1(ht, buff[i], i)); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("------ search ------\n"); for (int i = 0; i < 8; i++) printf("%s, %d\n", buff[i], search_hash1(ht, buff[i], &err)); printf("------ delete ------\n"); for (int i = 0; i < 8; i++) { printf("%s %d\n", buff[i], delete_hash1(ht, buff[i])); int x = search_hash1(ht, buff[i], &err); printf("%s, %d, %d\n", buff[i], x, err); } printf("-- %d, %d --\n", is_empty_hash1(ht), length_hash1(ht)); printf("----- delete hash -----\n"); delete_hash_table1(ht); } void test2(int size) { HashTable1 *ht = make_hash_table1(100003); bool err; char buff[12]; while (length_hash1(ht) < size) { sprintf(buff, "%d", rand()); search_hash1(ht, buff, &err); if (!err) insert_hash1(ht, buff, 1); } delete_hash_table1(ht); } int main(void) { test1(); for (int i = 95000; i <= 100000; i += 250) { clock_t s = clock(); test2(i); printf("%d, %.3f\n", i, (double)(clock() - s)/CLOCKS_PER_SEC); } return 0; }