Card 06/ 10
Hash Map With Chaining in Python, Java and TypeScript
A minimal map with put, get and resize, then each language's built-in and its order.
A hash map with chaining needs three operations: put, get, and a resize that rehashes every key. This card covers a minimal one in Python, Java and TypeScript, run on the same seven keys, and then each language's built-in map and the order in which it hands its keys back.
Each listing keeps an array of buckets, each bucket a list of key and value pairs. put finds the key's bucket, replaces the value if the key is already there, and otherwise appends the pair and grows the table once the keys outnumber three quarters of the buckets. get walks one bucket. The resize builds an array twice the size and inserts every pair again. Python uses its built-in hash and Java uses hashCode. TypeScript has no hash function for strings, so hashString copies the formula of Java's String.hashCode. A hash code can be negative, and Java's Math.floorMod and TypeScript's second % capacity keep the bucket index from being negative too.
Each listing puts seven fruit prices, then puts the same pairs into the language's built-in map and prints its keys in the order it iterates them, meaning the order it visits them in a loop. The highlighted lines pick a bucket, check the load factor, rehash a pair and build the built-in.
A hash map with chaining, and each language's built-in
class ChainedHashMap:
def __init__(self, capacity: int = 8) -> None:
self.buckets: list[list[tuple[str, int]]] = [[] for _ in range(capacity)]
self.size = 0
def _bucket_for(self, key: str) -> list[tuple[str, int]]:
return self.buckets[hash(key) % len(self.buckets)]
def put(self, key: str, value: int) -> None:
bucket = self._bucket_for(key)
for index, (stored, _) in enumerate(bucket):
if stored == key:
bucket[index] = (key, value)
return
bucket.append((key, value))
self.size += 1
if self.size / len(self.buckets) > 0.75:
self._resize()
def get(self, key: str) -> int | None:
for stored, value in self._bucket_for(key):
if stored == key:
return value
return None
def _resize(self) -> None:
old = self.buckets
self.buckets = [[] for _ in range(len(old) * 2)]
for bucket in old:
for key, value in bucket:
self._bucket_for(key).append((key, value))
PRICES = [("pear", 4), ("fig", 7), ("apple", 1), ("kiwi", 3), ("plum", 5), ("lime", 2), ("date", 6)]
prices = ChainedHashMap()
for name, price in PRICES:
prices.put(name, price)
print(f"capacity after {len(PRICES)} puts: {len(prices.buckets)}")
print(f"get apple: {prices.get('apple')}")
print(f"get mango: {'not found' if prices.get('mango') is None else 'found'}")
prices.put("fig", 8)
print(f"after put fig 8: get fig {prices.get('fig')}, size {prices.size}")
built_in = dict(PRICES)
print(f"dict order: {' '.join(built_in)}")capacity after 7 puts: 16
get apple: 1
get mango: not found
after put fig 8: get fig 8, size 7
dict order: pear fig apple kiwi plum lime dateimport java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class HashMapCode {
record Entry(String key, int value) {}
static class ChainedHashMap {
private List<List<Entry>> buckets = emptyBuckets(8);
private int size = 0;
private static List<List<Entry>> emptyBuckets(int capacity) {
List<List<Entry>> buckets = new ArrayList<>();
for (int i = 0; i < capacity; i++) buckets.add(new ArrayList<>());
return buckets;
}
private List<Entry> bucketFor(String key) {
return buckets.get(Math.floorMod(key.hashCode(), buckets.size()));
}
void put(String key, int value) {
List<Entry> bucket = bucketFor(key);
for (int i = 0; i < bucket.size(); i++) {
if (bucket.get(i).key().equals(key)) {
bucket.set(i, new Entry(key, value));
return;
}
}
bucket.add(new Entry(key, value));
size++;
if ((double) size / buckets.size() > 0.75) resize();
}
Integer get(String key) {
for (Entry entry : bucketFor(key)) {
if (entry.key().equals(key)) return entry.value();
}
return null;
}
private void resize() {
List<List<Entry>> old = buckets;
buckets = emptyBuckets(old.size() * 2);
for (List<Entry> bucket : old) {
for (Entry entry : bucket) bucketFor(entry.key()).add(entry);
}
}
int capacity() { return buckets.size(); }
int size() { return size; }
}
public static void main(String[] args) {
String[] names = {"pear", "fig", "apple", "kiwi", "plum", "lime", "date"};
int[] amounts = {4, 7, 1, 3, 5, 2, 6};
ChainedHashMap prices = new ChainedHashMap();
for (int i = 0; i < names.length; i++) prices.put(names[i], amounts[i]);
System.out.println("capacity after " + names.length + " puts: " + prices.capacity());
System.out.println("get apple: " + prices.get("apple"));
System.out.println("get mango: " + (prices.get("mango") == null ? "not found" : "found"));
prices.put("fig", 8);
System.out.println("after put fig 8: get fig " + prices.get("fig") + ", size " + prices.size());
Map<String, Integer> hashed = new HashMap<>();
Map<String, Integer> linked = new LinkedHashMap<>();
for (int i = 0; i < names.length; i++) {
hashed.put(names[i], amounts[i]);
linked.put(names[i], amounts[i]);
}
System.out.println("HashMap order: " + String.join(" ", hashed.keySet()));
System.out.println("LinkedHashMap order: " + String.join(" ", linked.keySet()));
}
}capacity after 7 puts: 16
get apple: 1
get mango: not found
after put fig 8: get fig 8, size 7
HashMap order: plum date apple pear kiwi fig lime
LinkedHashMap order: pear fig apple kiwi plum lime datetype Entry<V> = { key: string; value: V };
function hashString(key: string): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (Math.imul(31, hash) + key.charCodeAt(i)) | 0;
}
return hash;
}
function emptyBuckets<V>(capacity: number): Entry<V>[][] {
return Array.from({ length: capacity }, () => []);
}
class ChainedHashMap<V> {
private buckets: Entry<V>[][] = emptyBuckets<V>(8);
private count = 0;
private bucketFor(key: string): Entry<V>[] {
const capacity = this.buckets.length;
return this.buckets[((hashString(key) % capacity) + capacity) % capacity];
}
put(key: string, value: V): void {
const bucket = this.bucketFor(key);
const stored = bucket.find((entry) => entry.key === key);
if (stored) {
stored.value = value;
return;
}
bucket.push({ key, value });
this.count++;
if (this.count / this.buckets.length > 0.75) this.resize();
}
get(key: string): V | undefined {
return this.bucketFor(key).find((entry) => entry.key === key)?.value;
}
private resize(): void {
const old = this.buckets;
this.buckets = emptyBuckets<V>(old.length * 2);
for (const bucket of old) {
for (const entry of bucket) this.bucketFor(entry.key).push(entry);
}
}
get capacity(): number {
return this.buckets.length;
}
get size(): number {
return this.count;
}
}
const PRICES: [string, number][] = [
["pear", 4], ["fig", 7], ["apple", 1], ["kiwi", 3], ["plum", 5], ["lime", 2], ["date", 6],
];
const prices = new ChainedHashMap<number>();
for (const [name, price] of PRICES) prices.put(name, price);
console.log(`capacity after ${PRICES.length} puts: ${prices.capacity}`);
console.log(`get apple: ${prices.get("apple")}`);
console.log(`get mango: ${prices.get("mango") === undefined ? "not found" : "found"}`);
prices.put("fig", 8);
console.log(`after put fig 8: get fig ${prices.get("fig")}, size ${prices.size}`);
const builtIn = new Map(PRICES);
console.log(`Map order: ${[...builtIn.keys()].join(" ")}`);capacity after 7 puts: 16
get apple: 1
get mango: not found
after put fig 8: get fig 8, size 7
Map order: pear fig apple kiwi plum lime dateThe first four lines match in every language: the table grew from 8 to 16 buckets at the seventh put, and a second put for fig replaced its price without adding a key.
The built-ins differ in order. Python's dict has kept keys in the order they were first inserted since Python 3.7, and a TypeScript Map always has, so both print pear first. Java's HashMap promises no order, and printed plum first, following its buckets. LinkedHashMap keeps its entries in a list beside its buckets, and printed pear first. A program that needs its keys back in the order they arrived should use a map that promises that order, not rely on whatever a HashMap happens to print.
← → move through sections and cards