{"id":"dsa-hash-buckets","slug":"buckets","title":"Buckets and chaining","text":"You have used dicts and sets since early in the course, and the module on complexity relied on them: `x in seen` takes O(1) on average, however big the set is. In this module you build that data structure yourself, a **hash table**, so you know why it's fast, what it costs, and when it isn't fast at all.\n\n### Buckets\n\nA hash table keeps its items in a list of **buckets**. To decide which bucket a key goes in, a **hash function** turns the key into a whole number, and the remainder after dividing by the number of buckets picks one: `hash(key) % len(buckets)`. To find the key later, you work out the same bucket and look only there, never in the rest of the table.\n\nFor a whole number such as 12, Python's `hash` gives the number itself, so with 5 buckets the key 12 goes in bucket 12 % 5, which is 2:\n\n```python\nbuckets = [[] for i in range(5)]\nfor key in [12, 7, 30, 21, 5, 18, 44]:\n    buckets[key % 5].append(key)\n\nfor i in range(len(buckets)):\n    print(i, buckets[i])\n```\n\n```output\n0 [30, 5]\n1 [21]\n2 [12, 7]\n3 [18]\n4 [44]\n```\n\nTwo keys in one bucket, like 12 and 7, is a **collision**. Collisions can't be avoided, since there are far more possible keys than buckets, so each bucket here is a list that holds every key that lands in it. That's called **separate chaining**, and each bucket's list is a **chain**. Python's `%` never gives a negative remainder when the number of buckets is positive, so a negative key gets a real bucket too: `-7 % 5` is 3.\n\nEach bucket must be a list of its own. `[[]] * 5` looks the same, but it's five references to one and the same list, so a key added to one bucket shows up in all of them:\n\n```python\nbuckets = [[]] * 5\nbuckets[12 % 5].append(12)\nprint(buckets)\n```\n\n```output\n[[12], [12], [12], [12], [12]]\n```\n\n### Hashing strings\n\n`hash` works on strings too, but the number it gives for a string changes every time Python starts: Python mixes a random value into it, so that nobody can pick keys that all collide on purpose and slow a web server down. To see where words go, write a hash function of your own. This one starts from 0 and, for each character, multiplies the number so far by 37 and adds the character's code, `ord(char)`. The last column is the bucket in a table of 13:\n\n```python\ndef word_hash(word):\n    h = 0\n    for char in word:\n        h = h * 37 + ord(char)\n    return h\n\n\nfor fruit in [\"mango\", \"lemon\", \"melon\", \"grape\", \"peach\", \"apple\"]:\n    print(fruit, word_hash(fruit), word_hash(fruit) % 13)\n```\n\n```output\nmango 209351402 0\nlemon 207678779 9\nmelon 209551571 8\ngrape 198950063 0\npeach 215158545 4\napple 187624178 1\n```\n\nMango and grape collide in bucket 0. A hash function may follow any rule, with one condition: **a key gets the same number every time it's hashed**. Otherwise a lookup would look in another bucket than the one the key went into. Past that, the more evenly a function scatters keys over the buckets, the shorter the chains. Multiplying by 37 before each addition makes the position of a character matter, so \"lemon\" and \"melon\", which have the same letters, land in different buckets.\n\n### Keys and values\n\nA dict keeps a value with each key, so the buckets of a map hold **pairs**. Each pair here is a small list, `[key, value]`, so that its value can be changed:\n\n```python\nclass HashMap:\n    def __init__(self, size):\n        self.buckets = [[] for i in range(size)]\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def put(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n\n    def get(self, key):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                return pair[1]\n        return None\n\n\nrooms = HashMap(4)\nrooms.put(101, \"Ada\")\nrooms.put(205, \"Ben\")\nrooms.put(101, \"Cleo\")\nprint(rooms.get(101), rooms.get(205), rooms.get(310))\nprint(rooms.buckets)\n```\n\n```output\nCleo Ben None\n[[], [[101, 'Cleo'], [205, 'Ben']], [], []]\n```\n\n`put` searches the key's bucket first: a key that's already there gets its new value, and only a new key is appended. Without that search, the bucket would hold two pairs for room 101, and `get` would find the old one. The keys are compared with `==` because a bucket can hold other keys too: 101 and 205 share bucket 1.\n\n### What it costs\n\nWorking out the bucket is O(1). After that, `get` and `put` go through one chain. With n pairs in m buckets, a chain holds n / m pairs on average, so as long as there are about as many buckets as pairs, each operation takes **O(1) on average**. The worst case is O(n): if every key lands in the same bucket, the table is one long list. With 8 buckets, the keys 0, 8, 16, 24 and so on all go to bucket 0. Python's dict and set have the same worst case, and their hash functions make it rare. The memory is O(n + m): a pair for each key and a list for each bucket.\n","task":"Fill in the blanks: the bucket of a key, and what `put` does when the key is already in its bucket. The program should print:\n\n```\nIvy Max None\n[[], [[17, 'Ivy'], [21, 'Max']], [], []]\n```\n","kind":"fill","seed":"class HashMap:\n    def __init__(self, size):\n        self.buckets = [[] for i in range(size)]\n\n    def _index(self, key):\n        return [[hash(key) % len(self.buckets)‖key % len(self.buckets)]]\n\n    def put(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                [[pair[1] = value]]\n                return\n        bucket.append([key, value])\n\n    def get(self, key):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                return pair[1]\n        return None\n\n\nages = HashMap(4)\nages.put(17, \"Eli\")\nages.put(21, \"Max\")\nages.put(17, \"Ivy\")\nprint(ages.get(17), ages.get(21), ages.get(8))\nprint(ages.buckets)\n","solution":"class HashMap:\n    def __init__(self, size):\n        self.buckets = [[] for i in range(size)]\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def put(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n\n    def get(self, key):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                return pair[1]\n        return None\n\n\nages = HashMap(4)\nages.put(17, \"Eli\")\nages.put(21, \"Max\")\nages.put(17, \"Ivy\")\nprint(ages.get(17), ages.get(21), ages.get(8))\nprint(ages.buckets)\n","hints":["The bucket is the key's hash divided by the number of buckets, keeping only the remainder.","`_index` returns `hash(key) % len(self.buckets)`. In `put`, the pair whose key matches gets the new value in its second place: `pair[1] = value`."],"tests":[{"name":"Output","stdin":"","expect":"Ivy Max None\n[[], [[17, 'Ivy'], [21, 'Max']], [], []]","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write a class `HashSet` that keeps keys in buckets, without Python's `set` or `dict`:\n\n- `HashSet(bucket_count)` makes a set with `bucket_count` empty buckets.\n- `add(key)` adds the key and returns `True`, or returns `False` when the key is already in the set.\n- `contains(key)` returns whether the key is in the set.\n- `size()` returns the number of keys.\n\nWork out a key's bucket with `hash(key) % len(self.buckets)` and look only in that bucket. The hidden test adds 100,000 keys to a set of 100,000 buckets and then looks up 100,000 keys: a set that keeps all its keys in one list would search that whole list every time.\n\nFor example, this program:\n\n```python\ns = HashSet(8)\nprint(s.add(3), s.add(11), s.add(3))\nprint(s.contains(11), s.contains(19), s.size())\n```\n\nprints (3 and 11 share bucket 3; 19 would go there too, but it isn't in the set):\n\n```\nTrue True False\nTrue False 2\n```\n","kind":"code","seed":"class HashSet:\n    def __init__(self, bucket_count):\n        pass\n\n    def add(self, key):\n        pass\n\n    def contains(self, key):\n        pass\n\n    def size(self):\n        pass\n\n\nif __name__ == \"__main__\":\n    s = HashSet(8)\n    print(s.add(3), s.add(11), s.add(3))\n    print(s.contains(11), s.contains(19), s.size())\n","solution":"class HashSet:\n    def __init__(self, bucket_count):\n        self.buckets = [[] for i in range(bucket_count)]\n        self.count = 0\n\n    def add(self, key):\n        bucket = self.buckets[hash(key) % len(self.buckets)]\n        if key in bucket:\n            return False\n        bucket.append(key)\n        self.count += 1\n        return True\n\n    def contains(self, key):\n        return key in self.buckets[hash(key) % len(self.buckets)]\n\n    def size(self):\n        return self.count\n\n\nif __name__ == \"__main__\":\n    s = HashSet(8)\n    print(s.add(3), s.add(11), s.add(3))\n    print(s.contains(11), s.contains(19), s.size())\n","hints":["Start like `HashMap`: in `__init__`, make `bucket_count` empty buckets with `[[] for i in range(bucket_count)]`, and a counter that starts at 0.","`add` works out the bucket with `hash(key) % len(self.buckets)`. If the key is already in that bucket, return `False`; otherwise append it, add one to the counter and return `True`.","`contains` looks in the key's bucket only: `key in bucket` searches one short chain, not every key of the set."],"tests":[{"name":"Test 1","stdin":"","call":"s = HashSet(8)\nprint(s.add(3), s.add(11), s.add(3))\nprint(s.contains(11), s.contains(19), s.size())\n","expect":"True True False\nTrue False 2","hidden":false},{"name":"Test 2","stdin":"","call":"s = HashSet(4)\nfor word in [\"fig\", \"kiwi\", \"fig\", \"lime\"]:\n    s.add(word)\nprint(s.size(), s.contains(\"kiwi\"), s.contains(\"plum\"))\n","expect":"3 True False","hidden":true},{"name":"Test 3","stdin":"","call":"s = HashSet(5)\nprint(s.add(-3), s.add(2), s.contains(-3), s.contains(-8))\n","expect":"True True True False","hidden":true},{"name":"Test 4","stdin":"","call":"s = HashSet(1)\nprint(s.size(), s.add(10), s.add(20), s.add(10), s.size())\n","expect":"0 True True False 2","hidden":true},{"name":"Test 5","stdin":"","call":"s = HashSet(100_000)\nfor i in range(100_000):\n    s.add(i * 7)\nprint(s.size(), sum(1 for i in range(100_000) if s.contains(i * 3)))\n","expect":"100000 14286","hidden":true,"big":true}],"require":[],"forbid":[{"pattern":"[{}]|\\bset\\s*\\(|\\bdict\\s*\\(","message":"Build the set from your own buckets, without Python's `set` or `dict`."}]},{"task":"Add a method `remove(key)` to `HashMap`. It removes the key's pair from its bucket and returns the key's value, or returns `None` when the key isn't in the map. The other pairs in the bucket must stay.\n\nFor example, this program:\n\n```python\nrooms = HashMap(4)\nrooms.put(101, \"Ada\")\nrooms.put(205, \"Ben\")\nprint(rooms.remove(101), rooms.remove(101))\nprint(rooms.buckets)\n```\n\nprints:\n\n```\nAda None\n[[], [[205, 'Ben']], [], []]\n```\n","kind":"code","seed":"class HashMap:\n    def __init__(self, size):\n        self.buckets = [[] for i in range(size)]\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def put(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n\n    def get(self, key):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                return pair[1]\n        return None\n\n    def remove(self, key):\n        pass\n\n\nif __name__ == \"__main__\":\n    rooms = HashMap(4)\n    rooms.put(101, \"Ada\")\n    rooms.put(205, \"Ben\")\n    print(rooms.remove(101), rooms.remove(101))\n    print(rooms.buckets)\n","solution":"class HashMap:\n    def __init__(self, size):\n        self.buckets = [[] for i in range(size)]\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def put(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n\n    def get(self, key):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                return pair[1]\n        return None\n\n    def remove(self, key):\n        bucket = self.buckets[self._index(key)]\n        for i in range(len(bucket)):\n            if bucket[i][0] == key:\n                value = bucket[i][1]\n                del bucket[i]\n                return value\n        return None\n\n\nif __name__ == \"__main__\":\n    rooms = HashMap(4)\n    rooms.put(101, \"Ada\")\n    rooms.put(205, \"Ben\")\n    print(rooms.remove(101), rooms.remove(101))\n    print(rooms.buckets)\n","hints":["Find the key's bucket as `get` does, then the position of the key's pair in it.","`for i in range(len(bucket))` gives the positions. When `bucket[i][0] == key`, keep the value in a variable, remove the pair with `del bucket[i]` (or `bucket.pop(i)`), and return the value.","Return as soon as the pair is gone: `range(len(bucket))` was worked out before the bucket got shorter, so going on would look past its end and stop with an `IndexError`. When the loop ends without finding the key, return `None`."],"tests":[{"name":"Test 1","stdin":"","call":"rooms = HashMap(4)\nrooms.put(101, \"Ada\")\nrooms.put(205, \"Ben\")\nprint(rooms.remove(101), rooms.remove(101))\nprint(rooms.buckets)\n","expect":"Ada None\n[[], [[205, 'Ben']], [], []]","hidden":false},{"name":"Test 2","stdin":"","call":"m = HashMap(4)\nfor key in [1, 5, 9]:\n    m.put(key, key * 100)\nprint(m.remove(5), m.get(1), m.get(9), m.get(5))\nprint(m.buckets)\n","expect":"500 100 900 None\n[[], [[1, 100], [9, 900]], [], []]","hidden":true},{"name":"Test 3","stdin":"","call":"m = HashMap(4)\nm.put(2, \"two\")\nprint(m.remove(6), m.remove(3), m.buckets)\n","expect":"None None [[], [], [[2, 'two']], []]","hidden":true},{"name":"Test 4","stdin":"","call":"m = HashMap(8)\nm.put(\"tea\", 3)\nprint(m.remove(\"tea\"), m.get(\"tea\"))\nm.put(\"tea\", 4)\nprint(m.get(\"tea\"))\n","expect":"3 None\n4","hidden":true}],"require":[],"forbid":[]}]}