# CSPT13 Hash Tables III
## Hash Table Implementation w/ Chaining
```
class HashTableEntry:
"""
Linked List hash table key/value pair
"""
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
if isinstance(other, HashTableEntry):
return self.key == other.key
return False
def __repr__(self):
return f'HashTableEntry({self.key}, {self.value})'
class Node:
def __init__(self, value):
self.value = value
self.next = None
def __repr__(self):
return f'Node({self.value})'
class LinkedList:
def __init__(self):
self.head = None
def __repr__(self):
curStr = ""
cur = self.head
while cur is not None:
curStr += f'{str(cur.value)} -> '
cur = cur.next
return curStr
def find(self, value):
cur = self.head
while cur is not None:
if cur.value == value:
return cur
cur = cur.next
return None
def delete(self, value):
cur = self.head
# Special case of deleting head
if cur.value == value:
self.head = cur.next
return cur
# General case of deleting internal node
prev = cur
cur = cur.next
while cur is not None:
if cur.value == value: # Found it!
prev.next = cur.next # Cut it out
cur.next = None
return cur # Return deleted node
else:
prev = cur
cur = cur.next
return None # If we got here, nothing found
def insert_at_head(self, node):
node.next = self.head
self.head = node
def insert_at_head_or_overwrite(self, node):
existingNode = self.find(node.value)
if existingNode is not None:
existingNode.value = node.value
return False
else:
self.insert_at_head(node)
return True
MIN_CAPACITY = 8
class HashTable:
"""
A hash table that with `capacity` buckets
that accepts string keys
Implement this.
"""
def __init__(self, capacity):
self.table = [None] * capacity
self.num_elements = 0
def get_num_slots(self):
"""
Return the length of the list you're using to hold the hash
table data. (Not the number of items stored in the hash table,
but the number of slots in the main list.)
One of the tests relies on this.
Implement this.
"""
return len(self.table)
def get_load_factor(self):
"""
Return the load factor for this hash table.
Implement this.
"""
return self.num_elements / self.get_num_slots()
def fnv1(self, key):
"""
FNV-1 Hash, 64-bit
Implement this, and/or DJB2.
"""
# Your code here
def djb2(self, key):
"""
DJB2 hash, 32-bit
Implement this, and/or FNV-1.
"""
hash = 5381
for x in key:
hash = (( hash << 5) + hash) + ord(x)
return hash & 0xFFFFFFFF
def hash_index(self, key):
"""
Take an arbitrary key and return a valid integer index
between within the storage capacity of the hash table.
"""
return self.djb2(key) % self.get_num_slots()
def put(self, key, value):
"""
Store the value with the given key.
Hash collisions should be handled with Linked List Chaining.
Implement this.
"""
hash_index = self.hash_index(key)
if self.table[hash_index] != None:
linked_list = self.table[hash_index]
did_add_new_node = linked_list.insert_at_head_or_overwrite(Node(HashTableEntry(key, value)))
if did_add_new_node:
self.num_elements += 1
else:
linked_list = LinkedList()
linked_list.insert_at_head(Node(HashTableEntry(key, value)))
self.table[hash_index] = linked_list
self.num_elements += 1
if self.get_load_factor() > 0.7:
self.resize(self.get_num_slots() * 2)
def delete(self, key):
"""
Remove the value stored with the given key.
Print a warning if the key is not found.
Implement this.
"""
hash_index = self.hash_index(key)
if self.table[hash_index] != None:
linked_list = self.table[hash_index]
did_delete_node = linked_list.delete(HashTableEntry(key, None))
if did_delete_node != None:
self.num_elements -= 1
if self.get_load_factor() < 0.2:
self.resize(self.get_num_slots() / 2)
else:
print("Warning: node not found")
def get(self, key):
"""
Retrieve the value stored with the given key.
Returns None if the key is not found.
Implement this.
"""
hash_index = self.hash_index(key)
if self.table[hash_index] != None:
linked_list = self.table[hash_index]
node = linked_list.find(HashTableEntry(key, None))
if node != None:
return node.value.value
return None
def resize(self, new_capacity):
"""
Changes the capacity of the hash table and
rehashes all key/value pairs.
Implement this.
"""
old_table = self.table
self.table = [None] * int(new_capacity)
self.num_elements = 0
for element in old_table:
if element == None:
continue
curr_node = element.head
while curr_node != None:
temp = curr_node.next
curr_node.next = None
hash_index = self.hash_index(curr_node.value.key)
if self.table[hash_index] != None:
linked_list = self.table[hash_index]
linked_list.insert_at_head(curr_node)
else:
linked_list = LinkedList()
linked_list.insert_at_head(curr_node)
self.table[hash_index] = linked_list
curr_node = temp
self.num_elements += 1
if __name__ == "__main__":
ht = HashTable(8)
ht.put("line_1", "'Twas brillig, and the slithy toves")
ht.put("line_2", "Did gyre and gimble in the wabe:")
ht.put("line_3", "All mimsy were the borogoves,")
ht.put("line_4", "And the mome raths outgrabe.")
ht.put("line_5", '"Beware the Jabberwock, my son!')
ht.put("line_6", "The jaws that bite, the claws that catch!")
ht.put("line_7", "Beware the Jubjub bird, and shun")
ht.put("line_8", 'The frumious Bandersnatch!"')
ht.put("line_9", "He took his vorpal sword in hand;")
ht.put("line_10", "Long time the manxome foe he sought--")
ht.put("line_11", "So rested he by the Tumtum tree")
ht.put("line_12", "And stood awhile in thought.")
print("")
# Test storing beyond capacity
for i in range(1, 13):
print(ht.get(f"line_{i}"))
# Test resizing
old_capacity = ht.get_num_slots()
ht.resize(ht.capacity * 2)
new_capacity = ht.get_num_slots()
print(f"\nResized from {old_capacity} to {new_capacity}.\n")
# Test if data intact after resizing
for i in range(1, 13):
print(ht.get(f"line_{i}"))
print("")
```
## [Single Number](https://leetcode.com/problems/single-number/)
```
# https://leetcode.com/problems/single-number/
"""
Understand
[2, 2, 1] --> 1
[1] --> 1
[2, 1, 2] --> 1
Plan
Use a dictionary to keep track of how many times an item exists [element: # of times it appears]
Go through the dictionary and output the item that appears once
Runtime: O(n)
Space: O(n)
"""
class Solution:
def singleNumber(self, nums: List[int]) -> int:
dictionary = {}
for n in nums:
if n in dictionary:
dictionary[n] += 1
else:
dictionary[n] = 1
for (key, value) in dictionary.items():
if value == 1:
return key
return -1
```
## [Implement Hash Set](https://leetcode.com/problems/design-hashset/)
```
# https://leetcode.com/problems/design-hashset/
"""
Understand
mySet = MyHashSet()
mySet.add(1) // {1}
mySet.add(2) // {1, 2}
mySet.add(1) // {1, 2}
mySet.contains(1) // True
mySet.remove(1)
mySet.contains(1) // False
Plan
Implement a hash table to implement get/add/remove
Use a popular hashing algorithm to map elements into buckets
Solve collisions using chaining
For simplicity we just need to initialize an array of size 10001 and not need to resize
Runtime: O(1)
Space: O(1)
"""
from collections import deque
class MyHashSet:
def __init__(self):
self.arr = [None] * 10001
def hashIndex(self, key):
return hash(key) % len(self.arr)
def add(self, key: int) -> None:
hashIndex = self.hashIndex(key)
if self.arr[hashIndex] is None:
newList = deque()
newList.append(key)
self.arr[hashIndex] = newList
elif key not in self.arr[hashIndex]:
self.arr[hashIndex].append(key)
def remove(self, key: int) -> None:
hashIndex = self.hashIndex(key)
if self.arr[hashIndex] is not None:
try:
self.arr[hashIndex].remove(key)
except:
pass
def contains(self, key: int) -> bool:
hashIndex = self.hashIndex(key)
if self.arr[hashIndex] is not None:
return key in self.arr[hashIndex]
return False
```