Implementations

C++

#include <concepts>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
 
template <typename T>
concept HashableKey = requires(T key) {
	{ std::hash<T>{}(key) } -> std::same_as<std::size_t>;
	requires std::is_default_constructible_v<T>;
};
 
template <typename T>
concept HashableValue = std::is_default_constructible_v<T>;
 
template <HashableKey K, HashableValue V> class HashMap {
  private:
	using sz_t = std::size_t; // just shorter to write
 
	enum class State { occupied, empty, deleted };
 
	// a vector creates elems by value init so you'll have these as 0 aka
	// default vals value init is Slot some{}
	struct Slot {
		K key;
		V value;
		// the default would've been first one so occ
		State state = State::empty;
	};
 
	std::vector<Slot> table_;
	sz_t size_ = 0;
	sz_t deleted_ = 0;
 
	static constexpr double max_load_factor = 0.7;
 
	sz_t hash_key(const K &key) const {
		// can prob store it but this is fine
		return std::hash<K>{}(key) % table_.size();
	}
 
	sz_t probe_next(sz_t index) const { return (index + 1) % table_.size(); }
 
	// keep on probing till you hit what we are looking for
	sz_t find_slot_get(const K &key) const {
 
		for (sz_t index = hash_key(key); table_[index].state != State::empty;
		     index = probe_next(index)) {
			const auto &slot = table_[index];
			if (slot.state == State::occupied && slot.key == key) {
				return index;
			}
		}
 
		// INFO: instead of doing -1 ( which'll underflow ) do a SIZE_MAX
		// though technically you can use -1 as proxy for a and even comparisons
		// would work as literals would be implicitly cast to size_T
		return SIZE_MAX;
	}
	// keep on probing till you get an empty one but also rem the
	// first deleted one ( probe till empty to find if inserted ahead in seq )
	sz_t find_slot_insert(const K &key) const {
		sz_t deleted_idx = SIZE_MAX;
		sz_t index = hash_key(key);
		for (; table_[index].state != State::empty; index = probe_next(index)) {
			const auto &slot = table_[index];
 
			// exists already
			if (slot.state == State::occupied && slot.key == key) {
				return index; // allows for a[] get or put semantics
			}
			if (slot.state == State::deleted && deleted_idx == SIZE_MAX) {
				deleted_idx = index;
			}
		}
 
		return deleted_idx == SIZE_MAX ? index : deleted_idx;
	}
	void maybe_rehash() {
		// check for load factor and rehash if needed
		// note that load CAN be more than threshold - the rehash is eventual
		// on the next element
		auto load = static_cast<double>(size_ + deleted_) / table_.size();
		if (load > max_load_factor)
			rehash();
	}
 
	void rehash() {
		// grow in size and recreate it all
		sz_t new_size = table_.size() * 2 + 1;
		std::vector<Slot> new_table(new_size);
 
		// impl trick so I can use my operators[]
		new_table.swap(table_);
 
		// for (auto &entry : table_ | std::views::filter([](auto x) {
		// 	                   return x.state == State::occupied;
		// I wrote this ^ first but it's so verbose compared to what's below
 
		size_ = deleted_ = 0; // they'll be recomputed
		for (auto &entry : new_table) {
			if (entry.state != State::occupied)
				continue;
 
			operator[](entry.key) = entry.value;
		}
	}
 
  public:
	HashMap() : table_(32) {} // so that it doesn't grow on the first insert
 
	sz_t size() const { return size_; }
 
	V &operator[](const K &key) {
		maybe_rehash();
		// NOTE: rem to do this FIRST as index will be invalidated
		// if you do it later
 
		auto index = find_slot_insert(key);
		auto &slot = table_[index];
 
		if (slot.state == State::deleted) {
			deleted_--;
		} else if (slot.state == State::empty) {
			size_++;
		}
		slot.state = State::occupied;
		slot.key = key;
		// value retain whatever it was - this THEN needs me to make sure
		// I default init vals when I delete stuff
 
		return slot.value;
	}
 
	bool contains(const K &key) const {
		auto index = find_slot_get(key);
		return index != SIZE_MAX;
	}
 
	// a get that does not have insert semantics
	const V *find(const K &key) const {
		auto index = find_slot_get(key);
		return index == SIZE_MAX ? nullptr : &(table_[index].value);
	}
 
	// NOTE: a trick to just write the const one and then strip the const
	V *find(const K &key) {
		return const_cast<V *>(std::as_const(*this).find(key));
	}
 
	bool erase(const K &key) {
		auto index = find_slot_get(key);
		if (index == SIZE_MAX)
			return false; // not in table
 
		// NOTE: rem to default contruct the value for a later use in []
		auto &slot = table_[index];
		slot.state = State::deleted;
		slot.value = V{};
		deleted_++;
		size_--; // NOTE: rem the size change too
 
		return true;
	}
};
 
