Skip to content

Commit 89f4e49

Browse files
authored
Merge pull request keon#54 from orenovadia/hashtable-fixes
Hashtable fixes
2 parents d6d40fc + c320bb6 commit 89f4e49

File tree

1 file changed

+180
-34
lines changed

1 file changed

+180
-34
lines changed

map/hashtable.py

Lines changed: 180 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,200 @@
1-
"""
2-
MAP Abstract Data Type
3-
Map() Create a new, empty map. It returns an empty map collection.
4-
put(key,val) Add a new key-value pair to the map. If the key is already in the map then replace the old value with the new value.
5-
get(key) Given a key, return the value stored in the map or None otherwise.
6-
del Delete the key-value pair from the map using a statement of the form del map[key].
7-
len() Return the number of key-value pairs stored in the map.
8-
in Return True for a statement of the form key in map, if the given key is in the map, False otherwise.
9-
"""
1+
from unittest import TestCase
2+
3+
104
class HashTable(object):
11-
def __init__(self, size = 11):
5+
"""
6+
HashMap Data Type
7+
HashMap() Create a new, empty map. It returns an empty map collection.
8+
put(key, val) Add a new key-value pair to the map. If the key is already in the map then replace
9+
the old value with the new value.
10+
get(key) Given a key, return the value stored in the map or None otherwise.
11+
del_(key) or del map[key] Delete the key-value pair from the map using a statement of the form del map[key].
12+
len() Return the number of key-value pairs stored in the map.
13+
in Return True for a statement of the form key in map, if the given key is in the map, False otherwise.
14+
"""
15+
16+
_empty = object()
17+
_deleted = object()
18+
19+
def __init__(self, size=11):
1220
self.size = size
13-
self.keys = [None] * size # keys
14-
self.values = [None] * size # values
21+
self._len = 0
22+
self._keys = [self._empty] * size # keys
23+
self._values = [self._empty] * size # values
1524

1625
def put(self, key, value):
17-
hashval = self.hash(key)
18-
19-
if hashval not in keys:
20-
self.keys[hashval] = key
21-
self.values[hashval] = value
22-
else:
23-
if self.keys[hashval] == key: # replace
24-
self.values[hashval] = value
25-
else: # probing
26-
rehashval = self.rehash(key)
27-
while self.keys[rehashval] is not None and \
28-
self.keys[rehashval] != key:
29-
rehashval = self.rehash(rehashval)
30-
if keys[rehashval] is None:
31-
self.keys[rehashval] = key
32-
self.values[rehashval] = value
33-
else:
34-
self.values[rehashval] = value # replacee
26+
initial_hash = hash_ = self.hash(key)
27+
28+
while True:
29+
if self._keys[hash_] is self._empty or self._keys[hash_] is self._deleted:
30+
# can assign to hash_ index
31+
self._keys[hash_] = key
32+
self._values[hash_] = value
33+
self._len += 1
34+
return
35+
elif self._keys[hash_] == key:
36+
# key already exists here, assign over
37+
self._keys[hash_] = key
38+
self._values[hash_] = value
39+
return
40+
41+
hash_ = self._rehash(hash_)
42+
43+
if initial_hash == hash_:
44+
# table is full
45+
raise ValueError("Table is full")
3546

3647
def get(self, key):
37-
pass
48+
initial_hash = hash_ = self.hash(key)
49+
while True:
50+
if self._keys[hash_] is self._empty:
51+
# That key was never assigned
52+
return None
53+
elif self._keys[hash_] == key:
54+
# key found
55+
return self._values[hash_]
56+
57+
hash_ = self._rehash(hash_)
58+
if initial_hash == hash_:
59+
# table is full and wrapped around
60+
return None
61+
62+
def del_(self, key):
63+
initial_hash = hash_ = self.hash(key)
64+
while True:
65+
if self._keys[hash_] is self._empty:
66+
# That key was never assigned
67+
return None
68+
elif self._keys[hash_] == key:
69+
# key found, assign with deleted sentinel
70+
self._keys[hash_] = self._deleted
71+
self._values[hash_] = self._deleted
72+
self._len -= 1
73+
return
74+
75+
hash_ = self._rehash(hash_)
76+
if initial_hash == hash_:
77+
# table is full and wrapped around
78+
return None
3879

3980
def hash(self, key):
4081
return key % self.size
4182

