11.11* Hash tables in standard libraries

In this section we discuss how hash tables are implemented in different standard libraries.

Java API

Before version 1.2, Java had exactly the problem above, but in version 1.2 they changed the implementation. The current version of Java HashMap (and HashSet and Hashtable and similar) has the following characteristics:

  • It’s a separate chaining hash table.
  • Small buckets (<8 elements) are linked lists, but larger buckets are red-black trees instead. This ensures O(n \log(n)) worst time complexity, but it’s linear if the data is well-behaved.
  • The size of the hash table is a power of two (n=2^k) – meaning that the resizing factor is 2.
  • The maximum load factor for resizing is 0.75.

If you’re interested you can read the comment on lines 125–232 in HashMap.java, where some implementation details are explained.

Python

Python uses much more modern implementations than Java, of both hash functions and hash tables. Python hash functions uses a combination of the following techniques:

  • Strings are hashed using SipHash (see PEP-456).
  • Tuples are hashed using “a slightly simplified version of the xxHash non-cryptographic hash” (see lines 384–397 in tupleobject.c).
  • In addition, Python adds randomisation: Whenever you start a new Python interpreter, it creates a random constant which is combined with the hashes. This means that every new instance will generate new hashes for the same strings, tuples, and other built-in objeccts.

Python dictionaries are implemented as hash tables. Since version 3.6 they are based on the following ideas (lecture video from PyCon 2017):

Here’s a summary of the internal implementation:

  • All hash tables have a power-of-two size (n=2^k) – meaning that the resizing factor is 2.
  • This means that the table index is the first (least significant) k bits of the hash value.
  • It’s an open adressing hash table.
  • But it doesn’t use linear probing, instead they probe with +p in every step, where p depends on the higher bits in the hash value.
  • It keeps the full hash value in the table (which improves the speed when resizing)
  • To get better memory efficiency, it is split into (1) a size 2^k integer array with indices, and (2) a compact array with tuples of the form (hash,key,value). The indices in array(1) point to locations in array(2).
  • This also has the effect that it can iterate over the insertion order of the elements.
  • Deletion is done using tombstones.
  • The maximum load factor for resizing is 0.66.
  • See the source code here: https://github.com/python/cpython/blob/main/Objects/dictobject.c