int main() {
	static_assert(HashableKey<int>); // for lsp
 
	HashMap<std::string, int> len_map;
 
	len_map["abc"] = 3;
	(void)len_map.size();
	(void)len_map.contains("abc");
	(void)len_map.find("abc");
 
	auto *a = len_map.find("abc");
	*a = 5;
	std::printf("len is %d\n", len_map["abc"]);
 
	(void)len_map.erase("abc");
}

Java

import java.util.ArrayList;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Iterator;
import java.util.List;
 
// WARN:
// ideally this impl should've used nulls to represent empty
// this is more along the lines of my cpp impl as I wrote that first
// not using records and using nulls would be a ligher on memory impl
// not necessarily "cleaner"
 
// WARN: also remember to do .equals instead of ==
 
// a type param cannot be a pair, which makes sense, I mean even in cpp
// it would've been a pair wrapper
record Entry<K, V>(K key, V value) {
}
 
// K is an Object atleast so has an equals defined which may not be good
// but it's there. This doesn't need to be a type constraint just documented
// that hey - your equals need to be good
final class HashMap<K, V> implements Iterable<Entry<K, V>> {
    private static enum State {
        empty,
        occupied,
        deleted
    }
 
    // NOTE: that I don't need to ( but should ) use .key() instead of .key
    // this works because record is priv inside of hashmap so hashmap can
    // access privates of record
 
    // WARN: it's better if this wasn't a record ( to not churn heap )
    // but leaving the impl as it is for now
    private static record Slot<K, V>(K key, V value, State state) {
        // NOTE: you don't NEED the default constructible requirement since
        // values can just be null
 
        // NOTE: static factory using generics, could also have overloaded the ctor
        static <K, V> Slot<K, V> empty() {
            return new Slot<>(null, null, State.empty);
        }
 
        // record will have the full 3 arg ctor implicit
    }
 
    // not specific reason to use ArrayList, I don't really use the resizable
    // properties of it ( we assign a new one when we grow )
    // but easier to use with generics
    private List<Slot<K, V>> table = new ArrayList<>(Collections.nCopies(32, Slot.<K, V>empty()));
    private int size = 0;
    private int deleted = 0;
    private final double loadFactorThreshold = 0.7;
 
    private int hash(K key) {
        var code = key.hashCode() % table.size();
        // WARN: hashcode can be negative
        if (code < 0)
            code += table.size();
        return code;
    }
 
    private int probeNext(int index) {
        return (index + 1) % table.size();
    }
 
    private int indexForPut(final K key) {
        var index = hash(key);
        int deletedIndex = -1;
 
        while (table.get(index).state != State.empty) {
            var slot = table.get(index);
            if (slot.state == State.occupied && slot.key.equals(key)) {
                // NOTE: i often do a -1 here initially, my bias
                return index; // already present
            }
            if (slot.state == State.deleted && deletedIndex == -1) {
                deletedIndex = index;
            }
            index = probeNext(index);
        }
 
        return deletedIndex == -1 ? index : deletedIndex;
    }
 
    private int indexForGet(final K key) {
        var index = hash(key);
 
        while (table.get(index).state != State.empty) {
            var slot = table.get(index);
            if (slot.state == State.occupied && slot.key.equals(key)) {
                return index;
            }
 
            index = probeNext(index);
 
        }
 
        return -1; // not present
    }
 
    private void maybeRehash() {
        double load = (double) (size + deleted) / table.size();
        if (load > loadFactorThreshold) {
            rehash();
        }
    }
 
    private void rehash() {
        var oldTable = table;
        table = new ArrayList<>(Collections.nCopies(2 * oldTable.size() + 1, Slot.<K, V>empty()));
 
        size = deleted = 0;
        for (var slot : oldTable) {
            // WARN: again this'll be a LOT of churn via the puts
            if (slot.state == State.occupied) {
                put(slot.key, slot.value);
            }
        }
    }
 
    public V put(K key, V value) {
        // java semantics to return the old value
        maybeRehash();
 
        var index = indexForPut(key);
        var slot = table.get(index);
        V ret = null;
 
        if (slot.state == State.deleted) {
            deleted--;
            size++;
        } else if (slot.state == State.occupied)
            ret = slot.value;
        else {
            size++;
        }
 
        table.set(index, new Slot<>(key, value, State.occupied));
        return ret;
    }
 
