Compare commits

...

7 Commits

Author SHA1 Message Date
7e44b37850 remove print numbers 2025-03-26 01:29:03 +00:00
4cd7bb7d18 reorganise directories
upd chapter_01/binary.py
2025-03-26 01:27:35 +00:00
ab09aa5269 add visited set 2024-02-19 00:03:44 +00:00
1d22356ce5 upd readmes, rename exercises. 2024-02-06 22:04:37 +00:00
13a0852cb9 Merge branch 'master' of github.com:onyx-and-iris/grokking-algorithms 2024-02-06 21:53:23 +00:00
3fe14b8ac0 clean up repo.
add more notes
2024-02-06 21:53:17 +00:00
133502e1ad remove format, datefmt 2024-01-20 04:19:28 +00:00
49 changed files with 120 additions and 110 deletions

2
.gitignore vendored
View File

@@ -160,3 +160,5 @@ cython_debug/
#.idea/ #.idea/
words_alpha.txt words_alpha.txt
tests/

View File

@@ -40,13 +40,13 @@
[chapter12][knn] [chapter12][knn]
[binary]: ./chapter1/ [binary]: ./chapter_01/
[selection_sort]: ./chapter2/ [selection_sort]: ./chapter_02/
[recursion]: ./chapter3/ [recursion]: ./chapter_03/
[quick_sort]: ./chapter4/ [quick_sort]: ./chapter_04/
[bfs]: ./chapter6/ [bfs]: ./chapter_06/
[trees]: ./chapter7/ [trees]: ./chapter_07/
[dijkstra]: ./chapter9/ [dijkstra]: ./chapter_09/
[greedy]: ./chapter10/ [greedy]: ./chapter_10/
[dynamic]: ./chapter11/ [dynamic]: ./chapter_11/
[knn]: ./chapter12/ [knn]: ./chapter_12/

View File

@@ -1,40 +0,0 @@
import logging
import random
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def binary_search(arr, item):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
guess = arr[mid]
if guess == item:
return mid
elif guess > item:
high = mid - 1
else:
low = mid + 1
return None
LOWER = 1000
UPPER = 1000000
SAMPLE_SIZE = 1000
numbers = random.sample(range(LOWER, UPPER), SAMPLE_SIZE)
numbers.sort()
result = None
while result is None:
guess = random.randrange(LOWER, UPPER)
logger.debug(f"guess: {guess}")
result = binary_search(numbers, guess)
print(f"Found {guess} at index {result}.")

View File

@@ -1,7 +0,0 @@
import math
num_steps = int(math.log2(128))
print(
f"A binary search would take maximum {num_steps} steps "
"to search a list of 128 items."
)

View File

@@ -1,7 +0,0 @@
import math
num_steps = int(math.log2(128*2))
print(
f"A binary search would take maximum {num_steps} steps "
"to search a list of 256 items."
)

View File

@@ -1,10 +0,0 @@
import heapq
customers = []
heapq.heappush(customers, (2, "Harry"))
heapq.heappush(customers, (3, "Charles"))
heapq.heappush(customers, (1, "Riya"))
heapq.heappush(customers, (4, "Stacy"))
while customers:
print(heapq.heappop(customers))

View File

@@ -1,14 +0,0 @@
customers = []
customers.append((2, "Harry")) # no sort needed here because 1 item.
customers.append((3, "Charles"))
customers.sort(reverse=True)
# Need to sort to maintain order
customers.append((1, "Riya"))
customers.sort(reverse=True)
# Need to sort to maintain order
customers.append((4, "Stacy"))
customers.sort(reverse=True)
while customers:
print(customers.pop(0))
# Will print names in the order: Stacy, Charles, Harry, Riya.

View File

@@ -1,12 +0,0 @@
from queue import PriorityQueue
customers = (
PriorityQueue()
) # we initialise the PQ class instead of using a function to operate upon a list.
customers.put((2, "Harry"))
customers.put((3, "Charles"))
customers.put((1, "Riya"))
customers.put((4, "Stacy"))
while customers:
print(customers.get())

5
chapter_01/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Binary Search
Repeatedly split the array checking if value is greater or less than the mid point. Stop when the exact value is found.
It takes log N steps to reduce an array of size N to an array of size 1. Time complexity for this algorithm is `O(log N)`.

43
chapter_01/binary.py Normal file
View File

@@ -0,0 +1,43 @@
import logging
import random
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class BinarySearch:
def __init__(self, arr):
self._arr = arr
def search(self, item):
low = 0
high = len(self._arr) - 1
while low <= high:
mid = (low + high) // 2
guess = self._arr[mid]
if guess == item:
return mid
elif guess > item:
high = mid - 1
else:
low = mid + 1
return None
LOWER = 1000
UPPER = 1000000
SAMPLE_SIZE = 1000
numbers = random.sample(range(LOWER, UPPER), SAMPLE_SIZE)
numbers.extend([9000, 999999])
numbers.sort()
search = BinarySearch(numbers)
print(search.search(42))
print(search.search(9000))
print(search.search(20000))
print(search.search(60000))
print(search.search(999999))

