# 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 ```