    public V get(final K key) {
        var index = indexForGet(key);
        if (index == -1)
            throw new NoSuchElementException();
        return table.get(index).value;
    }
 
    public boolean containsKey(final K key) {
        return indexForGet(key) != -1;
    }
 
    public V erase(final K key) {
        var index = indexForGet(key);
        if (index == -1)
            throw new NoSuchElementException();
 
        var slot = table.get(index);
        table.set(index, new Slot<>(null, null, State.deleted));
        size--;
        deleted++;
        return slot.value;
    }
 
    @Override
    public Iterator<Entry<K, V>> iterator() {
        // I think I could've also used an array list iterator here
        // but needlessly overcomplicating
        return new Iterator<>() {
            private int index = 0;
            private int seen = 0;
 
            @Override
            public boolean hasNext() {
                return seen < size;
            }
 
            @Override
            public Entry<K, V> next() {
                if (!hasNext())
                    throw new NoSuchElementException();
 
                while (table.get(index).state != State.occupied) {
                    index++;
                }
 
                seen++;
                var slot = table.get(index);
                index++; // NOTE: rem this
                return new Entry<>(slot.key, slot.value);
            }
        };
    }
}

Notes

Core

There’s two

  • separate chaining where you have a vector of vector and iterate on collision
  • open addressing is similar except you loop over probe positions on collision

In both cases you resize on a load factor threshold and must rehash the full table again the hash to index mapping would need to change as size has changed.

For open addressing

  • linear
  • quad
  • double hash based probings

minor note: in sep chaining, once you hit the bucket, iter order does not matter while in case of sep chaining it must be same as the insert order though as impls go - usually via an array in sep chaining, the order is the same this is just a LOGICAL correctness related difference.

a “Slot” can be occupied, deleted or empty. Empty means probinh can stop, deleted means you can insert here but lookup still needs to keep on probing.

cpp

concepts

a concept is a compile time constraint on template on templated args so say template<Typename T extends Comparable just like java does it

// previous would've been a static_assert
template<typename T>
concept Integral = std::is_integral_v<T>;
 
// can then use it like
template<Integral T>
class Box{};
 
// or joining multiple concepts
template<typename T>
requires Integral<T> && SomeOther<T> // can think of it like an anon concept
class Box{};
 
// or making a new concept that's sum of multiple concepts
template<typename T>
concept HashKey = Hashable<T> && EqualityComparable<T>;
 
// and then just do
template<HashKey K>
class HashMap{};
 
// NOTE: requires is the JUICE
template<typename T>
// can define any no of vars, it just says say THESE exist
concept Example = requires(T a,T b,int c) {
    // then THESE must hold
    // valid or { valid } -> then result is
    a.size();
    typename T::some_type_in_scope;
    // the first type is supplied as decltype(a+b) implicity
    { a == b } -> std::convertible_to<bool>;
    { a + b } -> std::same_as<T>;
 
    // if you want to check some type bool to be true
    // use requires again
    requires std::is_default_constructible_v<T>;
};
 
// you only need requires when you need a body
concept Example = std::is_default_contructible<T> && ...;

NOTE: this syntax is getting very very confusing

static convs vs constexpr

Note that a const simply can be created at runtime or compile time. A static const a constexpr is basically same I believe.

https://stackoverflow.com/questions/41125651/constexpr-vs-static-const-which-one-to-prefer

Prefer just a constexpr. You do need a static if you want it a shared class member. Note that constexpr is const ( more like a superset of const ) and is inline too always.

Enum class

these are modern enums in cpp

enum class State{
    A,
    B
};
 
State s = State::A;
 
as opposed to leaking A and B in scope.
Also these are not interchangeable with ints now ( must cast if you want )

Lambda what can I omit?

auto min = []{}; is the smallest lambda

Also you can’t do auto x = cond ? lambda 1 : lambda 2 as each lambda has a uniq compiler generated type so auto can’t merge those. You would need an std::func todo the type erasure.:w

making const and non const variations

you can just make the const one and have const stripped away in non const ones

V *find(const K &key) {
    return const_cast<V *>(std::as_const(*this).find(key));
}

as_const would cast it to a const type ( just cast no ctor call ) so this’ll resolve the find and later we use const_cast to remove the const.


Java

Lang

  • no default values on args via equals, replacement is overloading
  • java has no way to do a = default ctor when a default ctro is omitted. you must write it in full
  • making a function cosnt is just not possible
  • chained assignments are possible in java
  • new ArrayList<>(10) gives capacity as 10 not size
  • hashCode() can be negative

Records

records are immutable

Compare

I always forget to do .equals and .compareTo and instead use operators. remember this bias.