42-
def rehash(self, oldhash):
83+
def _rehash(self, old_hash):
4384
"""
4485
linear probing
4586
"""
46-
return (oldhash + 1) % self.size
87+
return (old_hash + 1) % self.size
4788

48-
def __getitem__(self,key):
89+
def __getitem__(self, key):
4990
return self.get(key)
5091

92+
def __delitem__(self, key):
93+
return self.del_(key)
94+
5195
def __setitem__(self, key, value):
5296
self.put(key, value)
5397

98+
def __len__(self):
99+
return self._len
100+
101+
102+
class ResizableHashTable(HashTable):
103+
MIN_SIZE = 8
104+
105+
def __init__(self):
106+
super().__init__(self.MIN_SIZE)
107+
108+
def put(self, key, value):
109+
rv = super().put(key, value)
110+
# increase size of dict * 2 if filled >= 2/3 size (like python dict)
111+
if len(self) >= (self.size * 2) / 3:
112+
self.__resize()
113+
114+
def __resize(self):
115+
keys, values = self._keys, self._values
116+
self.size *= 2 # this will be the new size
117+
self._len = 0
118+
self._keys = [self._empty] * self.size
119+
self._values = [self._empty] * self.size
120+
for key, value in zip(keys, values):
121+
if key is not self._empty and key is not self._deleted:
122+
self.put(key, value)
123+
124+
125+
class TestHashTable(TestCase):
126+
def test_one_entry(self):
127+
m = HashTable(10)
128+
m.put(1, '1')
129+
self.assertEqual('1', m.get(1))
130+
131+
def test_add_entry_bigger_than_table_size(self):
132+
m = HashTable(10)
133+
m.put(11, '1')
134+
self.assertEqual('1', m.get(11))
135+
136+
def test_get_none_if_key_missing_and_hash_collision(self):
137+
m = HashTable(10)
138+
m.put(1, '1')
139+
self.assertEqual(None, m.get(11))
140+
141+
def test_two_entries_with_same_hash(self):
142+
m = HashTable(10)
143+
m.put(1, '1')
144+
m.put(11, '11')
145+
self.assertEqual('1', m.get(1))
146+
self.assertEqual('11', m.get(11))
147+
148+
def test_get_on_full_table_does_halts(self):
149+
# and does not search forever
150+
m = HashTable(10)
151+
for i in range(10, 20):
152+
m.put(i, i)
153+
self.assertEqual(None, m.get(1))
154+
155+
def test_delete_key(self):
156+
m = HashTable(10)
157+
m.put(1, 1)
158+
m.del_(1)
159+
self.assertEqual(None, m.get(1))
160+
161+
def test_delete_key_and_reassign(self):
162+
m = HashTable(10)
163+
m.put(1, 1)
164+
del m[1]
165+
m.put(1, 2)
166+
self.assertEqual(2, m.get(1))
167+
168+
def test_assigning_to_full_table_throws_error(self):
169+
m = HashTable(3)
170+
m.put(1, 1)
171+
m.put(2, 2)
172+
m.put(3, 3)
173+
with self.assertRaises(ValueError):
174+
m.put(4, 4)
175+
176+
def test_len_trivial(self):
177+
m = HashTable(10)
178+
self.assertEqual(0, len(m))
179+
for i in range(10):
180+
m.put(i, i)
181+
self.assertEqual(i + 1, len(m))
182+
183+
def test_len_after_deletions(self):
184+
m = HashTable(10)
185+
m.put(1, 1)
186+
self.assertEqual(1, len(m))
187+
m.del_(1)
188+
self.assertEqual(0, len(m))
189+
m.put(11, 42)
190+
self.assertEqual(1, len(m))
54191

192+
def test_resizable_hash_table(self):
193+
m = ResizableHashTable()
194+
self.assertEqual(ResizableHashTable.MIN_SIZE, m.size)
195+
for i in range(ResizableHashTable.MIN_SIZE):
196+
m.put(i, 'foo')
197+
self.assertEqual(ResizableHashTable.MIN_SIZE * 2, m.size)
198+
self.assertEqual('foo', m.get(1))
199+
self.assertEqual('foo', m.get(3))
200+
self.assertEqual('foo', m.get(ResizableHashTable.MIN_SIZE - 1))

0 commit comments

Comments
 (0)