Skip to content

Commit 4a6ab27

Browse files
committed
8/21/23
- anagrams.py - is_string_permutable_to_palindrome.py - is_anonymous_letter_constructible.py - lru_cache_hard_way.py - lru_cache.py
1 parent f68c64d commit 4a6ab27

File tree

6 files changed

+260
-17
lines changed

6 files changed

+260
-17
lines changed

elements-of-programming-interviews/problem_mapping.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1450,7 +1450,7 @@ problem_mapping = {
14501450
"total": 9
14511451
},
14521452
"Python: anagrams.py": {
1453-
"passed": 0,
1453+
"passed": 9,
14541454
"total": 9
14551455
}
14561456
},
@@ -1464,7 +1464,7 @@ problem_mapping = {
14641464
"total": 305
14651465
},
14661466
"Python: is_string_permutable_to_palindrome.py": {
1467-
"passed": 0,
1467+
"passed": 305,
14681468
"total": 305
14691469
}
14701470
},
@@ -1478,7 +1478,7 @@ problem_mapping = {
14781478
"total": 212
14791479
},
14801480
"Python: is_anonymous_letter_constructible.py": {
1481-
"passed": 0,
1481+
"passed": 212,
14821482
"total": 212
14831483
}
14841484
},
@@ -1492,7 +1492,7 @@ problem_mapping = {
14921492
"total": 101
14931493
},
14941494
"Python: lru_cache.py": {
1495-
"passed": 0,
1495+
"passed": 101,
14961496
"total": 101
14971497
}
14981498
},

elements-of-programming-interviews/python/anagrams.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,24 @@
22

33
from test_framework import generic_test, test_utils
44

5+
"""
6+
In: list of strings; strings fit in mem
7+
Out: list of list of strings, grouped by anagrams, must have at least 2
8+
9+
edges:
10+
- empty list, no anagrams, all anagrams
11+
12+
- sort lowercased strings, use as keys
13+
"""
14+
15+
import collections
16+
517

618
def find_anagrams(dictionary: List[str]) -> List[List[str]]:
7-
# TODO - you fill in here.
8-
return []
19+
dd = collections.defaultdict(list)
20+
for word in dictionary:
21+
dd["".join(sorted(word.lower()))].append(word)
22+
return [v for v in dd.values() if len(v) > 1]
923

1024

1125
if __name__ == '__main__':

elements-of-programming-interviews/python/is_anonymous_letter_constructible.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from test_framework import generic_test
2+
import collections
3+
import string
24

35

46
def is_letter_constructible_from_magazine(letter_text: str,
57
magazine_text: str) -> bool:
6-
# TODO - you fill in here.
7-
return True
8+
letter = collections.Counter(letter_text)
9+
magazine = collections.Counter(magazine_text)
10+
return all(letter[k] <= magazine[k] for k in letter if k not in string.whitespace)
811

912

1013
if __name__ == '__main__':

elements-of-programming-interviews/python/is_string_permutable_to_palindrome.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
from test_framework import generic_test
22

33

4+
"""
5+
in: string
6+
out: bool
7+
8+
edges:
9+
- empty (assume yes)
10+
- case? assuming it matters, "Aba" not palindrome
11+
- whitespace? assume words won't have whitespace, can handle if so
12+
13+
count letters?
14+
"""
15+
from collections import Counter
16+
17+
418
def can_form_palindrome(s: str) -> bool:
5-
# TODO - you fill in here.
6-
return True
19+
return len([v for v in Counter(s).values() if v % 2]) < 2
720

821

