ds-algo-study
Version:
Just experimenting with publishing a package
403 lines (371 loc) • 16.2 kB
JavaScript
// helper functions are static methods on the class
"use strict";
{
const MAX_PROBES = 256; // if chains or open addressing rehashes are longer than this, something is wrong
class HashTable {
constructor ( {
hashFunction: hashFunction = HashTable.basicHash,
pairsList: pairsList = null,
numSlots: numSlots = 256,
maxRatio: maxRatio = 0.75, // load factor ( keys/slots ) at which to grow and rehash
desiredCollisionProbability: desiredCollisionProbability = NaN,
ensurePowerOfTwo: ensurePowerOfTwo = false,
growthRate: growthRate = 2.0, // how much to multiple the number of slots by at rehashing
probeStrategy: probeStrategy = 'OPEN_ADDRESSING', // or 'CHAINING'
} = {} ) {
let numKeys = 0;
const spec = {
numKeys,
numSlots, maxRatio, desiredCollisionProbability, ensurePowerOfTwo,
probeStrategy, growthRate,
hashFunction
};
Object.assign( this, spec );
this.growAndRehashAllEntries( { pairsList, growthRate: 1.0 } );
console.log( "Created HashTable to spec", spec );
}
// rehash and grow
growAndRehashAllEntries ( {
pairsList: pairsList = this.allPairs(),
growthRate: growthRate = this.growthRate
} = {} ) {
this.numSlots = this.calculateNumSlots( { pairsList, growthRate } );
this.slots = new Array( this.numSlots );
this.numKeys = 0;
if ( Array.isArray( pairsList ) ) {
pairsList.forEach( ( [ key, value ] ) => this.insert( key, value ) );
}
}
// Key Equality
static orderedStringify ( obj ) {
const allKeys = [];
JSON.stringify( obj, ( k, v ) => ( allKeys.push( k ), v ) );
return JSON.stringify( obj, allKeys.sort() );
}
static keysEqual ( k1, k2 ) {
const sk1 = HashTable.anythingToString( k1 );
const sk2 = HashTable.anythingToString( k2 );
return sk1 == sk2;
}
// insert
set ( key, value ) {
return this.insert( key, value );
}
insert ( key, value ) {
let hash = this.hashValueToTableSize( this.hashFunction( key ) );
if ( this.probeStrategy == 'CHAINING' ) {
let probe = 0;
let link = this.slots[ hash ];
while ( !!link && link.occupied && !HashTable.keysEqual( link.key, key ) && !!link.next ) {
link = link.next;
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
}
if ( !link ) {
this.slots[ hash ] = { occupied: true, key, value, next: undefined };
this.numKeys += 1;
} else if ( !link.occupied ) {
link.key = key;
link.value = value;
link.occupied = true;
this.numKeys += 1;
} else if ( HashTable.keysEqual( link.key, key ) ) {
link.value = value;
} else if ( !link.next ) {
link.next = { occupied: true, key, value, next: undefined };
this.numKeys += 1;
}
} else {
let probe = 0;
while ( !!this.slots[ hash ] && !HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
hash = this.hashValueToTableSize( this.hashFunction( key, probe ) );
}
if ( !!this.slots[ hash ] && HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
this.slots[ hash ].value = value;
} else {
this.slots[ hash ] = { key, value };
this.numKeys += 1;
}
}
if ( this.numKeys / this.numSlots > this.maxRatio ) {
this.growAndRehashAllEntries();
}
}
// get
get ( key ) {
return this.retrieve( key );
}
retrieve ( key ) {
let hash = this.hashValueToTableSize( this.hashFunction( key ) );
if ( this.probeStrategy == 'CHAINING' ) {
let probe = 0;
let link = this.slots[ hash ];
while ( !( link.occupied || HashTable.keysEqual( link.key, key ) ) && !!link.next ) {
link = link.next;
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
}
if ( HashTable.keysEqual( link.key, key ) ) {
return link.value;
} else {
throw new TypeError( `Key not found ${ key }` );
}
} else {
let probe = 0;
while ( !!this.slots[ hash ] && !HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
hash = this.hashValueToTableSize( this.hashFunction( key, probe ) );
}
if ( !!this.slots[ hash ] && HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
return this.slots[ hash ].value;
} else {
throw new TypeError( `Key not found ${ key }` );
}
}
}
// has
has ( key ) {
return this.contains( key );
}
contains ( key ) {
let hash = this.hashValueToTableSize( this.hashFunction( key ) );
if ( this.probeStrategy == 'CHAINING' ) {
let probe = 0;
let link = this.slots[ hash ];
if ( !!link ) {
while ( !( link.occupied || HashTable.keysEqual( link.key, key ) ) && !!link.next ) {
link = link.next;
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
}
if ( HashTable.keysEqual( link.key, key ) && link.occupied ) {
return true;
}
}
return false;
} else {
let probe = 0;
while ( !!this.slots[ hash ] && !HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
hash = this.hashValueToTableSize( this.hashFunction( key, probe ) );
}
if ( !!this.slots[ hash ] && HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
return true;
}
return false;
}
}
// delete
delete ( key ) {
return this.remove( key );
}
remove ( key ) {
let hash = this.hashValueToTableSize( this.hashFunction( key ) );
if ( this.probeStrategy == 'CHAINING' ) {
let probe = 0;
let link = this.slots[ hash ];
if ( !!link ) {
while ( !( link.occupied || HashTable.keysEqual( link.key, key ) ) && !!link.next ) {
link = link.next;
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
}
if ( HashTable.keysEqual( link.key, key ) && link.occupied ) {
link.occupied = false;
this.numKeys -= 1;
return true;
}
}
return false;
} else {
let probe = 0;
while ( !!this.slots[ hash ] && !HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
hash = this.hashValueToTableSize( this.hashFunction( key, probe ) );
}
if ( !!this.slots[ hash ] && HashTable.keysEqual( key, this.slots[ hash ].key ) ) {
delete this.slots[ hash ];
this.numKeys -= 1;
return true;
}
return false;
}
}
// iterate table to get all pairs
allPairs () {
const pairs = [];
if ( this.probeStrategy == 'CHAINING' ) {
for ( let hash = 0; hash < this.slots.length; hash++ ) {
let probe = 0;
let link = this.slots[ hash ];
if ( !!link ) {
if ( link.occupied ) {
pairs.push( [ link.key, link.value ] );
}
while ( !!link.next ) {
link = link.next;
if ( link.occupied ) {
pairs.push( [ link.key, link.value ] );
}
probe += 1;
if ( probe > MAX_PROBES ) {
throw new TypeError( `${ this.probeStrategy } Max Probes Exceeded At ${ probe }` );
}
}
}
}
} else {
for ( let hash = 0; hash < this.slots.length; hash++ ) {
if ( !!this.slots[ hash ] ) {
const item = this.slots[ hash ];
pairs.push( [ item.key, item.value ] );
}
}
}
return pairs;
}
// Sizing calculations
// The following methods are used to calculate
// correspondence between slots and keys counts
// given their ratio or a desired collision probability
static numSlotsForRatio ( r, numKeys ) {
return numKeys / r;
}
static numKeysForRatio ( r, numSlots ) {
return numSlots * r;
}
// the following two formulas are from the
// birthday cooincidence probability problem
// generalized to collissions
// from https://en.wikipedia.org/wiki/Birthday_problem#Cast_as_a_collision_problem
static numSlotsForCollisionProbability ( p, numKeys ) {
return ( numKeys ** 2 ) / ( 2 * Math.log( 1 / 1 - p ) );
}
static numKeysForCollisionProbability ( p, numSlots ) {
return Math.ceil( Math.sqrt( Math.log( 1 / ( 1 - p ) ) * 2 * numSlots ) );
}
// This is interesting
// If we set desired collision probability
// Then the maxRatio permitted changes with the number of keys
// So we need to check
// On every insert
// The new max ratio
// And see if the actual table ratio exceeds it.
// And if it does, we need to grow the table.
static maxRatioForCollisionProbability ( p, numKeys ) {
return 2 * Math.log( 1 / ( 1 - p ) ) / numKeys;
}
static toThisOrNextPowerOfTwo ( num ) {
return 2 ** ( Math.ceil( Math.log2( num ) ) );
}
calculateNumSlots ( {
pairsList: pairsList = null,
growthRate: growthRate = this.growthRate
} = {} ) {
let minSlots;
if ( Array.isArray( pairsList ) ) {
const approxNumKeys = pairsList.length;
minSlots = HashTable.numSlotsForRatio( this.maxRatio, approxNumKeys );
}
minSlots = this.numSlots * growthRate;
if ( !Number.isNaN( this.desiredCollisionProbability ) && !Number.isNaN( this.numKeys ) ) {
minSlots = Math.max( minSlots, HashTable.numSlotsForCollisionProbability( this.desiredCollisionProbability, this.numKeys ) );
}
if ( this.ensurePowerOfTwo ) {
minSlots = HashTable.toThisOrNextPowerOfTwo( minSlots );
}
return minSlots;
}
// hash function
static basicHash ( key, seed = 11.37 /* why not? :) */ ) {
// This code is take from my tifuhash https://github.com/dosyago-coder-0/tifuhash
const keyString = HashTable.anythingToString( key );
let n = Array.from( keyString );
let m = seed + '';
if ( n.length == 0 ) {
// seed only
n = [ m ];
}
const s = parseFloat( n.length ? n.pop() : 0 );
m = Array.from( m ).concat( n );
const isFloat = m.every( x => !isNaN( parseFloat( x ) ) );
if ( isFloat ) {
m = m.map( x => parseFloat( x ) );
} else {
m = HashTable.stringToUTF8Bytes( m.join( '' ) );
}
let a = new Float64Array( 4 );
a[ 0 ] = 1;
a[ 2 ] = s ? Math.pow( s + 1 / s, 1 / 2 ) : 3;
a[ 3 ] = s ? Math.pow( s + 1 / s, 1 / 5 ) : 7;
m.forEach( ( x, i ) => {
a[ 1 ] = ( x + i + 1 ) / a[ 3 ];
a[ 2 ] += a[ 0 ] / a[ 1 ]; a[ 2 ] = 1 / a[ 2 ];
a[ 3 ] += x; a[ 3 ] = a[ 0 ] / a[ 3 ];
a[ 0 ] = a[ 1 ] + 1;
} );
a[ 2 ] *= Math.PI + a[ 3 ];
a[ 3 ] *= Math.E + a[ 2 ];
const h = new Uint32Array( a.buffer );
return ( h[ 4 ] ^ h[ 5 ] ^ h[ 6 ] ^ h[ 7 ] ) >>> 0;
}
// static helpers
static stringToUTF8Bytes ( s ) {
const utf8Str = unescape( encodeURIComponent( s ) );
return Array.from( utf8Str ).map( c => c.charCodeAt( 0 ) );
}
static anythingToString ( a ) {
const type = Object.prototype.toString.call( a );
let json = '[json:circular]';
try {
json = HashTable.orderedStringify( a );
} catch ( e ) { console.warn( e ) };
const str = a + '';
const numStr = a + 0 + '';
const numStrStrict = ( a * 1 ) + '';
const rep = `${ type }:${ json }:${ str }:${ numStr }:${ numStrStrict }`;
return rep;
}
hashValueToTableSize ( hval ) {
// could also add
// ( as in Java implementaion to ensure high bits have effect ):
// hval = hval ^ (hval >> 16);
return hval % this.numSlots;
}
}
Object.assign( self, { HashTable } );
test();
function test () {
const x = new HashTable();
x.insert( 'a', 1 );
x.insert( 'b', 2 );
x.insert( { c: 3 }, 3 );
Object.assign( self, { x } );
const y = new HashTable( { probeStrategy: 'CHAINING' } );
y.insert( 'a', 1 );
y.insert( 'b', 2 );
y.insert( { c: 3 }, 3 );
Object.assign( self, { y } );
}
}