7
chapter_02/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Selection Sort
We have to perform N swaps a total of N times. This takes N^N steps, therefore:
This algorithm has time complexity `O(N^2)`
Technically (`n 1, n - 2 ... 2, 1` ~= N/2) swaps are performed but in BigO the constants are dropped.

10
chapter_03/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Recursion
Recursive functions must have both:
- one or more base cases
- a recursive case
The base cases are required to ensure the recursion stops when meeting a condition
The recursive case adds functions onto the call stack and completes each one top down.

9
chapter_04/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Quicksort
Similar to the previous recursive function, quicksort uses divide and conquer.
The base case occurs for an array size 0 or 1 (doesn't need to be sorted).
The recursive case works by partitioning the array around a chosen pivot repeatedly until the base case is met and then combining all sorted sub-arrays.
Note. Quicksort should be implemented using a random pivot to ensure average runtimes.

View File

@@ -32,10 +32,14 @@ SAMPLE_SIZE = 1000
numbers = random.sample(range(LOWER, UPPER), SAMPLE_SIZE) numbers = random.sample(range(LOWER, UPPER), SAMPLE_SIZE)
numbers.sort() numbers.sort()
seen = set()
count = 0
result = None result = None
while result is None: while result is None:
guess = random.randrange(LOWER, UPPER) guess = random.randrange(LOWER, UPPER)
logger.debug(f"guess: {guess}") if guess not in seen:
count += 1
seen.add(guess)
result = binary_search(numbers, 0, len(numbers) - 1, guess) result = binary_search(numbers, 0, len(numbers) - 1, guess)
print(f"Found {guess} at index {result}.") print(f"Found {guess} at index {result} after {count} attempts.")

9
chapter_06/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Breadth-First Search
Can tell you if there's a path between A and B and will find the shortest.
In these examples, 1st degree Mango sellers are found before 2nd degree, 2nd before 3rd and so on.
Visted nodes should be stored in a set to ensure no infinite loops.
Running time for BFS on a directed graph: `O(V + E`) where V = vertices, E = edges.

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -2,4 +2,4 @@
- Dijkstra's algorithm works when all weights are non-negative - Dijkstra's algorithm works when all weights are non-negative
- If there are negative weights use Bellman-Ford. - If there are negative weights use Bellman-Ford.
- Priority queue + min heap is optimal when compared to a function that operates on a list. - The book demonstrates a function that operates on a list. Priority queue + min heap added for completeness.

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -20,10 +20,15 @@ def dijkstra(graph, node):
costs[node] = 0 costs[node] = 0
parents = {node: None for node in graph} parents = {node: None for node in graph}
queue = [(0, node)] queue = [(0, node)]
visited = set()
while queue: while queue:
current_cost, current_node = heapq.heappop(queue) current_cost, current_node = heapq.heappop(queue)
if current_node in visited:
continue
logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue") logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue")
visited.add(current_node)
for next_node, weight in graph[current_node].items(): for next_node, weight in graph[current_node].items():
new_cost = current_cost + weight new_cost = current_cost + weight

View File

@@ -19,10 +19,15 @@ def dijkstra(graph, node):
costs[node] = 0 costs[node] = 0
parents = {node: None for node in graph} parents = {node: None for node in graph}
queue = [(0, node)] queue = [(0, node)]
visited = set()
while queue: while queue:
current_cost, current_node = heapq.heappop(queue) current_cost, current_node = heapq.heappop(queue)
if current_node in visited:
continue
logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue") logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue")
visited.add(current_node)
for next_node, weight in graph[current_node].items(): for next_node, weight in graph[current_node].items():
new_cost = current_cost + weight new_cost = current_cost + weight

View File

@@ -19,10 +19,15 @@ def dijkstra(graph, node):
costs[node] = 0 costs[node] = 0
parents = {node: None for node in graph} parents = {node: None for node in graph}
queue = [(0, node)] queue = [(0, node)]
visited = set()
while queue: while queue:
current_cost, current_node = heapq.heappop(queue) current_cost, current_node = heapq.heappop(queue)
if current_node in visited:
continue
logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue") logger.debug(f"node {current_node} with cost {current_cost} popped from pqueue")
visited.add(current_node)
for next_node, weight in graph[current_node].items(): for next_node, weight in graph[current_node].items():
new_cost = current_cost + weight new_cost = current_cost + weight

View File

@@ -3,11 +3,7 @@ from dataclasses import dataclass
import numpy as np import numpy as np
logging.basicConfig( logging.basicConfig(level=logging.DEBUG)
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)