Skip to content

Commit 609459a

Browse files
committed
Algorithms update
1 parent e565dbd commit 609459a

File tree

1 file changed

+104
-2
lines changed

1 file changed

+104
-2
lines changed

docs/python/useful_algorithms.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ tags:
1515
- [Overview](#overview)
1616
- [Merging Overlapping Intervals](#merging-overlapping-intervals)
1717
- [Binary Search](#binary-search)
18+
- [Get Factors](#get-factors)
19+
- [To Base-N](#to-base-n)
20+
- [Timer Decorator](#timer-decorator)
1821

1922
## Overview
2023

21-
Just a set of useful reusable algorithms...
24+
I've written a bunch of algorithms which I find useful and reusable...
2225

2326
## Merging Overlapping Intervals
2427

@@ -75,4 +78,103 @@ def binary_search(target, low:int, high:int, func, *func_args, reverse_search=Fa
7578
low = candidate
7679
else:
7780
high = candidate
78-
```
81+
```
82+
83+
## Get Factors
84+
85+
Here is a function that returns all the factors for a given integer. Note how I'm making use of the `cache` decorator, in order to cache the factors of any integer that has been seen before.
86+
87+
```python
88+
@cache
89+
def get_factors(num: int) -> set[int]:
90+
""" Gets the factors for a given number. Returns a set[int] of factors.
91+
# E.g. when num=8, factors will be 1, 2, 4, 8 """
92+
factors = set()
93+
94+
# Iterate from 1 to sqrt of 8,
95+
# since a larger factor of num must be a multiple of a smaller factor already checked
96+
for i in range(1, int(num**0.5) + 1): # e.g. with num=8, this is range(1, 3)
97+
if num % i == 0: # if it is a factor, then dividing num by it will yield no remainder
98+
factors.add(i) # e.g. 1, 2
99+
factors.add(num//i) # i.e. 8//1 = 8, 8//2 = 4
100+
101+
return factors
102+
```
103+
104+
## To Base-N
105+
106+
This function returns the string representation of an integer, after conversion to any arbitrary base.
107+
108+
```python
109+
def to_base_n(number: int, base: int):
110+
""" Convert any integer number into a base-n string representation of that number.
111+
E.g. to_base_n(38, 5) = 123
112+
113+
Args:
114+
number (int): The number to convert
115+
base (int): The base to apply
116+
117+
Returns:
118+
[str]: The string representation of the number
119+
"""
120+
ret_str = ""
121+
curr_num = number
122+
while curr_num:
123+
ret_str = str(curr_num % base) + ret_str
124+
curr_num //= base
125+
126+
return ret_str if number > 0 else "0"
127+
```
128+
129+
## Timer Decorator
130+
131+
A Python **decorator** is essentially a function that is used to modify or extend the behavior of other functions or methods. It allows for the addition of functionality to an existing piece of code without changing its structure. This is particularly useful for code reusability, separation of concerns, and adhering to the DRY (Don't Repeat Yourself) principle.
132+
133+
In Python, decorators are applied by prefixing a function definition with `@decorator-name`. When a function is decorated, it is passed to the decorator as an argument, and the decorator returns a new function with the enhanced functionality.
134+
135+
I found myself writing this code all the time:
136+
137+
```python
138+
if __name__ == "__main__":
139+
t1 = time.perf_counter()
140+
main()
141+
t2 = time.perf_counter()
142+
print(f"Execution time: {t2 - t1:0.4f} seconds")
143+
```
144+
145+
I figured... this is a great candidate for a decorator! And here it is:
146+
147+
```python
148+
@contextlib.contextmanager
149+
def timer(description="Execution time"):
150+
"""A context manager to measure the time taken by a block of code or function.
151+
152+
Args:
153+
- description (str): A description for the timing output.
154+
Default is "Execution time".
155+
"""
156+
t1 = time.perf_counter()
157+
yield
158+
t2 = time.perf_counter()
159+
logger.info(f"{description}: {t2 - t1:.3f} seconds")
160+
```
161+
162+
It works like this:
163+
164+
- I use an existing decorator - `@contextlib.contextmanager` - to turn my function into a resource that we can invoke using the `with` statement.
165+
- Inside the function:
166+
- `t1` is set to the current time using `time.perf_counter()`.
167+
- The `yield` statement pauses the function, allowing the block of code within the `with` statement to execute. The context manager waits at this point until the block completes its execution.
168+
- After the block inside the `with` statement finishes, execution resumes in the `timer` function. `t2` is set to the current time, again using `time.perf_counter()`.
169+
- The function then calculates the duration the code block took to execute by subtracting `t1` from `t2`. This duration is then logged with the provided description.
170+
171+
So now, I can use the `timer` decorator like this:
172+
173+
```python
174+
import aoc_common.aoc_commons as ac
175+
176+
with ac.timer():
177+
logger.info(f"Part 1 soln={part1(input_data)}")
178+
```
179+
180+
Much better!

0 commit comments

Comments
 (0)