JavaScriptオブジェクトのプロパティアクセスにおけるビッグ・オー で書いたようにJavaScriptのオブジェクトは基本ハッシュテーブルであるし、Map オブジェクトも中の実装はChromeではハッシュテーブルであると書かれてある(Optimizing hash tables: hiding the hash code )。だからJavaScriptで自前のハッシュテーブルを作るというのは必要のないことなのだけど試しに作ってみたかった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const stringHash = require ("string-hash" );module .exports = class MyHashTable { constructor (numberOfBucket ) { this .numberOfBucket = parseInt (numberOfBucket, 10 ); this .hashTable = new Array (this .numberOfBucket ); } get (key ) { key = '' + key; const hashTableIndex = this .getHashTableIndex (key); const bucket = this .hashTable [hashTableIndex] || []; const bucketLength = bucket.length ; for (let i=0 ; i<bucketLength; i++) { const data = bucket[i]; if (data[0 ] === key) { return data[1 ]; } } } set (key, value ) { key = '' + key; const hashTableIndex = this .getHashTableIndex (key); if (!this .hashTable [hashTableIndex]) { this .hashTable [hashTableIndex] = []; } this .hashTable [hashTableIndex].push ([key, value]); } getHashTableIndex (key ){ const hash = stringHash (key); return hash % this .numberOfBucket ; } }
実装はHow to implement a simple hash table in JavaScript – freeCodeCamp やImplementing a hash table in JavaScript を参考にしている。
string-hash は、与えられた文字列を0から4294967295の数値にして返してくれる。
ハッシュテーブルのサイズは、ハッシュ関数の処理やコンフリクトしたときの対応方法によるらしいのだけど、とりあえずインスタンス作成時に設定するようにして、ハッシュテーブルに入れるデータ数に1.3をかけた値より大きな素数を使うようにした。素数についてはThe Prime Database: The Nth Prime Page を使って出した。
But a good general “rule of thumb” is:
The hash table should be an array with length about 1.3 times the maximum number of keys that will actually be in the table, and
Size of hash table array should be a prime number
http://cseweb.ucsd.edu/~kube/cls/100/Lectures/lec16/lec16-8.html#pgfId-975583
下記のような感じで簡単に測定してみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const dataLength = 10000 ;const numberOfBucket = 13003 ;const MyHashTable = require ('./my-hash-table' );const myHashTable = new MyHashTable (numberOfBucket);console .time ('set' );let key, value;for (let i=0 ; i<dataLength; i++) { key = Math .floor (Math .random ()*dataLength).toString (16 ); value = Math .floor (Math .random ()*dataLength).toString (16 ); myHashTable.set (key, value); } console .timeEnd ('set' );console .time ('get' );myHashTable.get (key); console .timeEnd ('get' );
結果が10000件の追加に19.117ms
で取得に0.067ms
。
データを以下のように変更して再度実行
1 2 const dataLength = 100000 ;const numberOfBucket = 130003 ;
結果が100000件の追加に112.608ms
で取得が0.061ms
。
データを1000000件にすると追加に1592.088ms
で取得に0.068ms
。データの件数が増えても、データの取得に時間がかかるようにはなっていない。いい感じに見える。
なおデータを1000000件でハッシュテーブルのサイズを1(検索にO(n)
かかるはず)にしたら、追加には870.672ms
となり取得には12.525ms
かかった(繰り返し測定してみると、早い時は2msくらいで終わる)。
上記の計測をオブジェクトに置き換えて
1 2 3 4 5 6 7 8 9 10 11 12 13 const dataLength = 1000000 ;const myHashTable = {};console .time ('set' );let key, value;for (let i=0 ; i<dataLength; i++) { key = Math .floor (Math .random ()*dataLength).toString (16 ); value = Math .floor (Math .random ()*dataLength).toString (16 ); myHashTable[key] = value; } console .timeEnd ('set' );console .time ('get' );myHashTable[key]; console .timeEnd ('get' );
としてみたろころ、データの追加には1632.026ms
で、取得には0.008ms
になった。繰り返し計測してみないと誤差の範囲かどうかは明確には言えないけど、ほぼ同じパフォーマンスのように思う。
さらにMapに置き換えてみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 const dataLength = 1000000 ;const myHashTable = new Map ();console .time ('set' );let key, value;for (let i=0 ; i<dataLength; i++) { key = Math .floor (Math .random ()*dataLength).toString (16 ); value = Math .floor (Math .random ()*dataLength).toString (16 ); myHashTable.set (key, value); } console .timeEnd ('set' );console .time ('get' );myHashTable.get (key); console .timeEnd ('get' );
データの追加には1178.558ms
で、取得には0.006ms
になった。
そのほか参考
以上。