高速にデータを挿入、検索、削除、更新を行う手段に使われるのがハッシュ法である。 大抵のプログラムには備わっているので、通常はそれを使えばよい。
地図システムのように、データはシンプルであるがデータ量が膨大な場合には 汎用的なクラスライブラリでは対応できないことがある。
ハッシュ法ではハッシュ値のぶつかりを避ける方法として、チェインを作る方法と オープンアドレス法が知られている[1]。
前者の方がやや高度で、削除ではチェインの繋ぎ変えも必要となり、管理用のメモリもいる。
後者もなるべくぶつかりを避けようとすると、プログラムが複雑になる。
しかし、ぶつかりを覚悟すれば実装はシンプルである。 シンプルであるがゆえに、少々ぶつかりがあっても、パフォーマンスがよいことを経験している。
ハッシュ値としては、空きと削除の二つ分の数値は使えない。
自分がよく使うハッシュ関数を下に示す。 実際には、ハッシュサイズ(HASHSIZE) はもっと大きな値を使うが、 ここでは説明のために小さな値としている。
下のプログラムでは、"abs", "atof" など12個の関数名について、 ハッシュ値(ハッシュ関数で得られた値)を求めている。
#include <stdio.h> #define HASHSIZE 19 // 素数 ... , 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61 char *func[] = { "abs", "atof", "atoi", "atol", "close", "feof", "fgetc", "fgets", "fopen", "fputc", "fputs", "free" }; int hash(char *key) { int n, h = 0; for (n = 0; key[n] != '\0'; n++) { h = (h * 137 + key[n]) % HASHSIZE; } return h; } int main() { int n; printf("HASHSIZE = %d\n", HASHSIZE); for (n = 0; n < sizeof(func)/sizeof(char*); n++) { printf("%-6s", func[n]); } printf("\n"); for (n = 0; n < sizeof(func)/sizeof(char*); n++) { printf(" %2d ", hash(func[n])); } }
実行結果を下に示す。
HASHSIZE = 19 abs atof atoi atol close feof fgetc fgets fopen fputc fputs free 7 3 6 9 13 7 18 15 11 14 11 3
ハッシュ法による文字列管理では、ハッシュサイズに等しい要素数を持つ配列が用いられる。 例えば "abs" のハッシュ値は7であるから、下図に示すように、 第7番目(先頭が第0番目)の配列要素にそのアドレスが格納される。 "atof"、"atoi" と順に割り当てていく。 "feof" の番になると、第7番目の要素は既に割り当て済みなので、配列の添え字を進めて、 未割り当て要素を探す。 この例では、直ぐ隣が空きであったため、"feof"には第8番目の要素が割り当てられる。 同様に、"fputs"、"free" も本来のハッシュ値とは違う場所が割り当てられる。
ある文字列例えば "feof" が登録されているかどうかを調べるには、 "feof"のハッシュ値が 7 であるから、もし、第7要素が空きであれば、登録されていないことになる。 何らかの文字列が登録されていたら、この文字列と "feof" を比較する。 もし、一致すれば登録されていたことになる。一致しなかった場合、配列の添え字を先に進めて、 ここが空きなら、"feof" は登録されていないことが確定する。 何らかの文字列が登録されていた場合は前と同じ処理を繰り返す。 なお、添え字が配列の末尾まで達したら、 配列の先頭に戻って同じことを行う。
衝突により、本来の場所と異なる位置に登録されることがあるため、 文字列の比較は2回以上になるケースもあるが、衝突がなければ、文字列の比較は精々1回で済む。
以上のように、ハッシュ法は登録処理も検索処理も非常に高速に行われるため、 広く使われる技術である。 ハッシュ表に格納するのは、文字列とは限らず、数値とか構造体データなど、どのようなデータでもよい。
なるべく衝突を減らすためには、ハッシュ表のサイズは、 登録する文字列の個数に比べて、十分に余裕を持たせることが必要である。 また、HASHSIZEはきりのいい数字にせず、素数にした方がよい。 このハッシュ関数では 137 という中途半端な数値が使われているが、 これもなるべく衝突が起こらないようにするためである。
以上述べたことをプログラム化したものを下に示す。
#include <stdio.h> #define HASHSIZE 19 char *hashTable[HASHSIZE]; // 素数 ... , 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61 char *func[] = { // 登録する単語 "abs", "atof", "atoi", "atol", "close", "feof", "fgetc", "fgets", "fopen", "fputc", "fputs", "free" }; char *check[] = { // チェックする単語 "strcmp", "atof", "printf", "free" }; int hash(char *key) { int n, h = 0; for (n = 0; key[n] != '\0'; n++) { h = (h * 137 + key[n]) % HASHSIZE; } return h; } int put(char *str) { int n, h = hash(str); for (n = 0; n < HASHSIZE; n++) { int ix = (h + n) % HASHSIZE; if (hashTable[ix] == NULL) { hashTable[ix] = str; return ix; // 新規登録 } else if (strcmp(hashTable[ix], str) == 0) { return ix; // 登録済み } } printf("Error: Hash Table Full"); return -1; } int contain(char *str) { int n, h = hash(str); for (n = 0; n < HASHSIZE; n++) { int ix = (h + n) % HASHSIZE; if (hashTable[ix] == NULL) { return -1; // 登録なし } else if (strcmp(hashTable[ix], str) == 0) { return ix; // 登録済み } } return -1; } int main() { int n; printf("HASHSIZE = %d\n", HASHSIZE); for (n = 0; n < sizeof(func)/sizeof(char*); n++) { put(func[n]); } for (n = 0; n < sizeof(check)/sizeof(char*); n++) { printf("\"%s\"は登録されて%s\n", check[n], contain(check[n]) >= 0 ? "います" : "いません"); } }
実行結果を以下に示す。
HASHSIZE = 19 "strcmp"は登録されていません "atof"は登録されています "printf"は登録されていません "free"は登録されています
文字列に日本語を含む場合には、
地図システムではノード(折れ線データの頂点)数は膨大である。 7,000万程度のノードの id(8バイト) と整数化した経度(4バイト)と緯度(4バイト)の管理を HashMap で行うにはノートパソコンでは主メモリが足りない。そこで、次のようにした。
簡単なオープンアドレス方式とした。メモリ使用量削減から、ノード情報は class 定義はせず、 long[] nodes(ノードid)、int[] lons、int[] lats の三つの配列で管理する。
パフォーマンス上は全く問題がなかった。MAXSIZE は 100000000 でもいいであろうが、 メモリに余裕があるため、130000000 とした[2023.1.28]。
ハッシュ表に当たるのが long[] nodes である。int[] lons、lats には無駄があるが、 class を導入するよりはメモリ使用量は少なくて済む。
static int MAXSIZE = 130000000; static long[] nodes = new long[MAXSIZE]; // node id static int[] lons = new int[MAXSIZE]; static int[] lats = new int[MAXSIZE]; static int hash(long id) { return (int)(Math.abs(id * 137) % MAXSIZE); } static int put(long id, int lon, int lat) { int n, h = hash(id); for (n = 0; n < MAXSIZE; n++) { int ix = (h + n) % MAXSIZE; if (nodes[ix] == 0) { nodes[ix] = id; lons[ix] = lon; lats[ix] = lat; num_nodes++; if (num_nodes % 1000000 == 0) { System.out.printf("%d万\r", num_nodes / 10000); } return ix; // 新規登録 } else if (nodes[ix] == id) { return ix; // 登録済み } } System.out.println("Error: Hash Table Full"); return -1; } static int get(long id) { int n, h = hash(id); for (n = 0; n < MAXSIZE; n++) { int ix = (h + n) % MAXSIZE; if (nodes[ix] == 0) { return -1; // 登録なし } else if (nodes[ix] == id) { return ix; // 登録済み } } return -1; // 存在しない }