922
if __name__ == '__main__':
23+
assert can_form_palindrome("")
24+
assert can_form_palindrome("aa")
25+
assert can_form_palindrome("Aaa")
26+
assert not can_form_palindrome("ba")
27+
assert not can_form_palindrome("john says no")
28+
assert not can_form_palindrome("sit on a potato pan otis")
29+
assert can_form_palindrome("sitonapotatopanotis")
30+
1031
exit(
1132
generic_test.generic_test_main(
1233
'is_string_permutable_to_palindrome.py',

elements-of-programming-interviews/python/lru_cache.py

+42-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,57 @@
11
from test_framework import generic_test
22
from test_framework.test_failure import TestFailure
33

4+
"""
5+
- isbns are positive ints
6+
- LRU can be implemented with a queue
7+
- hashing done with dict
8+
- naive approach:
9+
- use queue and dict
10+
- whenever lookup/insert occurs, find item in queue, remove, and re-insert
11+
- if an item is ever outside the valid length of the queue, dequeue it and erase from dict
12+
- lookup, insert, and erase are O(n) where n is cache capacity. Can we achieve better? O(1)?
13+
- If we keep an op counter, our "queue" could be implemented as a bst or sorted array? (no, we'd still
14+
have to shift)
15+
- Could we use a doubly linked list? each dict entry has value and dll node pointer
16+
- have pointers newest and oldest, both start null
17+
- anytime something is updated, if other nodes were pointing to it, we clip it out and connect them
18+
- we then make it the newest
19+
- if we have to evict, we knock out the oldest and make its follower the new oldest
20+
"""
21+
22+
from collections import OrderedDict
23+
424

525
class LruCache:
626
def __init__(self, capacity: int) -> None:
7-
# TODO - you fill in here.
8-
return
27+
self.data = OrderedDict()
28+
self.capacity = capacity
29+
30+
def _update_isbn(self, isbn):
31+
if isbn not in self.data:
32+
return
33+
price = self.data[isbn]
34+
del self.data[isbn]
35+
self.data[isbn] = price
936

1037
def lookup(self, isbn: int) -> int:
11-
# TODO - you fill in here.
12-
return 0
38+
if isbn not in self.data:
39+
return -1
40+
self._update_isbn(isbn)
41+
return self.data[isbn]
1342

1443
def insert(self, isbn: int, price: int) -> None:
15-
# TODO - you fill in here.
16-
return
44+
if isbn in self.data:
45+
self._update_isbn(isbn)
46+
return
47+
self.data[isbn] = price
48+
if len(self.data) > self.capacity:
49+
self.data.popitem(last=False) # FIFO order
1750

1851
def erase(self, isbn: int) -> bool:
19-
# TODO - you fill in here.
52+
if isbn not in self.data:
53+
return False
54+
del self.data[isbn]
2055
return True
2156

2257

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from test_framework import generic_test
2+
from test_framework.test_failure import TestFailure
3+
4+
"""
5+
- isbns are positive ints
6+
- LRU can be implemented with a queue
7+
- hashing done with dict
8+
- naive approach:
9+
- use queue and dict
10+
- whenever lookup/insert occurs, find item in queue, remove, and re-insert
11+
- if an item is ever outside the valid length of the queue, dequeue it and erase from dict
12+
- lookup, insert, and erase are O(n) where n is cache capacity. Can we achieve better? O(1)?
13+
- If we keep an op counter, our "queue" could be implemented as a bst or sorted array? (no, we'd still
14+
have to shift)
15+
- Could we use a doubly linked list? each dict entry has value and dll node pointer
16+
- have pointers newest and oldest, both start null
17+
- anytime something is updated, if other nodes were pointing to it, we clip it out and connect them
18+
- we then make it the newest
19+
- if we have to evict, we knock out the oldest and make its follower the new oldest
20+
"""
21+
22+
23+
class QueueNode:
24+
def __init__(self, isbn, price):
25+
self.isbn = isbn
26+
self.price = price
27+
self.more = None
28+
self.less = None
29+
30+
def __repr__(self):
31+
more = self.more.isbn if self.more else None
32+
less = self.less.isbn if self.less else None
33+
return f"QueueNode(isbn: {self.isbn}, price: {self.price}, more: {more}, less: {less})"
34+
35+
36+
class LruCache:
37+
def __init__(self, capacity: int) -> None:
38+
self.head = None
39+
self.tail = None
40+
self.capacity = capacity
41+
self.data = {}
42+
43+
def __repr__(self):
44+
nodes = []
45+
node = self.head
46+
while node:
47+
nodes.append(repr(node))
48+
nodes.append("\n")
49+
node = node.less
50+
# print(f"repring node: {repr(node)}")
51+
return "".join(nodes)
52+
53+
def _eviction_policy(self):
54+
if len(self.data) <= self.capacity:
55+
return
56+
if self.capacity == 0:
57+
self.head = None
58+
self.tail = None
59+
self.data = {}
60+
else:
61+
to_delete = self.tail
62+
self.tail = to_delete.more
63+
self.tail.less = None
64+
del self.data[to_delete.isbn]
65+
66+
def _update_recency(self, node):
67+
if self.head == self.tail == None:
68+
# first node inserted
69+
self.head = node
70+
self.tail = node
71+
return
72+
73+
if node == self.head:
74+
# Node is already most recent, nothing to do here
75+
return
76+
77+
if node == self.tail:
78+
# If there's a node more recent than this one,
79+
# it becomes the new tail
80+
if node.more:
81+
self.tail = node.more
82+
self.tail.less = None
83+
84+
if node != self.head and node != self.tail:
85+
# Ensure old more/less point to each other, if they exist
86+
if node.more:
87+
node.more.less = node.less
88+
if node.less:
89+
node.less.more = node.more
90+
91+
# Bump previous head down if it existed and we weren't it.
92+
self.head.more = node
93+
node.less = self.head
94+
self.head = node
95+
node.more = None
96+
97+
def lookup(self, isbn: int) -> int:
98+
if isbn not in self.data:
99+
# print(f"Lookup {isbn} (not found)")
100+
# print(f"{self.__repr__()}")
101+
return -1
102+
node = self.data[isbn]
103+
price = node.price
104+
self._update_recency(node)
105+
# print(f"Lookup {isbn}")
106+
# print(f"{self.__repr__()}")
107+
return price
108+
109+
def insert(self, isbn: int, price: int) -> None:
110+
if isbn in self.data:
111+
node = self.data[isbn]
112+
else:
113+
node = QueueNode(isbn, price)
114+
self.data[isbn] = node
115+
self._update_recency(node)
116+
self._eviction_policy()
117+
# print(f"Insert ({isbn},{price})")
118+
# print(f"{self.__repr__()}")
119+
120+
def erase(self, isbn: int) -> bool:
121+
if isbn not in self.data:
122+
# print(f"Erase {isbn} (not found)")
123+
# print(f"{self.__repr__()}")
124+
125+
return False
126+
127+
node = self.data[isbn]
128+
if node.more:
129+
node.more.less = node.less
130+
if node.less:
131+
node.less.more = node.more
132+
133+
if node == self.head:
134+
self.head = node.less
135+
if node == self.tail:
136+
self.tail = node.more
137+
138+
del self.data[isbn]
139+
# print(f"Erase {isbn}")
140+
# print(f"{self.__repr__()}")
141+
return True
142+
143+
144+
def lru_cache_tester(commands):
145+
if len(commands) < 1 or commands[0][0] != 'LruCache':
146+
raise RuntimeError('Expected LruCache as first command')
147+
148+
cache = LruCache(commands[0][1])
149+
150+
for cmd in commands[1:]:
151+
if cmd[0] == 'lookup':
152+
result = cache.lookup(cmd[1])
153+
if result != cmd[2]:
154+
raise TestFailure('Lookup: expected ' + str(cmd[2]) +
155+
', got ' + str(result))
156+
elif cmd[0] == 'insert':
157+
cache.insert(cmd[1], cmd[2])
158+
elif cmd[0] == 'erase':
159+
result = 1 if cache.erase(cmd[1]) else 0
160+
if result != cmd[2]:
161+
raise TestFailure('Erase: expected ' + str(cmd[2]) + ', got ' +
162+
str(result))
163+
else:
164+
raise RuntimeError('Unexpected command ' + cmd[0])
165+
166+
167+
if __name__ == '__main__':
168+
exit(
169+
generic_test.generic_test_main('lru_cache.py', 'lru_cache.tsv',
170+
lru_cache_tester))

0 commit comments

Comments
 (0)