diff --git a/ciphers/atbash.py b/ciphers/atbash.py index 4e8f663ed02d..6930f07861ef 100644 --- a/ciphers/atbash.py +++ b/ciphers/atbash.py @@ -3,52 +3,34 @@ import string -def atbash_slow(sequence: str) -> str: +def atbash(text: str) -> str: """ - >>> atbash_slow("ABCDEFG") - 'ZYXWVUT' + Encodes or decodes text using the Atbash cipher. - >>> atbash_slow("aW;;123BX") - 'zD;;123YC' - """ - output = "" - for i in sequence: - extract = ord(i) - if 65 <= extract <= 90: - output += chr(155 - extract) - elif 97 <= extract <= 122: - output += chr(219 - extract) - else: - output += i - return output - - -def atbash(sequence: str) -> str: - """ - >>> atbash("ABCDEFG") - 'ZYXWVUT' - - >>> atbash("aW;;123BX") - 'zD;;123YC' - """ - letters = string.ascii_letters - letters_reversed = string.ascii_lowercase[::-1] + string.ascii_uppercase[::-1] - return "".join( - letters_reversed[letters.index(c)] if c in letters else c for c in sequence - ) + The Atbash cipher substitutes each letter with its mirror in the alphabet: + A -> Z, B -> Y, C -> X, ... Z -> A (case is preserved) + Non-alphabetic characters are left unchanged. + Args: + text: The input string to encode/decode -def benchmark() -> None: - """Let's benchmark our functions side-by-side...""" - from timeit import timeit + Returns: + The transformed string + """ + # Create translation tables for uppercase and lowercase + lowercase_map = str.maketrans(string.ascii_lowercase, string.ascii_lowercase[::-1]) + uppercase_map = str.maketrans(string.ascii_uppercase, string.ascii_uppercase[::-1]) - print("Running performance benchmarks...") - setup = "from string import printable ; from __main__ import atbash, atbash_slow" - print(f"> atbash_slow(): {timeit('atbash_slow(printable)', setup=setup)} seconds") - print(f"> atbash(): {timeit('atbash(printable)', setup=setup)} seconds") + # Apply both translation mappings + return text.translate(lowercase_map).translate(uppercase_map) +# Example usage if __name__ == "__main__": - for example in ("ABCDEFGH", "123GGjj", "testStringtest", "with space"): - print(f"{example} encrypted in atbash: {atbash(example)}") - benchmark() + test_string = "Hello, World! 123" + encoded = atbash(test_string) + decoded = atbash(encoded) + + print(f"Original: {test_string}") + print(f"Encoded: {encoded}") + print(f"Decoded: {decoded}") diff --git a/ciphers/hill_cipher.py b/ciphers/hill_cipher.py index 33b2529f017b..610b34b88ff7 100644 --- a/ciphers/hill_cipher.py +++ b/ciphers/hill_cipher.py @@ -1,39 +1,21 @@ """ +Hill Cipher Implementation -Hill Cipher: -The 'HillCipher' class below implements the Hill Cipher algorithm which uses -modern linear algebra techniques to encode and decode text using an encryption -key matrix. +This module provides a complete implementation of the Hill Cipher algorithm, +which uses linear algebra techniques for text encryption and decryption. +The cipher supports alphanumeric characters (A-Z, 0-9) and uses modular +arithmetic with base 36. -Algorithm: -Let the order of the encryption key be N (as it is a square matrix). -Your text is divided into batches of length N and converted to numerical vectors -by a simple mapping starting with A=0 and so on. +Classes: + HillCipher: Implements the Hill Cipher encryption and decryption operations. -The key is then multiplied with the newly created batch vector to obtain the -encoded vector. After each multiplication modular 36 calculations are performed -on the vectors so as to bring the numbers between 0 and 36 and then mapped with -their corresponding alphanumerics. - -While decrypting, the decrypting key is found which is the inverse of the -encrypting key modular 36. The same process is repeated for decrypting to get -the original message back. - -Constraints: -The determinant of the encryption key matrix must be relatively prime w.r.t 36. - -Note: -This implementation only considers alphanumerics in the text. If the length of -the text to be encrypted is not a multiple of the break key(the length of one -batch of letters), the last character of the text is added to the text until the -length of the text reaches a multiple of the break_key. So the text after -decrypting might be a little different than the original text. +Functions: + main: Command-line interface for the Hill Cipher operations. References: -https://apprendre-en-ligne.net/crypto/hill/Hillciph.pdf -https://www.youtube.com/watch?v=kfmNeskzs2o -https://www.youtube.com/watch?v=4RhLNDqcjpA - + https://apprendre-en-ligne.net/crypto/hill/Hillciph.pdf + https://www.youtube.com/watch?v=kfmNeskzs2o + https://www.youtube.com/watch?v=4RhLNDqcjpA """ import string @@ -44,71 +26,170 @@ class HillCipher: - key_string = string.ascii_uppercase + string.digits - # This cipher takes alphanumerics into account - # i.e. a total of 36 characters + """ + Implementation of the Hill Cipher algorithm using matrix operations. + + Attributes: + key_string (str): String of valid characters (A-Z and 0-9) + modulus (function): Vectorized function for mod 36 operation + to_int (function): Vectorized rounding function + encrypt_key (np.ndarray): Encryption key matrix + break_key (int): Size of the encryption key matrix (N x N) + + Methods: + replace_letters: Convert character to numerical value + replace_digits: Convert numerical value to character + check_determinant: Validate encryption key determinant + process_text: Prepare text for encryption/decryption + encrypt: Encrypt plaintext using Hill Cipher + make_decrypt_key: Compute decryption key matrix + decrypt: Decrypt ciphertext using Hill Cipher + """ - # take x and return x % len(key_string) + key_string = string.ascii_uppercase + string.digits modulus = np.vectorize(lambda x: x % 36) - to_int = np.vectorize(round) def __init__(self, encrypt_key: np.ndarray) -> None: """ - encrypt_key is an NxN numpy array + Initialize Hill Cipher with encryption key matrix. + + Args: + encrypt_key: Square matrix used for encryption + + Raises: + ValueError: If encryption key is invalid + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.break_key + 2 """ - self.encrypt_key = self.modulus(encrypt_key) # mod36 calc's on the encrypt key - self.check_determinant() # validate the determinant of the encryption key + self.encrypt_key = self.modulus(encrypt_key) + self.check_determinant() self.break_key = encrypt_key.shape[0] def replace_letters(self, letter: str) -> int: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.replace_letters('T') - 19 - >>> hill_cipher.replace_letters('0') - 26 + Convert character to its numerical equivalent. + + Args: + letter: Character to convert (A-Z or 0-9) + + Returns: + Numerical value (0-35) + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.replace_letters('A') + 0 + >>> cipher.replace_letters('Z') + 25 + >>> cipher.replace_letters('0') + 26 + >>> cipher.replace_letters('9') + 35 """ return self.key_string.index(letter) def replace_digits(self, num: int) -> str: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.replace_digits(19) - 'T' - >>> hill_cipher.replace_digits(26) - '0' + Convert numerical value to its character equivalent. + + Args: + num: Numerical value (0-35) + + Returns: + Character equivalent (A-Z or 0-9) + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.replace_digits(0) + 'A' + >>> cipher.replace_digits(25) + 'Z' + >>> cipher.replace_digits(26) + '0' + >>> cipher.replace_digits(35) + '9' """ return self.key_string[round(num)] def check_determinant(self) -> None: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.check_determinant() + Validate encryption key determinant. + + The determinant must be coprime with 36 for the key to be valid. + + Raises: + ValueError: If determinant is not coprime with 36 + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) # Valid key + + >>> invalid_key = np.array([[2, 2], [1, 1]]) + >>> HillCipher(invalid_key) # Determinant 0 + Traceback (most recent call last): + ... + ValueError: determinant modular 36 of encryption key(0) is not co prime + w.r.t 36. Try another key. + + >>> invalid_key2 = np.array([[4, 2], [6, 3]]) + >>> HillCipher(invalid_key2) # Determinant 0 + Traceback (most recent call last): + ... + ValueError: determinant modular 36 of encryption key(0) is not co prime + w.r.t 36. Try another key. """ + det = round(np.linalg.det(self.encrypt_key)) if det < 0: det = det % len(self.key_string) req_l = len(self.key_string) - if greatest_common_divisor(det, len(self.key_string)) != 1: + if greatest_common_divisor(det, req_l) != 1: msg = ( - f"determinant modular {req_l} of encryption key({det}) " - f"is not co prime w.r.t {req_l}.\nTry another key." + f"determinant modular {req_l} of encryption key({det}) is not co prime " + f"w.r.t {req_l}.\nTry another key." ) raise ValueError(msg) def process_text(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.process_text('Testing Hill Cipher') - 'TESTINGHILLCIPHERR' - >>> hill_cipher.process_text('hello') - 'HELLOO' + Prepare text for encryption/decryption by: + 1. Converting to uppercase + 2. Removing invalid characters + 3. Padding with last character to make length multiple of key size + + Args: + text: Text to process + + Returns: + Processed text ready for encryption/decryption + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.process_text('Test!123') + 'TEST123' + >>> cipher.process_text('hello') + 'HELLOO' + >>> cipher.process_text('a') + 'AA' + >>> cipher.process_text('abc') + 'ABCC' """ chars = [char for char in text.upper() if char in self.key_string] + # Handle empty input case + if not chars: + return "" + last = chars[-1] while len(chars) % self.break_key != 0: chars.append(last) @@ -117,22 +198,47 @@ def process_text(self, text: str) -> str: def encrypt(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.encrypt('testing hill cipher') - 'WHXYJOLM9C6XT085LL' - >>> hill_cipher.encrypt('hello') - '85FF00' + Encrypt plaintext using Hill Cipher. + + Args: + text: Plaintext to encrypt + + Returns: + Encrypted ciphertext + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.encrypt('Test') + '4Q6J' + >>> cipher.encrypt('Hello World') + '85FF00V4ZAH8' + >>> cipher.encrypt('ABC') + 'A0K' + >>> cipher.encrypt('123') + '0RZ' + >>> cipher.encrypt('a') + 'PP' """ text = self.process_text(text.upper()) + if not text: + return "" + encrypted = "" for i in range(0, len(text) - self.break_key + 1, self.break_key): + # Extract batch of characters batch = text[i : i + self.break_key] + + # Convert to numerical vector vec = [self.replace_letters(char) for char in batch] batch_vec = np.array([vec]).T - batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[ - 0 - ] + + # Matrix multiplication and mod 36 + product = self.encrypt_key.dot(batch_vec) + batch_encrypted = self.modulus(product).T.tolist()[0] + + # Convert back to characters encrypted_batch = "".join( self.replace_digits(num) for num in batch_encrypted ) @@ -142,44 +248,94 @@ def encrypt(self, text: str) -> str: def make_decrypt_key(self) -> np.ndarray: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.make_decrypt_key() - array([[ 6, 25], - [ 5, 26]]) + Compute decryption key matrix from encryption key. + + Returns: + Decryption key matrix + + Raises: + ValueError: If modular inverse doesn't exist + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.make_decrypt_key() + array([[ 6, 25], + [ 5, 26]]) + + >>> key3x3 = np.array([[1,2,3],[4,5,6],[7,8,9]]) + >>> cipher3 = HillCipher(key3x3) + >>> cipher3.make_decrypt_key() # Determinant 0 should be invalid + Traceback (most recent call last): + ... + ValueError: determinant modular 36 of encryption key(0) is not co prime + w.r.t 36. Try another key. """ + det = round(np.linalg.det(self.encrypt_key)) if det < 0: det = det % len(self.key_string) - det_inv = None + + det_inv: int | None = None for i in range(len(self.key_string)): if (det * i) % len(self.key_string) == 1: det_inv = i break - inv_key = ( - det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key) - ) + if det_inv is None: + raise ValueError("Modular inverse does not exist for decryption key") + det_float = np.linalg.det(self.encrypt_key) + inv_key = det_inv * det_float * np.linalg.inv(self.encrypt_key) return self.to_int(self.modulus(inv_key)) def decrypt(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) - >>> hill_cipher.decrypt('WHXYJOLM9C6XT085LL') - 'TESTINGHILLCIPHERR' - >>> hill_cipher.decrypt('85FF00') - 'HELLOO' + Decrypt ciphertext using Hill Cipher. + + Args: + text: Ciphertext to decrypt + + Returns: + Decrypted plaintext + + Examples: + >>> key = np.array([[2, 5], [1, 6]]) + >>> cipher = HillCipher(key) + >>> cipher.decrypt('4Q6J') + 'TEST' + >>> cipher.decrypt('85FF00V4ZAH8') + 'HELLOWORLDD' + >>> cipher.decrypt('A0K') + 'ABCC' + >>> cipher.decrypt('0RZ') + '1233' + >>> cipher.decrypt('PP') + 'AA' + >>> cipher.decrypt('') + '' """ - decrypt_key = self.make_decrypt_key() text = self.process_text(text.upper()) + if not text: + return "" + + decrypt_key = self.make_decrypt_key() decrypted = "" for i in range(0, len(text) - self.break_key + 1, self.break_key): + # Extract batch of characters batch = text[i : i + self.break_key] + + # Convert to numerical vector vec = [self.replace_letters(char) for char in batch] batch_vec = np.array([vec]).T - batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0] + + # Matrix multiplication and mod 36 + product = decrypt_key.dot(batch_vec) + batch_decrypted = self.modulus(product).T.tolist()[0] + + # Convert back to characters decrypted_batch = "".join( self.replace_digits(num) for num in batch_decrypted ) @@ -189,26 +345,39 @@ def decrypt(self, text: str) -> str: def main() -> None: + """ + Command-line interface for Hill Cipher operations. + + Steps: + 1. User inputs encryption key size + 2. User inputs encryption key matrix rows + 3. User chooses encryption or decryption + 4. User inputs text to process + 5. Program outputs result + """ n = int(input("Enter the order of the encryption key: ")) hill_matrix = [] print("Enter each row of the encryption key with space separated integers") - for _ in range(n): - row = [int(x) for x in input().split()] + for i in range(n): + row = [int(x) for x in input(f"Row {i + 1}: ").split()] hill_matrix.append(row) hc = HillCipher(np.array(hill_matrix)) - print("Would you like to encrypt or decrypt some text? (1 or 2)") - option = input("\n1. Encrypt\n2. Decrypt\n") + print("\nWould you like to encrypt or decrypt some text?") + option = input("1. Encrypt\n2. Decrypt\nEnter choice (1/2): ") + if option == "1": - text_e = input("What text would you like to encrypt?: ") - print("Your encrypted text is:") - print(hc.encrypt(text_e)) + text = input("\nEnter text to encrypt: ") + print("\nEncrypted text:") + print(hc.encrypt(text)) elif option == "2": - text_d = input("What text would you like to decrypt?: ") - print("Your decrypted text is:") - print(hc.decrypt(text_d)) + text = input("\nEnter text to decrypt: ") + print("\nDecrypted text:") + print(hc.decrypt(text)) + else: + print("Invalid option selected") if __name__ == "__main__": @@ -216,4 +385,18 @@ def main() -> None: doctest.testmod() + print("\nRunning sample tests...") + key = np.array([[2, 5], [1, 6]]) + cipher = HillCipher(key) + + # Test encryption/decryption round trip + plaintext = "HELLO123" + encrypted = cipher.encrypt(plaintext) + decrypted = cipher.decrypt(encrypted) + + print(f"\nOriginal text: {plaintext}") + print(f"Encrypted text: {encrypted}") + print(f"Decrypted text: {decrypted}") + + # Run CLI interface main() diff --git a/ciphers/shuffled_shift_cipher.py b/ciphers/shuffled_shift_cipher.py index 08b2cab97c69..09c1c65d0117 100644 --- a/ciphers/shuffled_shift_cipher.py +++ b/ciphers/shuffled_shift_cipher.py @@ -42,7 +42,7 @@ def __str__(self) -> str: """ :return: passcode of the cipher object """ - return "".join(self.__passcode) + return self.__passcode def __neg_pos(self, iterlist: list[int]) -> list[int]: """ @@ -56,19 +56,19 @@ def __neg_pos(self, iterlist: list[int]) -> list[int]: iterlist[i] *= -1 return iterlist - def __passcode_creator(self) -> list[str]: + def __passcode_creator(self) -> str: """ Creates a random password from the selection buffer of 1. uppercase letters of the English alphabet 2. lowercase letters of the English alphabet 3. digits from 0 to 9 - :rtype: list + :rtype: str :return: a password of a random length between 10 to 20 """ choices = string.ascii_letters + string.digits password = [random.choice(choices) for _ in range(random.randint(10, 20))] - return password + return "".join(password) def __make_key_list(self) -> list[str]: """ @@ -104,15 +104,14 @@ def __make_key_list(self) -> list[str]: temp_list: list[str] = [] # algorithm for creating a new shuffled list, keys_l, out of key_list_options - for i in key_list_options: - temp_list.extend(i) + for char in key_list_options: + temp_list.append(char) # checking breakpoints at which to pivot temporary sublist and add it into # keys_l - if i in breakpoints or i == key_list_options[-1]: + if char in breakpoints or char == key_list_options[-1]: keys_l.extend(temp_list[::-1]) temp_list.clear() - # returning a shuffled keys_l to prevent brute force guessing of shift key return keys_l @@ -135,14 +134,13 @@ def decrypt(self, encoded_message: str) -> str: """ decoded_message = "" + key_len = len(self.__key_list) # decoding shift like Caesar cipher algorithm implementing negative shift or # reverse shift or left shift - for i in encoded_message: - position = self.__key_list.index(i) - decoded_message += self.__key_list[ - (position - self.__shift_key) % -len(self.__key_list) - ] + for char in encoded_message: + position = self.__key_list.index(char) + decoded_message += self.__key_list[(position - self.__shift_key) % key_len] return decoded_message @@ -157,14 +155,13 @@ def encrypt(self, plaintext: str) -> str: """ encoded_message = "" + key_len = len(self.__key_list) # encoding shift like Caesar cipher algorithm implementing positive shift or # forward shift or right shift - for i in plaintext: - position = self.__key_list.index(i) - encoded_message += self.__key_list[ - (position + self.__shift_key) % len(self.__key_list) - ] + for char in plaintext: + position = self.__key_list.index(char) + encoded_message += self.__key_list[(position + self.__shift_key) % key_len] return encoded_message diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py index 8558305eefe4..bc1090cf7d97 100644 --- a/data_structures/binary_tree/avl_tree.py +++ b/data_structures/binary_tree/avl_tree.py @@ -8,6 +8,7 @@ from __future__ import annotations +import doctest import math import random from typing import Any @@ -330,8 +331,6 @@ def __str__( def _test() -> None: - import doctest - doctest.testmod() diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py index 3f214d0113a4..1f6baf5aeeb7 100644 --- a/data_structures/binary_tree/binary_search_tree.py +++ b/data_structures/binary_tree/binary_search_tree.py @@ -93,6 +93,7 @@ from collections.abc import Iterable, Iterator from dataclasses import dataclass +from pprint import pformat from typing import Any, Self @@ -115,8 +116,6 @@ def __iter__(self) -> Iterator[int]: yield from self.right or [] def __repr__(self) -> str: - from pprint import pformat - if self.left is None and self.right is None: return str(self.value) return pformat({f"{self.value}": (self.left, self.right)}, indent=1) diff --git a/data_structures/binary_tree/diff_views_of_binary_tree.py b/data_structures/binary_tree/diff_views_of_binary_tree.py index 3198d8065918..450c60a19373 100644 --- a/data_structures/binary_tree/diff_views_of_binary_tree.py +++ b/data_structures/binary_tree/diff_views_of_binary_tree.py @@ -173,7 +173,6 @@ def binary_tree_bottom_side_view(root: TreeNode) -> list[int]: >>> binary_tree_bottom_side_view(None) [] """ - from collections import defaultdict def breadth_first_search(root: TreeNode, bottom_view: list[int]) -> None: """ diff --git a/data_structures/binary_tree/red_black_tree.py b/data_structures/binary_tree/red_black_tree.py index 752db1e7026c..1039be892e3a 100644 --- a/data_structures/binary_tree/red_black_tree.py +++ b/data_structures/binary_tree/red_black_tree.py @@ -1,20 +1,35 @@ from __future__ import annotations +import doctest from collections.abc import Iterator +from pprint import pformat class RedBlackTree: """ A Red-Black tree, which is a self-balancing BST (binary search tree). - This tree has similar performance to AVL trees, but the balancing is - less strict, so it will perform faster for writing/deleting nodes - and slower for reading in the average case, though, because they're - both balanced binary search trees, both will get the same asymptotic - performance. - To read more about them, https://en.wikipedia.org/wiki/Red-black_tree - Unless otherwise specified, all asymptotic runtimes are specified in - terms of the size of the tree. + + Examples: + >>> tree = RedBlackTree(0) + >>> tree = tree.insert(8).insert(-8).insert(4).insert(12) + >>> tree.check_color_properties() + True + >>> list(tree.inorder_traverse()) + [-8, 0, 4, 8, 12] + >>> tree.search(4).label + 4 + >>> tree.floor(5) + 4 + >>> tree.ceil(5) + 8 + >>> tree.get_min() + -8 + >>> tree.get_max() + 12 + >>> tree = tree.remove(4) + >>> 4 in tree + False """ def __init__( @@ -25,12 +40,21 @@ def __init__( left: RedBlackTree | None = None, right: RedBlackTree | None = None, ) -> None: - """Initialize a new Red-Black Tree node with the given values: - label: The value associated with this node - color: 0 if black, 1 if red - parent: The parent to this node - left: This node's left child - right: This node's right child + """Initialize a new Red-Black Tree node. + + Args: + label: The value associated with this node + color: 0 if black, 1 if red + parent: The parent to this node + left: This node's left child + right: This node's right child + + Examples: + >>> node = RedBlackTree(5) + >>> node.label + 5 + >>> node.color + 0 """ self.label = label self.parent = parent @@ -38,12 +62,23 @@ def __init__( self.right = right self.color = color - # Here are functions which are specific to red-black trees - def rotate_left(self) -> RedBlackTree: - """Rotate the subtree rooted at this node to the left and - returns the new root to this subtree. - Performing one rotation can be done in O(1). + """Rotate the subtree rooted at this node to the left. + + Returns: + The new root of the subtree + + Examples: + >>> root = RedBlackTree(2) + >>> root.right = RedBlackTree(4) + >>> root.right.left = RedBlackTree(3) + >>> new_root = root.rotate_left() + >>> new_root.label + 4 + >>> new_root.left.label + 2 + >>> new_root.left.right.label + 3 """ parent = self.parent right = self.right @@ -63,9 +98,22 @@ def rotate_left(self) -> RedBlackTree: return right def rotate_right(self) -> RedBlackTree: - """Rotate the subtree rooted at this node to the right and - returns the new root to this subtree. - Performing one rotation can be done in O(1). + """Rotate the subtree rooted at this node to the right. + + Returns: + The new root of the subtree + + Examples: + >>> root = RedBlackTree(4) + >>> root.left = RedBlackTree(2) + >>> root.left.right = RedBlackTree(3) + >>> new_root = root.rotate_right() + >>> new_root.label + 2 + >>> new_root.right.label + 4 + >>> new_root.right.left.label + 3 """ if self.left is None: return self @@ -85,13 +133,23 @@ def rotate_right(self) -> RedBlackTree: return left def insert(self, label: int) -> RedBlackTree: - """Inserts label into the subtree rooted at self, performs any - rotations necessary to maintain balance, and then returns the - new root to this subtree (likely self). - This is guaranteed to run in O(log(n)) time. + """Insert a label into the tree. + + Args: + label: The value to insert + + Returns: + The root of the tree + + Examples: + >>> tree = RedBlackTree() + >>> tree = tree.insert(5).insert(3).insert(7) + >>> list(tree.inorder_traverse()) + [3, 5, 7] + >>> tree.check_color_properties() + True """ if self.label is None: - # Only possible with an empty tree self.label = label return self if self.label == label: @@ -110,7 +168,7 @@ def insert(self, label: int) -> RedBlackTree: return self.parent or self def _insert_repair(self) -> None: - """Repair the coloring from inserting into a tree.""" + """Repair the coloring after insertion.""" if self.parent is None: # This node is the root, so it just needs to be black self.color = 0 @@ -148,35 +206,39 @@ def _insert_repair(self) -> None: self.grandparent._insert_repair() def remove(self, label: int) -> RedBlackTree: - """Remove label from this tree.""" + """Remove a label from the tree. + + Args: + label: The value to remove + + Returns: + The root of the tree + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> tree = tree.remove(3) + >>> 3 in tree + False + >>> tree.check_color_properties() + True + """ if self.label == label: if self.left and self.right: - # It's easier to balance a node with at most one child, - # so we replace this node with the greatest one less than - # it and remove that. value = self.left.get_max() if value is not None: self.label = value self.left.remove(value) else: - # This node has at most one non-None child, so we don't - # need to replace child = self.left or self.right if self.color == 1: - # This node is red, and its child is black - # The only way this happens to a node with one child - # is if both children are None leaves. - # We can just remove this node and call it a day. if self.parent: if self.is_left(): self.parent.left = None else: self.parent.right = None - # The node is black elif child is None: - # This node and its child are black if self.parent is None: - # The tree is now empty return RedBlackTree(None) else: self._remove_repair() @@ -186,8 +248,6 @@ def remove(self, label: int) -> RedBlackTree: self.parent.right = None self.parent = None else: - # This node is black and its child is red - # Move the child node here and make it black self.label = child.label self.left = child.left self.right = child.right @@ -203,7 +263,7 @@ def remove(self, label: int) -> RedBlackTree: return self.parent or self def _remove_repair(self) -> None: - """Repair the coloring of the tree that may have been messed up.""" + """Repair the coloring after removal.""" if ( self.parent is None or self.sibling is None @@ -276,42 +336,28 @@ def _remove_repair(self) -> None: self.parent.sibling.color = 0 def check_color_properties(self) -> bool: - """Check the coloring of the tree, and return True iff the tree - is colored in a way which matches these five properties: - (wording stolen from wikipedia article) - 1. Each node is either red or black. - 2. The root node is black. - 3. All leaves are black. - 4. If a node is red, then both its children are black. - 5. Every path from any node to all of its descendent NIL nodes - has the same number of black nodes. - This function runs in O(n) time, because properties 4 and 5 take - that long to check. """ - # I assume property 1 to hold because there is nothing that can - # make the color be anything other than 0 or 1. - # Property 2 - if self.color: - # The root was red - print("Property 2") + Verify that all Red-Black Tree properties are satisfied: + 1. Root node is black + 2. No two consecutive red nodes + 3. All paths have same black height + + Returns: + True if all properties are satisfied, False otherwise + """ + # Property 1: Root must be black + if self.parent is None and self.color != 0: return False - # Property 3 does not need to be checked, because None is assumed - # to be black and is all the leaves. - # Property 4 + + # Property 2: No two consecutive red nodes if not self.check_coloring(): - print("Property 4") return False - # Property 5 - if self.black_height() is None: - print("Property 5") - return False - # All properties were met - return True + + # Property 3: All paths have same black height + return self.black_height() is not None def check_coloring(self) -> bool: - """A helper function to recursively check Property 4 of a - Red-Black Tree. See check_color_properties for more info. - """ + """Check if the tree satisfies Red-Black property 4.""" if self.color == 1 and 1 in (color(self.left), color(self.right)): return False if self.left and not self.left.check_coloring(): @@ -319,38 +365,65 @@ def check_coloring(self) -> bool: return not (self.right and not self.right.check_coloring()) def black_height(self) -> int | None: - """Returns the number of black nodes from this node to the - leaves of the tree, or None if there isn't one such value (the - tree is color incorrectly). """ - if self is None or self.left is None or self.right is None: - # If we're already at a leaf, there is no path - return 1 - left = RedBlackTree.black_height(self.left) - right = RedBlackTree.black_height(self.right) - if left is None or right is None: - # There are issues with coloring below children nodes - return None - if left != right: - # The two children have unequal depths + Calculate the black height of the tree and verify consistency + - Black height = number of black nodes from current node to any leaf + - Returns None if any path has different black height + + Returns: + Black height if consistent, None otherwise + """ + # Leaf node case (both children are None) + if self.left is None and self.right is None: + # Count: current node (if black) + leaf (black) + return 1 + (1 - self.color) # 2 if black, 1 if red + + # Get black heights from both subtrees + left_bh = self.left.black_height() if self.left else 1 + right_bh = self.right.black_height() if self.right else 1 + + # Validate consistency + if left_bh is None or right_bh is None or left_bh != right_bh: return None - # Return the black depth of children, plus one if this node is - # black - return left + (1 - self.color) - # Here are functions which are general to all binary search trees + # Add current node's contribution (1 if black, 0 if red) + return left_bh + (1 - self.color) def __contains__(self, label: int) -> bool: - """Search through the tree for label, returning True iff it is - found somewhere in the tree. - Guaranteed to run in O(log(n)) time. + """Check if the tree contains a label. + + Args: + label: The value to check + + Returns: + True if the label is in the tree, False otherwise + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3) + >>> 3 in tree + True + >>> 4 in tree + False """ return self.search(label) is not None def search(self, label: int) -> RedBlackTree | None: - """Search through the tree for label, returning its node if - it's found, and None otherwise. - This method is guaranteed to run in O(log(n)) time. + """Search for a label in the tree. + + Args: + label: The value to search for + + Returns: + The node containing the label, or None if not found + + Examples: + >>> tree = RedBlackTree(5) + >>> node = tree.search(5) + >>> node.label + 5 + >>> tree.search(10) is None + True """ if self.label == label: return self @@ -365,8 +438,22 @@ def search(self, label: int) -> RedBlackTree | None: return self.left.search(label) def floor(self, label: int) -> int | None: - """Returns the largest element in this tree which is at most label. - This method is guaranteed to run in O(log(n)) time.""" + """Find the largest element <= label. + + Args: + label: The value to find the floor of + + Returns: + The floor value, or None if no such element exists + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> tree.floor(6) + 5 + >>> tree.floor(2) is None + True + """ if self.label == label: return self.label elif self.label is not None and self.label > label: @@ -382,8 +469,21 @@ def floor(self, label: int) -> int | None: return self.label def ceil(self, label: int) -> int | None: - """Returns the smallest element in this tree which is at least label. - This method is guaranteed to run in O(log(n)) time. + """Find the smallest element >= label. + + Args: + label: The value to find the ceil of + + Returns: + The ceil value, or None if no such element exists + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> tree.ceil(6) + 7 + >>> tree.ceil(8) is None + True """ if self.label == label: return self.label @@ -400,28 +500,42 @@ def ceil(self, label: int) -> int | None: return self.label def get_max(self) -> int | None: - """Returns the largest element in this tree. - This method is guaranteed to run in O(log(n)) time. + """Get the maximum element in the tree. + + Returns: + The maximum value, or None if the tree is empty + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> tree.get_max() + 7 """ if self.right: - # Go as far right as possible return self.right.get_max() else: return self.label def get_min(self) -> int | None: - """Returns the smallest element in this tree. - This method is guaranteed to run in O(log(n)) time. + """Get the minimum element in the tree. + + Returns: + The minimum value, or None if the tree is empty + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> tree.get_min() + 3 """ if self.left: - # Go as far left as possible return self.left.get_min() else: return self.label @property def grandparent(self) -> RedBlackTree | None: - """Get the current node's grandparent, or None if it doesn't exist.""" + """Get the grandparent of this node.""" if self.parent is None: return None else: @@ -429,7 +543,7 @@ def grandparent(self) -> RedBlackTree | None: @property def sibling(self) -> RedBlackTree | None: - """Get the current node's sibling, or None if it doesn't exist.""" + """Get the sibling of this node.""" if self.parent is None: return None elif self.parent.left is self: @@ -438,23 +552,29 @@ def sibling(self) -> RedBlackTree | None: return self.parent.left def is_left(self) -> bool: - """Returns true iff this node is the left child of its parent.""" + """Check if this node is the left child of its parent.""" if self.parent is None: return False return self.parent.left is self def is_right(self) -> bool: - """Returns true iff this node is the right child of its parent.""" + """Check if this node is the right child of its parent.""" if self.parent is None: return False return self.parent.right is self def __bool__(self) -> bool: + """Return True if the tree is not empty.""" return True def __len__(self) -> int: - """ - Return the number of nodes in this tree. + """Return the number of nodes in the tree. + + Examples: + >>> tree = RedBlackTree(5) + >>> tree = tree.insert(3).insert(7) + >>> len(tree) + 3 """ ln = 1 if self.left: @@ -464,6 +584,18 @@ def __len__(self) -> int: return ln def preorder_traverse(self) -> Iterator[int | None]: + """Traverse the tree in pre-order. + + Yields: + The values in pre-order + + Examples: + >>> tree = RedBlackTree(2) + >>> tree.left = RedBlackTree(1) + >>> tree.right = RedBlackTree(3) + >>> list(tree.preorder_traverse()) + [2, 1, 3] + """ yield self.label if self.left: yield from self.left.preorder_traverse() @@ -471,6 +603,18 @@ def preorder_traverse(self) -> Iterator[int | None]: yield from self.right.preorder_traverse() def inorder_traverse(self) -> Iterator[int | None]: + """Traverse the tree in in-order. + + Yields: + The values in in-order + + Examples: + >>> tree = RedBlackTree(2) + >>> tree.left = RedBlackTree(1) + >>> tree.right = RedBlackTree(3) + >>> list(tree.inorder_traverse()) + [1, 2, 3] + """ if self.left: yield from self.left.inorder_traverse() yield self.label @@ -478,6 +622,18 @@ def inorder_traverse(self) -> Iterator[int | None]: yield from self.right.inorder_traverse() def postorder_traverse(self) -> Iterator[int | None]: + """Traverse the tree in post-order. + + Yields: + The values in post-order + + Examples: + >>> tree = RedBlackTree(2) + >>> tree.left = RedBlackTree(1) + >>> tree.right = RedBlackTree(3) + >>> list(tree.postorder_traverse()) + [1, 3, 2] + """ if self.left: yield from self.left.postorder_traverse() if self.right: @@ -485,8 +641,7 @@ def postorder_traverse(self) -> Iterator[int | None]: yield self.label def __repr__(self) -> str: - from pprint import pformat - + """Return a string representation of the tree.""" if self.left is None and self.right is None: return f"'{self.label} {(self.color and 'red') or 'blk'}'" return pformat( @@ -508,6 +663,10 @@ def __eq__(self, other: object) -> bool: else: return False + def __hash__(self): + """Return a hash value for the node.""" + return hash((self.label, self.color)) + def color(node: RedBlackTree | None) -> int: """Returns the color of a node, allowing for None leaves.""" @@ -525,7 +684,6 @@ def color(node: RedBlackTree | None) -> int: def test_rotations() -> bool: """Test that the rotate_left and rotate_right functions work.""" - # Make a tree to test on tree = RedBlackTree(0) tree.left = RedBlackTree(-10, parent=tree) tree.right = RedBlackTree(10, parent=tree) @@ -533,7 +691,6 @@ def test_rotations() -> bool: tree.left.right = RedBlackTree(-5, parent=tree.left) tree.right.left = RedBlackTree(5, parent=tree.right) tree.right.right = RedBlackTree(20, parent=tree.right) - # Make the right rotation left_rot = RedBlackTree(10) left_rot.left = RedBlackTree(0, parent=left_rot) left_rot.left.left = RedBlackTree(-10, parent=left_rot.left) @@ -546,7 +703,6 @@ def test_rotations() -> bool: return False tree = tree.rotate_right() tree = tree.rotate_right() - # Make the left rotation right_rot = RedBlackTree(-10) right_rot.left = RedBlackTree(-20, parent=right_rot) right_rot.right = RedBlackTree(0, parent=right_rot) @@ -598,16 +754,12 @@ def test_insert_and_search() -> bool: tree.insert(10) tree.insert(11) if any(i in tree for i in (5, -6, -10, 13)): - # Found something not in there return False - # Find all these things in there return all(i in tree for i in (11, 12, -8, 0)) def test_insert_delete() -> bool: - """Test the insert() and delete() method of the tree, verifying the - insertion and removal of elements, and the balancing of the tree. - """ + """Test the insert() and delete() method of the tree.""" tree = RedBlackTree(0) tree = tree.insert(-12) tree = tree.insert(8) @@ -699,13 +851,20 @@ def main() -> None: """ >>> pytests() """ + + failures, _ = doctest.testmod() + if failures == 0: + print("All doctests passed!") + else: + print(f"{failures} doctests failed!") + print_results("Rotating right and left", test_rotations()) print_results("Inserting", test_insert()) print_results("Searching", test_insert_and_search()) print_results("Deleting", test_insert_delete()) print_results("Floor and ceil", test_floor_ceil()) print_results("Tree traversal", test_tree_traversal()) - print_results("Tree traversal", test_tree_chaining()) + print_results("Tree chaining", test_tree_chaining()) print("Testing tree balancing...") print("This should only be a few seconds.") test_insertion_speed() diff --git a/data_structures/binary_tree/treap.py b/data_structures/binary_tree/treap.py index 3114c6fa1c26..7755188d1524 100644 --- a/data_structures/binary_tree/treap.py +++ b/data_structures/binary_tree/treap.py @@ -1,5 +1,7 @@ from __future__ import annotations +import doctest +from pprint import pformat from random import random @@ -16,8 +18,6 @@ def __init__(self, value: int | None = None): self.right: Node | None = None def __repr__(self) -> str: - from pprint import pformat - if self.left is None and self.right is None: return f"'{self.value}: {self.prior:.5}'" else: @@ -173,7 +173,5 @@ def main() -> None: if __name__ == "__main__": - import doctest - doctest.testmod() main() diff --git a/data_structures/heap/skew_heap.py b/data_structures/heap/skew_heap.py index 0839db711cb1..dc0efbf1d2bb 100644 --- a/data_structures/heap/skew_heap.py +++ b/data_structures/heap/skew_heap.py @@ -2,25 +2,23 @@ from __future__ import annotations -from collections.abc import Iterable, Iterator -from typing import Any, Generic, TypeVar +from collections.abc import Callable, Iterable, Iterator +from typing import Any -T = TypeVar("T", bound=bool) - -class SkewNode(Generic[T]): +class SkewNode: """ One node of the skew heap. Contains the value and references to two children. """ - def __init__(self, value: T) -> None: - self._value: T = value - self.left: SkewNode[T] | None = None - self.right: SkewNode[T] | None = None + def __init__(self, value: Any) -> None: + self._value: Any = value + self.left: SkewNode | None = None + self.right: SkewNode | None = None @property - def value(self) -> T: + def value(self) -> Any: """ Return the value of the node. @@ -30,68 +28,56 @@ def value(self) -> T: 3.14159 >>> SkewNode("hello").value 'hello' - >>> SkewNode(None).value - >>> SkewNode(True).value True - >>> SkewNode([]).value - [] - >>> SkewNode({}).value - {} - >>> SkewNode(set()).value - set() - >>> SkewNode(0.0).value - 0.0 - >>> SkewNode(-1e-10).value - -1e-10 >>> SkewNode(10).value 10 - >>> SkewNode(-10.5).value - -10.5 - >>> SkewNode().value - Traceback (most recent call last): - ... - TypeError: SkewNode.__init__() missing 1 required positional argument: 'value' """ return self._value @staticmethod def merge( - root1: SkewNode[T] | None, root2: SkewNode[T] | None - ) -> SkewNode[T] | None: + root1: SkewNode | None, root2: SkewNode | None, comp: Callable[[Any, Any], bool] + ) -> SkewNode | None: """ - Merge 2 nodes together. - >>> SkewNode.merge(SkewNode(10),SkewNode(-10.5)).value + Merge two nodes together. + >>> def comp(a, b): return a < b + >>> SkewNode.merge(SkewNode(10), SkewNode(-10.5), comp).value -10.5 - >>> SkewNode.merge(SkewNode(10),SkewNode(10.5)).value + >>> SkewNode.merge(SkewNode(10), SkewNode(10.5), comp).value 10 - >>> SkewNode.merge(SkewNode(10),SkewNode(10)).value + >>> SkewNode.merge(SkewNode(10), SkewNode(10), comp).value 10 - >>> SkewNode.merge(SkewNode(-100),SkewNode(-10.5)).value + >>> SkewNode.merge(SkewNode(-100), SkewNode(-10.5), comp).value -100 """ + # Handle empty nodes if not root1: return root2 - if not root2: return root1 - if root1.value > root2.value: - root1, root2 = root2, root1 - - result = root1 - temp = root1.right - result.right = root1.left - result.left = SkewNode.merge(temp, root2) - - return result - - -class SkewHeap(Generic[T]): + # Compare values using provided comparison function + if comp(root1.value, root2.value): + # root1 is smaller, make it the new root + result = root1 + temp = root1.right + result.right = root1.left + result.left = SkewNode.merge(temp, root2, comp) + return result + else: + # root2 is smaller or equal, use it as new root + result = root2 + temp = root2.right + result.right = root2.left + result.left = SkewNode.merge(root1, temp, comp) + return result + + +class SkewHeap: """ - A data structure that allows inserting a new value and to pop the smallest - values. Both operations take O(logN) time where N is the size of the - structure. + A data structure that allows inserting a new value and popping the smallest + values. Both operations take O(logN) time where N is the size of the heap. Wiki: https://en.wikipedia.org/wiki/Skew_heap Visualization: https://www.cs.usfca.edu/~galles/visualization/SkewHeap.html @@ -111,20 +97,32 @@ class SkewHeap(Generic[T]): [-1, 0, 1] """ - def __init__(self, data: Iterable[T] | None = ()) -> None: + def __init__( + self, + data: Iterable[Any] | None = None, + comp: Callable[[Any, Any], bool] = lambda a, b: a < b, + ) -> None: """ + Initialize the skew heap with optional data and comparison function + >>> sh = SkewHeap([3, 1, 3, 7]) >>> list(sh) [1, 3, 3, 7] + + # Max-heap example + >>> max_heap = SkewHeap([3, 1, 3, 7], comp=lambda a, b: a > b) + >>> list(max_heap) + [7, 3, 3, 1] """ - self._root: SkewNode[T] | None = None + self._root: SkewNode | None = None + self._comp = comp if data: for item in data: self.insert(item) def __bool__(self) -> bool: """ - Check if the heap is not empty. + Check if the heap is not empty >>> sh = SkewHeap() >>> bool(sh) @@ -138,27 +136,31 @@ def __bool__(self) -> bool: """ return self._root is not None - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[Any]: """ - Returns sorted list containing all the values in the heap. + Iterate through all values in sorted order >>> sh = SkewHeap([3, 1, 3, 7]) >>> list(sh) [1, 3, 3, 7] """ + # Create a temporary heap for iteration + temp_heap = SkewHeap(comp=self._comp) result: list[Any] = [] - while self: - result.append(self.pop()) - # Pushing items back to the heap not to clear it. - for item in result: - self.insert(item) + # Pop all elements from the heap + while self: + item = self.pop() + result.append(item) + temp_heap.insert(item) + # Restore the heap state + self._root = temp_heap._root return iter(result) - def insert(self, value: T) -> None: + def insert(self, value: Any) -> None: """ - Insert the value into the heap. + Insert a new value into the heap >>> sh = SkewHeap() >>> sh.insert(3) @@ -168,11 +170,11 @@ def insert(self, value: T) -> None: >>> list(sh) [1, 3, 3, 7] """ - self._root = SkewNode.merge(self._root, SkewNode(value)) + self._root = SkewNode.merge(self._root, SkewNode(value), self._comp) - def pop(self) -> T | None: + def pop(self) -> Any: """ - Pop the smallest value from the heap and return it. + Remove and return the smallest value from the heap >>> sh = SkewHeap([3, 1, 3, 7]) >>> sh.pop() @@ -189,15 +191,13 @@ def pop(self) -> T | None: IndexError: Can't get top element for the empty heap. """ result = self.top() - self._root = ( - SkewNode.merge(self._root.left, self._root.right) if self._root else None - ) - + if self._root: + self._root = SkewNode.merge(self._root.left, self._root.right, self._comp) return result - def top(self) -> T: + def top(self) -> Any: """ - Return the smallest value from the heap. + Return the smallest value without removing it >>> sh = SkewHeap() >>> sh.insert(3) @@ -219,7 +219,7 @@ def top(self) -> T: def clear(self) -> None: """ - Clear the heap. + Clear all elements from the heap >>> sh = SkewHeap([3, 1, 3, 7]) >>> sh.clear() diff --git a/data_structures/stacks/stack_with_doubly_linked_list.py b/data_structures/stacks/stack_with_doubly_linked_list.py index 50c5236e073c..8b472293721f 100644 --- a/data_structures/stacks/stack_with_doubly_linked_list.py +++ b/data_structures/stacks/stack_with_doubly_linked_list.py @@ -3,19 +3,15 @@ from __future__ import annotations -from typing import Generic, TypeVar -T = TypeVar("T") - - -class Node(Generic[T]): +class Node[T]: def __init__(self, data: T): self.data = data # Assign data self.next: Node[T] | None = None # Initialize next as null self.prev: Node[T] | None = None # Initialize prev as null -class Stack(Generic[T]): +class Stack[T]: """ >>> stack = Stack() >>> stack.is_empty() diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py index d1200f4d65ca..8543021f2eb2 100644 --- a/digital_image_processing/test_digital_image_processing.py +++ b/digital_image_processing/test_digital_image_processing.py @@ -2,9 +2,10 @@ PyTest's for Digital Image Processing """ +import os + import numpy as np from cv2 import COLOR_BGR2GRAY, cvtColor, imread -from numpy import array, uint8 from PIL import Image from digital_image_processing import change_contrast as cc @@ -23,112 +24,103 @@ gray = cvtColor(img, COLOR_BGR2GRAY) -# Test: convert_to_negative() def test_convert_to_negative(): + """Test negative image conversion.""" negative_img = cn.convert_to_negative(img) - # assert negative_img array for at least one True + # Verify output contains at least one non-zero value assert negative_img.any() -# Test: change_contrast() def test_change_contrast(): - with Image.open("digital_image_processing/image_data/lena_small.jpg") as img: - # Work around assertion for response - assert str(cc.change_contrast(img, 110)).startswith( + """Test contrast adjustment functionality.""" + with Image.open("digital_image_processing/image_data/lena_small.jpg") as img_pil: + # Verify returns a PIL Image object + assert str(cc.change_contrast(img_pil, 110)).startswith( " str: if __name__ == "__main__": n = int(input("Enter the size of the butterfly pattern: ")) print(butterfly_pattern(n)) + +if __name__ == "__main__": + import doctest + + # Run the doctests + doctest.testmod() diff --git a/graphs/minimum_spanning_tree_kruskal2.py b/graphs/minimum_spanning_tree_kruskal2.py index 0ddb43ce8e6e..36ca1cbb2615 100644 --- a/graphs/minimum_spanning_tree_kruskal2.py +++ b/graphs/minimum_spanning_tree_kruskal2.py @@ -1,11 +1,7 @@ from __future__ import annotations -from typing import Generic, TypeVar -T = TypeVar("T") - - -class DisjointSetTreeNode(Generic[T]): +class DisjointSetTreeNode[T]: # Disjoint Set Node to store the parent and rank def __init__(self, data: T) -> None: self.data = data @@ -13,7 +9,7 @@ def __init__(self, data: T) -> None: self.rank = 0 -class DisjointSetTree(Generic[T]): +class DisjointSetTree[T]: # Disjoint Set DataStructure def __init__(self) -> None: # map from node name to the node object @@ -46,7 +42,7 @@ def union(self, data1: T, data2: T) -> None: self.link(self.find_set(data1), self.find_set(data2)) -class GraphUndirectedWeighted(Generic[T]): +class GraphUndirectedWeighted[T]: def __init__(self) -> None: # connections: map from the node to the neighbouring nodes (with weights) self.connections: dict[T, dict[T, int]] = {} @@ -118,4 +114,5 @@ def kruskal(self) -> GraphUndirectedWeighted[T]: num_edges += 1 graph.add_edge(u, v, w) disjoint_set.union(u, v) + # Return the generated Minimum Spanning Tree return graph diff --git a/graphs/minimum_spanning_tree_prims2.py b/graphs/minimum_spanning_tree_prims2.py index 6870cc80f844..d961b5e764c3 100644 --- a/graphs/minimum_spanning_tree_prims2.py +++ b/graphs/minimum_spanning_tree_prims2.py @@ -10,9 +10,6 @@ from __future__ import annotations from sys import maxsize -from typing import Generic, TypeVar - -T = TypeVar("T") def get_parent_position(position: int) -> int: @@ -47,7 +44,7 @@ def get_child_right_position(position: int) -> int: return (2 * position) + 2 -class MinPriorityQueue(Generic[T]): +class MinPriorityQueue[T]: """ Minimum Priority Queue Class @@ -184,7 +181,7 @@ def _swap_nodes(self, node1_pos: int, node2_pos: int) -> None: self.position_map[node2_elem] = node1_pos -class GraphUndirectedWeighted(Generic[T]): +class GraphUndirectedWeighted[T]: """ Graph Undirected Weighted Class @@ -217,7 +214,7 @@ def add_edge(self, node1: T, node2: T, weight: int) -> None: self.connections[node2][node1] = weight -def prims_algo( +def prims_algo[T]( graph: GraphUndirectedWeighted[T], ) -> tuple[dict[T, int], dict[T, T | None]]: """ @@ -248,7 +245,6 @@ def prims_algo( if priority_queue.is_empty(): return dist, parent - # initialization node = priority_queue.extract_min() dist[node] = 0 diff --git a/hashes/sha256.py b/hashes/sha256.py index bcc83edca480..6acd23ef0489 100644 --- a/hashes/sha256.py +++ b/hashes/sha256.py @@ -16,6 +16,8 @@ """ import argparse +import doctest +import hashlib import struct import unittest @@ -200,8 +202,6 @@ class SHA256HashTest(unittest.TestCase): """ def test_match_hashes(self) -> None: - import hashlib - msg = bytes("Test String", "utf-8") assert SHA256(msg).hash == hashlib.sha256(msg).hexdigest() @@ -214,8 +214,6 @@ def main() -> None: # unittest.main() - import doctest - doctest.testmod() parser = argparse.ArgumentParser() diff --git a/machine_learning/local_weighted_learning/local_weighted_learning.py b/machine_learning/local_weighted_learning/local_weighted_learning.py index f3056da40e24..3f3a4bd0dcf5 100644 --- a/machine_learning/local_weighted_learning/local_weighted_learning.py +++ b/machine_learning/local_weighted_learning/local_weighted_learning.py @@ -33,6 +33,7 @@ import matplotlib.pyplot as plt import numpy as np +import seaborn as sns def weight_matrix(point: np.ndarray, x_train: np.ndarray, tau: float) -> np.ndarray: @@ -134,7 +135,6 @@ def load_data( Load data from seaborn and split it into x and y points >>> pass # No doctests, function is for demo purposes only """ - import seaborn as sns data = sns.load_dataset(dataset_name) x_data = np.array(data[x_name]) diff --git a/machine_learning/mfcc.py b/machine_learning/mfcc.py index dcc3151d5a1a..7c4108e4d37c 100644 --- a/machine_learning/mfcc.py +++ b/machine_learning/mfcc.py @@ -61,6 +61,7 @@ import numpy as np import scipy.fftpack as fft +from scipy.io import wavfile from scipy.signal import get_window logging.basicConfig(filename=f"{__file__}.log", level=logging.INFO) @@ -464,7 +465,6 @@ def example(wav_file_path: str = "./path-to-file/sample.wav") -> np.ndarray: Returns: np.ndarray: The computed MFCCs for the audio. """ - from scipy.io import wavfile # Load the audio from the WAV file sample_rate, audio = wavfile.read(wav_file_path) diff --git a/maths/sum_of_digits.py b/maths/sum_of_digits.py index d5488bb9e9e0..ee5c51959619 100644 --- a/maths/sum_of_digits.py +++ b/maths/sum_of_digits.py @@ -1,3 +1,7 @@ +from collections.abc import Callable +from timeit import timeit + + def sum_of_digits(n: int) -> int: """ Find the sum of digits of a number. @@ -31,7 +35,7 @@ def sum_of_digits_recursion(n: int) -> int: 0 """ n = abs(n) - return n if n < 10 else n % 10 + sum_of_digits(n // 10) + return n if n < 10 else n % 10 + sum_of_digits_recursion(n // 10) def sum_of_digits_compact(n: int) -> int: @@ -53,8 +57,6 @@ def benchmark() -> None: """ Benchmark multiple functions, with three different length int values. """ - from collections.abc import Callable - from timeit import timeit def benchmark_a_function(func: Callable, value: int) -> None: call = f"{func.__name__}({value})" diff --git a/matrix/matrix_class.py b/matrix/matrix_class.py index a5940a38e836..394e38c164d6 100644 --- a/matrix/matrix_class.py +++ b/matrix/matrix_class.py @@ -2,118 +2,29 @@ from __future__ import annotations +from typing import final + +@final class Matrix: """ Matrix object generated from a 2D array where each element is an array representing - a row. - Rows can contain type int or float. - Common operations and information available. - >>> rows = [ - ... [1, 2, 3], - ... [4, 5, 6], - ... [7, 8, 9] - ... ] - >>> matrix = Matrix(rows) - >>> print(matrix) - [[1. 2. 3.] - [4. 5. 6.] - [7. 8. 9.]] - - Matrix rows and columns are available as 2D arrays - >>> matrix.rows - [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - >>> matrix.columns() - [[1, 4, 7], [2, 5, 8], [3, 6, 9]] - - Order is returned as a tuple - >>> matrix.order - (3, 3) - - Squareness and invertability are represented as bool - >>> matrix.is_square - True - >>> matrix.is_invertable() - False - - Identity, Minors, Cofactors and Adjugate are returned as Matrices. Inverse can be - a Matrix or Nonetype - >>> print(matrix.identity()) - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]] - >>> print(matrix.minors()) - [[-3. -6. -3.] - [-6. -12. -6.] - [-3. -6. -3.]] - >>> print(matrix.cofactors()) - [[-3. 6. -3.] - [6. -12. 6.] - [-3. 6. -3.]] - >>> # won't be apparent due to the nature of the cofactor matrix - >>> print(matrix.adjugate()) - [[-3. 6. -3.] - [6. -12. 6.] - [-3. 6. -3.]] - >>> matrix.inverse() - Traceback (most recent call last): - ... - TypeError: Only matrices with a non-zero determinant have an inverse - - Determinant is an int, float, or Nonetype - >>> matrix.determinant() - 0 - - Negation, scalar multiplication, addition, subtraction, multiplication and - exponentiation are available and all return a Matrix - >>> print(-matrix) - [[-1. -2. -3.] - [-4. -5. -6.] - [-7. -8. -9.]] - >>> matrix2 = matrix * 3 - >>> print(matrix2) - [[3. 6. 9.] - [12. 15. 18.] - [21. 24. 27.]] - >>> print(matrix + matrix2) - [[4. 8. 12.] - [16. 20. 24.] - [28. 32. 36.]] - >>> print(matrix - matrix2) - [[-2. -4. -6.] - [-8. -10. -12.] - [-14. -16. -18.]] - >>> print(matrix ** 3) - [[468. 576. 684.] - [1062. 1305. 1548.] - [1656. 2034. 2412.]] - - Matrices can also be modified - >>> matrix.add_row([10, 11, 12]) - >>> print(matrix) - [[1. 2. 3.] - [4. 5. 6.] - [7. 8. 9.] - [10. 11. 12.]] - >>> matrix2.add_column([8, 16, 32]) - >>> print(matrix2) - [[3. 6. 9. 8.] - [12. 15. 18. 16.] - [21. 24. 27. 32.]] - >>> print(matrix * matrix2) - [[90. 108. 126. 136.] - [198. 243. 288. 304.] - [306. 378. 450. 472.] - [414. 513. 612. 640.]] + a row. Supports both integer and float values. """ - def __init__(self, rows: list[list[int]]): + def __init__(self, rows: list[list[float]]) -> None: + """ + Initialize matrix from 2D list. Validates input structure and types. + Raises TypeError for invalid input structure or element types. + """ error = TypeError( "Matrices must be formed from a list of zero or more lists containing at " "least one and the same number of values, each of which must be of type " "int or float." ) - if len(rows) != 0: + + # Validate matrix structure and content + if rows: cols = len(rows[0]) if cols == 0: raise error @@ -127,55 +38,66 @@ def __init__(self, rows: list[list[int]]): else: self.rows = [] - # MATRIX INFORMATION - def columns(self) -> list[list[int]]: + # MATRIX INFORMATION METHODS + def columns(self) -> list[list[float]]: + """Return matrix columns as 2D list""" return [[row[i] for row in self.rows] for i in range(len(self.rows[0]))] @property def num_rows(self) -> int: + """Get number of rows in matrix""" return len(self.rows) @property def num_columns(self) -> int: + """Get number of columns in matrix""" return len(self.rows[0]) @property def order(self) -> tuple[int, int]: + """Get matrix dimensions as (rows, columns) tuple""" return self.num_rows, self.num_columns @property def is_square(self) -> bool: + """Check if matrix is square (rows == columns)""" return self.order[0] == self.order[1] def identity(self) -> Matrix: + """Generate identity matrix of same dimensions""" values = [ - [0 if column_num != row_num else 1 for column_num in range(self.num_rows)] + [ + 0.0 if column_num != row_num else 1.0 + for column_num in range(self.num_rows) + ] for row_num in range(self.num_rows) ] return Matrix(values) - def determinant(self) -> int: + def determinant(self) -> float: + """Calculate matrix determinant. Returns 0 for non-square matrices.""" if not self.is_square: - return 0 + return 0.0 if self.order == (0, 0): - return 1 + return 1.0 if self.order == (1, 1): - return int(self.rows[0][0]) + return float(self.rows[0][0]) if self.order == (2, 2): - return int( + return float( (self.rows[0][0] * self.rows[1][1]) - (self.rows[0][1] * self.rows[1][0]) ) - else: - return sum( - self.rows[0][column] * self.cofactors().rows[0][column] - for column in range(self.num_columns) - ) + return sum( + self.rows[0][column] * self.cofactors().rows[0][column] + for column in range(self.num_columns) + ) def is_invertable(self) -> bool: + """Check if matrix is invertible (non-zero determinant)""" return bool(self.determinant()) - def get_minor(self, row: int, column: int) -> int: + def get_minor(self, row: int, column: int) -> float: + """Calculate minor for specified element (determinant of submatrix)""" values = [ [ self.rows[other_row][other_column] @@ -187,12 +109,12 @@ def get_minor(self, row: int, column: int) -> int: ] return Matrix(values).determinant() - def get_cofactor(self, row: int, column: int) -> int: - if (row + column) % 2 == 0: - return self.get_minor(row, column) - return -1 * self.get_minor(row, column) + def get_cofactor(self, row: int, column: int) -> float: + """Calculate cofactor for specified element (signed minor)""" + return self.get_minor(row, column) * (-1 if (row + column) % 2 else 1) def minors(self) -> Matrix: + """Generate matrix of minors""" return Matrix( [ [self.get_minor(row, column) for column in range(self.num_columns)] @@ -201,103 +123,109 @@ def minors(self) -> Matrix: ) def cofactors(self) -> Matrix: + """Generate cofactor matrix""" return Matrix( [ - [ - self.minors().rows[row][column] - if (row + column) % 2 == 0 - else self.minors().rows[row][column] * -1 - for column in range(self.minors().num_columns) - ] - for row in range(self.minors().num_rows) + [self.get_cofactor(row, column) for column in range(self.num_columns)] + for row in range(self.num_rows) ] ) def adjugate(self) -> Matrix: - values = [ - [self.cofactors().rows[column][row] for column in range(self.num_columns)] - for row in range(self.num_rows) - ] - return Matrix(values) + """Generate adjugate matrix (transpose of cofactor matrix)""" + return Matrix( + [ + [ + self.cofactors().rows[column][row] + for column in range(self.num_columns) + ] + for row in range(self.num_rows) + ] + ) def inverse(self) -> Matrix: - determinant = self.determinant() - if not determinant: + """Calculate matrix inverse. Raises TypeError for singular matrices.""" + det = self.determinant() + if abs(det) < 1e-10: # Floating point tolerance raise TypeError("Only matrices with a non-zero determinant have an inverse") - return self.adjugate() * (1 / determinant) + return self.adjugate() * (1 / det) def __repr__(self) -> str: + """Official string representation of matrix""" return str(self.rows) def __str__(self) -> str: - if self.num_rows == 0: + """User-friendly string representation of matrix""" + if not self.rows: return "[]" if self.num_rows == 1: - return "[[" + ". ".join(str(self.rows[0])) + "]]" + return "[[" + ". ".join(str(val) for val in self.rows[0]) + "]]" return ( "[" + "\n ".join( - [ - "[" + ". ".join([str(value) for value in row]) + ".]" - for row in self.rows - ] + "[" + ". ".join(str(val) for val in row) + ".]" for row in self.rows ) + "]" ) - # MATRIX MANIPULATION - def add_row(self, row: list[int], position: int | None = None) -> None: - type_error = TypeError("Row must be a list containing all ints and/or floats") + # MATRIX MANIPULATION METHODS + def add_row(self, row: list[float], position: int | None = None) -> None: + """Add row to matrix. Validates type and length.""" if not isinstance(row, list): - raise type_error + raise TypeError("Row must be a list") for value in row: if not isinstance(value, (int, float)): - raise type_error + raise TypeError("Row elements must be int or float") if len(row) != self.num_columns: - raise ValueError( - "Row must be equal in length to the other rows in the matrix" - ) + raise ValueError("Row length must match matrix columns") + if position is None: self.rows.append(row) else: - self.rows = self.rows[0:position] + [row] + self.rows[position:] + # Fix RUF005: Use iterable unpacking instead of concatenation + self.rows = [*self.rows[:position], row, *self.rows[position:]] - def add_column(self, column: list[int], position: int | None = None) -> None: - type_error = TypeError( - "Column must be a list containing all ints and/or floats" - ) + def add_column(self, column: list[float], position: int | None = None) -> None: + """Add column to matrix. Validates type and length.""" if not isinstance(column, list): - raise type_error + raise TypeError("Column must be a list") for value in column: if not isinstance(value, (int, float)): - raise type_error + raise TypeError("Column elements must be int or float") if len(column) != self.num_rows: - raise ValueError( - "Column must be equal in length to the other columns in the matrix" - ) + raise ValueError("Column length must match matrix rows") + if position is None: - self.rows = [self.rows[i] + [column[i]] for i in range(self.num_rows)] + for i, value in enumerate(column): + self.rows[i].append(value) else: - self.rows = [ - self.rows[i][0:position] + [column[i]] + self.rows[i][position:] - for i in range(self.num_rows) - ] + # Fix RUF005: Use iterable unpacking instead of concatenation + for i, value in enumerate(column): + self.rows[i] = [ + *self.rows[i][:position], + value, + *self.rows[i][position:], + ] # MATRIX OPERATIONS def __eq__(self, other: object) -> bool: + """Check matrix equality""" if not isinstance(other, Matrix): return NotImplemented return self.rows == other.rows def __ne__(self, other: object) -> bool: + """Check matrix inequality""" return not self == other def __neg__(self) -> Matrix: - return self * -1 + """Negate matrix elements""" + return self * -1.0 def __add__(self, other: Matrix) -> Matrix: + """Matrix addition. Requires same dimensions.""" if self.order != other.order: - raise ValueError("Addition requires matrices of the same order") + raise ValueError("Addition requires matrices of same dimensions") return Matrix( [ [self.rows[i][j] + other.rows[i][j] for j in range(self.num_columns)] @@ -306,8 +234,9 @@ def __add__(self, other: Matrix) -> Matrix: ) def __sub__(self, other: Matrix) -> Matrix: + """Matrix subtraction. Requires same dimensions.""" if self.order != other.order: - raise ValueError("Subtraction requires matrices of the same order") + raise ValueError("Subtraction requires matrices of same dimensions") return Matrix( [ [self.rows[i][j] - other.rows[i][j] for j in range(self.num_columns)] @@ -316,47 +245,46 @@ def __sub__(self, other: Matrix) -> Matrix: ) def __mul__(self, other: Matrix | float) -> Matrix: + """Matrix multiplication (scalar or matrix)""" if isinstance(other, (int, float)): - return Matrix( - [[int(element * other) for element in row] for row in self.rows] - ) + # Preserve float precision by removing int conversion + return Matrix([[element * other for element in row] for row in self.rows]) elif isinstance(other, Matrix): if self.num_columns != other.num_rows: raise ValueError( - "The number of columns in the first matrix must " - "be equal to the number of rows in the second" + "Matrix multiplication requires columns of first matrix " + "to match rows of second matrix" ) return Matrix( [ - [Matrix.dot_product(row, column) for column in other.columns()] + [Matrix.dot_product(row, col) for col in other.columns()] for row in self.rows ] ) - else: - raise TypeError( - "A Matrix can only be multiplied by an int, float, or another matrix" - ) + raise TypeError("Matrix can only be multiplied by scalar or another matrix") - def __pow__(self, other: int) -> Matrix: - if not isinstance(other, int): - raise TypeError("A Matrix can only be raised to the power of an int") + def __pow__(self, exponent: int) -> Matrix: + """Matrix exponentiation. Requires square matrix.""" + if not isinstance(exponent, int): + raise TypeError("Exponent must be integer") if not self.is_square: raise ValueError("Only square matrices can be raised to a power") - if other == 0: + if exponent == 0: return self.identity() - if other < 0: + if exponent < 0: if self.is_invertable(): - return self.inverse() ** (-other) + return self.inverse() ** (-exponent) raise ValueError( - "Only invertable matrices can be raised to a negative power" + "Only invertible matrices can be raised to negative powers" ) result = self - for _ in range(other - 1): + for _ in range(exponent - 1): result *= self return result @classmethod - def dot_product(cls, row: list[int], column: list[int]) -> int: + def dot_product(cls, row: list[float], column: list[float]) -> float: + """Calculate dot product of two vectors""" return sum(row[i] * column[i] for i in range(len(row))) diff --git a/matrix/pascal_triangle.py b/matrix/pascal_triangle.py index 7f6555f9c8b9..4b087525de27 100644 --- a/matrix/pascal_triangle.py +++ b/matrix/pascal_triangle.py @@ -1,16 +1,19 @@ """ -This implementation demonstrates how to generate the elements of a Pascal's triangle. -The element havingva row index of r and column index of c can be derivedvas follows: -triangle[r][c] = triangle[r-1][c-1]+triangle[r-1][c] +This implementation demonstrates how to generate the elements of Pascal's Triangle. +An element with row index r and column index c can be derived as: +triangle[r][c] = triangle[r-1][c-1] + triangle[r-1][c] -A Pascal's triangle is a triangular array containing binomial coefficients. +Pascal's Triangle is a triangular array containing binomial coefficients. https://en.wikipedia.org/wiki/Pascal%27s_triangle """ +from collections.abc import Callable +from timeit import timeit + def print_pascal_triangle(num_rows: int) -> None: """ - Print Pascal's triangle for different number of rows + Print Pascal's triangle for the specified number of rows >>> print_pascal_triangle(5) 1 1 1 @@ -20,7 +23,7 @@ def print_pascal_triangle(num_rows: int) -> None: """ triangle = generate_pascal_triangle(num_rows) for row_idx in range(num_rows): - # Print left spaces + # Print leading spaces for _ in range(num_rows - row_idx - 1): print(end=" ") # Print row values @@ -34,7 +37,7 @@ def print_pascal_triangle(num_rows: int) -> None: def generate_pascal_triangle(num_rows: int) -> list[list[int]]: """ - Create Pascal's triangle for different number of rows + Generate Pascal's triangle for the specified number of rows >>> generate_pascal_triangle(0) [] >>> generate_pascal_triangle(1) @@ -50,22 +53,20 @@ def generate_pascal_triangle(num_rows: int) -> list[list[int]]: >>> generate_pascal_triangle(-5) Traceback (most recent call last): ... - ValueError: The input value of 'num_rows' should be greater than or equal to 0 + ValueError: Input value 'num_rows' must be >= 0 >>> generate_pascal_triangle(7.89) Traceback (most recent call last): ... - TypeError: The input value of 'num_rows' should be 'int' + TypeError: Input value 'num_rows' must be an integer """ if not isinstance(num_rows, int): - raise TypeError("The input value of 'num_rows' should be 'int'") + raise TypeError("Input value 'num_rows' must be an integer") if num_rows == 0: return [] - elif num_rows < 0: - raise ValueError( - "The input value of 'num_rows' should be greater than or equal to 0" - ) + if num_rows < 0: + raise ValueError("Input value 'num_rows' must be >= 0") triangle: list[list[int]] = [] for current_row_idx in range(num_rows): @@ -81,7 +82,7 @@ def populate_current_row(triangle: list[list[int]], current_row_idx: int) -> lis [1, 1] """ current_row = [-1] * (current_row_idx + 1) - # first and last elements of current row are equal to 1 + # First and last elements of current row are always 1 current_row[0], current_row[-1] = 1, 1 for current_col_idx in range(1, current_row_idx): calculate_current_element( @@ -103,22 +104,19 @@ def calculate_current_element( >>> current_row [1, 2, 1] """ - above_to_left_elt = triangle[current_row_idx - 1][current_col_idx - 1] - above_to_right_elt = triangle[current_row_idx - 1][current_col_idx] - current_row[current_col_idx] = above_to_left_elt + above_to_right_elt + above_left = triangle[current_row_idx - 1][current_col_idx - 1] + above_right = triangle[current_row_idx - 1][current_col_idx] + current_row[current_col_idx] = above_left + above_right def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: """ - This function returns a matrix representing the corresponding pascal's triangle - according to the given input of number of rows of Pascal's triangle to be generated. - It reduces the operations done to generate a row by half - by eliminating redundant calculations. + Returns a matrix representing Pascal's triangle. + Reduces operations by half by eliminating redundant calculations. - :param num_rows: Integer specifying the number of rows in the Pascal's triangle - :return: 2-D List (matrix) representing the Pascal's triangle + :param num_rows: Number of rows in the Pascal's triangle + :return: 2D list representing the Pascal's triangle - Return the Pascal's triangle of given rows >>> generate_pascal_triangle_optimized(3) [[1], [1, 1], [1, 2, 1]] >>> generate_pascal_triangle_optimized(1) @@ -128,29 +126,27 @@ def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: >>> generate_pascal_triangle_optimized(-5) Traceback (most recent call last): ... - ValueError: The input value of 'num_rows' should be greater than or equal to 0 + ValueError: Input value 'num_rows' must be >= 0 >>> generate_pascal_triangle_optimized(7.89) Traceback (most recent call last): ... - TypeError: The input value of 'num_rows' should be 'int' + TypeError: Input value 'num_rows' must be an integer """ if not isinstance(num_rows, int): - raise TypeError("The input value of 'num_rows' should be 'int'") + raise TypeError("Input value 'num_rows' must be an integer") if num_rows == 0: return [] - elif num_rows < 0: - raise ValueError( - "The input value of 'num_rows' should be greater than or equal to 0" - ) + if num_rows < 0: + raise ValueError("Input value 'num_rows' must be >= 0") result: list[list[int]] = [[1]] for row_index in range(1, num_rows): temp_row = [0] + result[-1] + [0] row_length = row_index + 1 - # Calculate the number of distinct elements in a row + # Calculate number of distinct elements in row distinct_elements = sum(divmod(row_length, 2)) row_first_half = [ temp_row[i - 1] + temp_row[i] for i in range(1, distinct_elements + 1) @@ -165,15 +161,12 @@ def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: def benchmark() -> None: """ - Benchmark multiple functions, with three different length int values. + Benchmark functions with different input sizes """ - from collections.abc import Callable - from timeit import timeit def benchmark_a_function(func: Callable, value: int) -> None: call = f"{func.__name__}({value})" timing = timeit(f"__main__.{call}", setup="import __main__") - # print(f"{call:38} = {func(value)} -- {timing:.4f} seconds") print(f"{call:38} -- {timing:.4f} seconds") for value in range(15): # (1, 7, 14): diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 5a143c739b9d..6eaacff2966a 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -1,13 +1,13 @@ from __future__ import annotations from collections.abc import Callable -from typing import Generic, TypeVar +from typing import TypeVar T = TypeVar("T") U = TypeVar("U") -class DoubleLinkedListNode(Generic[T, U]): +class DoubleLinkedListNode[T, U]: """ Double Linked List Node built specifically for LFU Cache @@ -30,7 +30,7 @@ def __repr__(self) -> str: ) -class DoubleLinkedList(Generic[T, U]): +class DoubleLinkedList[T, U]: """ Double Linked List built specifically for LFU Cache @@ -161,7 +161,7 @@ def remove( return node -class LFUCache(Generic[T, U]): +class LFUCache[T, U]: """ LFU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. diff --git a/other/lru_cache.py b/other/lru_cache.py index 4f0c843c86cc..1d9a67f4ad0b 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -1,13 +1,9 @@ from __future__ import annotations from collections.abc import Callable -from typing import Generic, TypeVar -T = TypeVar("T") -U = TypeVar("U") - -class DoubleLinkedListNode(Generic[T, U]): +class DoubleLinkedListNode[T, U]: """ Double Linked List Node built specifically for LRU Cache @@ -28,7 +24,7 @@ def __repr__(self) -> str: ) -class DoubleLinkedList(Generic[T, U]): +class DoubleLinkedList[T, U]: """ Double Linked List built specifically for LRU Cache @@ -143,7 +139,7 @@ def remove( return node -class LRUCache(Generic[T, U]): +class LRUCache[T, U]: """ LRU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. @@ -222,7 +218,6 @@ def __repr__(self) -> str: Return the details for the cache instance [hits, misses, capacity, current_size] """ - return ( f"CacheInfo(hits={self.hits}, misses={self.miss}, " f"capacity={self.capacity}, current size={self.num_keys})" @@ -240,7 +235,6 @@ def __contains__(self, key: T) -> bool: >>> 1 in cache True """ - return key in self.cache def get(self, key: T) -> U | None: @@ -267,7 +261,6 @@ def put(self, key: T, value: U) -> None: """ Sets the value for the input key and updates the Double Linked List """ - if key not in self.cache: if self.num_keys >= self.capacity: # delete first node (oldest) when over capacity @@ -286,7 +279,6 @@ def put(self, key: T, value: U) -> None: self.cache[key] = DoubleLinkedListNode(key, value) self.list.add(self.cache[key]) self.num_keys += 1 - else: # bump node to the end of the list, update value node = self.list.remove(self.cache[key]) diff --git a/pyproject.toml b/pyproject.toml index 2ead5cd51ae8..afca7514fb00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "pillow>=11", "rich>=13.9.4", "scikit-learn>=1.5.2", + "seaborn>=0.13.2", "sphinx-pyproject>=0.3", "statsmodels>=0.14.4", "sympy>=1.13.3", diff --git a/requirements.txt b/requirements.txt index 66b5d8a6b94e..53b19a980dd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ pandas pillow rich scikit-learn +seaborn sphinx-pyproject statsmodels sympy