├── .gitignore ├── README.md └── core ├── formatted-strings └── formatted_strings.py ├── iterators └── custom_iterator.py ├── looping └── looping.py ├── match-case └── match_case.py ├── misc ├── days_to_hours.py └── days_to_units.py ├── os-environment-vars └── os_environment_vars.py ├── recursion ├── BST │ └── inorder_successor.py ├── hanoi │ ├── hanoi.py │ ├── same-in-java │ │ └── Hanoi.java │ └── same-in-js │ │ └── hanoi.js ├── recursive_code_call_tree_drawer.py ├── recursive_palindrome_checker.py └── recursive_power_simulator.py ├── searching-algorithms ├── binary-search │ ├── binary_search.py │ └── binary_search_tester.py └── linear_search │ ├── linear_search.py │ └── linear_search_tester.py ├── sorting-algorithms ├── bubble-sort │ ├── bubble-sort-tester.py │ └── bubble_sort.py ├── insertion-sort │ ├── insertion-sort-tester.py │ └── insertion_sort.py ├── merge-sort │ ├── merge-sort-tester.py │ └── merge_sort.py ├── quick-sort │ ├── quick-sort-tester.py │ └── quick_sort.py └── selection-sort │ ├── selection-sort-tester.py │ └── selection_sort.py ├── strings-n-slicing ├── strings-n-slicing.md └── strings-n-slicing.py ├── sum-to-goal └── sum-to-goal.py ├── types ├── types.md └── types.py ├── working-with-csv-files ├── csv_reader.py └── laureates.csv └── working-with-datetime ├── laureates.csv └── using_datetime.py /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | 165 | #java bytecode 166 | *.class -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python 2 | -------------------------------------------------------------------------------- /core/formatted-strings/formatted_strings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Prints a greeting using two different string formatting methods. 3 | 4 | name (str): The name to use in the greeting. 5 | 6 | formatted_1 (str): Uses f-string formatting to insert the name into the greeting string. 7 | Prints: 8 | Hello Reza 9 | 10 | greet_format (str): A greeting string with a placeholder. 11 | formatted_2 (str): Uses the .format() method to insert the name into the greeting string. 12 | Prints: 13 | Hello Reza 14 | """ 15 | 16 | # Define the name to use in the greeting 17 | name = "Reza" 18 | # Use f-string formatting to insert the name into the greeting string 19 | formatted_1 = f"Hello {name}" 20 | print(formatted_1) 21 | 22 | # OR (the same result...) 23 | 24 | # Define a greeting string with a placeholder 25 | greet_format = "Hello {}" 26 | # Use the .format() method to insert the name into the greeting string 27 | formatted_2 = greet_format.format(name) 28 | print(formatted_2) 29 | -------------------------------------------------------------------------------- /core/iterators/custom_iterator.py: -------------------------------------------------------------------------------- 1 | # A class to demonstrate how to create a custom iterator 2 | class MyNumbers: 3 | def __init__(self, start=1): 4 | self.number = start 5 | 6 | # This method is required for iterators 7 | def __iter__(self): 8 | return self 9 | 10 | # This method is required for iterators 11 | def __next__(self): 12 | x = self.number 13 | self.number += 1 14 | return x 15 | 16 | my_numbers_object = MyNumbers(10) 17 | 18 | for item in my_numbers_object: 19 | if item > 20: 20 | break 21 | print(item) 22 | 23 | my_numbers_iterator = iter(my_numbers_object) 24 | 25 | print(next(my_numbers_iterator)) 26 | print(next(my_numbers_iterator)) -------------------------------------------------------------------------------- /core/looping/looping.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loops through lists of names and ages using various looping constructs. 3 | 4 | NAMES (list): A list of names. 5 | AGES (list): A list of ages corresponding to the names. 6 | 7 | # while loop 8 | Loops while i is less than the length of NAMES, printing each name and age. 9 | Increments i on each iteration. 10 | 11 | # for loop 12 | Loops through each name in NAMES, printing the name. 13 | 14 | # for loop using zip 15 | Loops through NAMES and AGES simultaneously using zip, printing each name and age. 16 | 17 | # for loop using reversed 18 | Loops through NAMES in reverse order, printing each name. 19 | 20 | # for loop using range 21 | Loops twice, printing the name at index 0 and 1. 22 | 23 | # for loop using enumerate 24 | Loops through NAMES, printing the index and name on each iteration. 25 | """ 26 | 27 | NAMES = ["Reza", "Ryan"] 28 | AGES = [47, 2] 29 | 30 | # while loop 31 | i = 0 32 | while i < len(NAMES): 33 | print(NAMES[i], AGES[i]) 34 | i += 1 35 | 36 | # for loop 37 | for name in NAMES: 38 | print(name) 39 | 40 | # for loop using zip 41 | for name, age in zip(NAMES, AGES): 42 | print(f"{name} {age}") 43 | 44 | # for loop using reversed 45 | for name in reversed(NAMES): 46 | print(name) 47 | 48 | # for loop using range 49 | for i in range(2): 50 | print(NAMES[i]) 51 | 52 | # for loop using enumerate 53 | for i, name in enumerate(NAMES): 54 | print(f"{i} {name}") 55 | -------------------------------------------------------------------------------- /core/match-case/match_case.py: -------------------------------------------------------------------------------- 1 | """ 2 | Prints a message based on the value of mycolor using pattern matching. 3 | 4 | Parameters: 5 | mycolor (str): The color to check. 6 | 7 | match mycolor.lower(): 8 | Checks the lowercase value of mycolor against the cases. 9 | 10 | case 'blue': 11 | Prints 'Blue' if mycolor is 'blue'. 12 | 13 | case 'green': 14 | Prints 'Green' if mycolor is 'green'. 15 | 16 | case 'yellow': 17 | Prints 'Yellow' if mycolor is 'yellow'. 18 | 19 | case _: 20 | Prints 'Any other color!' if mycolor does not match any other case. 21 | 22 | This construct was added in Python 3.10 23 | """ 24 | 25 | mycolor = "Yellow" 26 | 27 | match mycolor.lower(): 28 | case "blue": 29 | print("Blue") 30 | case "green": 31 | print("Green") 32 | case "yellow": 33 | print("Yellow") 34 | case _: 35 | print("Any other color!") 36 | -------------------------------------------------------------------------------- /core/misc/days_to_hours.py: -------------------------------------------------------------------------------- 1 | # A simple Python program to calculate the number of hours in a list composed of a few number of days... 2 | hours_in_a_day = 24 3 | 4 | 5 | # A function to return the number of hours in a given number of days passed in: 6 | def days_to_hours(num_of_days): 7 | return f"{num_of_days} day(s) is/are {num_of_days * hours_in_a_day} hours." 8 | 9 | 10 | # A function to calculate the number of hours in the number of days passed in: 11 | def calculate_hours(num_of_days): 12 | try: 13 | user_input_number = int(num_of_days) 14 | 15 | # we want to do conversion only for positive integers 16 | if user_input_number > 0: 17 | calculated_value = days_to_hours(user_input_number) 18 | print(calculated_value) 19 | elif user_input_number == 0: 20 | print("You entered a '0'! Please enter a valid positive number!") 21 | else: 22 | print( 23 | f"You entered a negative number: '{num_of_days}'! Please enter a valid positive number!" 24 | ) 25 | 26 | except ValueError: 27 | print(f"Your input: '{num_of_days}', is not a valid number!") 28 | 29 | 30 | # A loop to accept a list of days as the input from user and calculate hours per day for each member of that list, until the user enters "exit" 31 | user_input = input( 32 | "Enter a comma separated list of days and I will convert them to hours!\n" 33 | ) 34 | while user_input != "exit": 35 | for num_of_days in set(user_input.split(", ")): 36 | calculate_hours(num_of_days) 37 | user_input = input( 38 | "Enter a comma separated list of days and I will convert them to hours!\n" 39 | ) 40 | -------------------------------------------------------------------------------- /core/misc/days_to_units.py: -------------------------------------------------------------------------------- 1 | # A simple Python program to calculate the number of conversion_units pr day in a list composed of a few number of 'days:unit' elements... 2 | 3 | 4 | # A function to return the number of conversion_units(hours or minutes) in a given number of days passed in: 5 | def days_to_units(num_of_days, conversion_unit): 6 | if conversion_unit == "hours": 7 | return f"{num_of_days} day(s) is/are {num_of_days * 24} hours!" 8 | elif conversion_unit == "minutes": 9 | return f"{num_of_days} day(s) is/are {num_of_days * 24 * 60} minutes!" 10 | else: 11 | return f"unsupported unit: '{conversion_unit}'!" 12 | 13 | 14 | # A function to calculate the number of units in the number of days (values passed in as a dictionary): 15 | def calculate_units(days_and_unit_dictionary): 16 | try: 17 | user_input_number_of_days = int(days_and_unit_dictionary["days"]) 18 | user_input_unit = days_and_unit_dictionary["unit"] 19 | 20 | # we want to do conversion only for positive integers 21 | if user_input_number_of_days > 0: 22 | calculated_value = days_to_units(user_input_number_of_days, user_input_unit) 23 | print(calculated_value) 24 | elif user_input_number_of_days == 0: 25 | print("you entered a 0! Please enter a valid positive number!") 26 | else: 27 | print( 28 | f"You entered a negative number: '{user_input_number_of_days}'! Please enter a valid positive number!" 29 | ) 30 | 31 | except ValueError: 32 | print( 33 | f"Your input: '{days_and_unit_dictionary['days']}', is not a valid number!" 34 | ) 35 | 36 | 37 | # A loop to accept a list of 'days:unit' elements as the input from user and calculate units per day for each member of that list, until the user enters "exit" 38 | user_input = input( 39 | "Enter a comma separated list of 'days:unit' elements and I will convert each element's 'days' to its equivalent 'unit's!\n" 40 | ) 41 | while user_input != "exit": 42 | for key_value_entry in set(user_input.split(", ")): 43 | try: 44 | days_and_unit = key_value_entry.split(":") 45 | days_and_unit_dictionary = { 46 | "days": days_and_unit[0], 47 | "unit": days_and_unit[1], 48 | } 49 | calculate_units(days_and_unit_dictionary) 50 | 51 | except IndexError: 52 | print("Error(s) found in the input format, please double check!") 53 | 54 | user_input = input( 55 | "Enter a comma separated list of 'days:unit' elements and I will convert each element's 'days' to its equivalent 'unit's!\n" 56 | ) 57 | -------------------------------------------------------------------------------- /core/os-environment-vars/os_environment_vars.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks the ENV_NAME environment variable to determine the environment. 3 | current_env (a str) gets the ENV_NAME environment variable, defaulting to DEVELOPMENT. 4 | You can test this program using "export ENV_NAME=something_else" in terminal (Mac/Linux.) 5 | """ 6 | 7 | import os 8 | 9 | DEVELOPMENT = "development" 10 | STAGING = "staging" 11 | PRODUCTION = "production" 12 | 13 | current_env = os.environ.get("ENV_NAME", DEVELOPMENT) 14 | 15 | if current_env == DEVELOPMENT: 16 | print("Development environment") 17 | elif current_env == PRODUCTION: 18 | print("Production environment") 19 | elif current_env == STAGING: 20 | print("Staging environment") 21 | else: 22 | print("Unknown environment") 23 | -------------------------------------------------------------------------------- /core/recursion/BST/inorder_successor.py: -------------------------------------------------------------------------------- 1 | # A program to find the inorder successor of a node in a BST 2 | import queue 3 | 4 | 5 | # A class to represent a BST 6 | class BST: 7 | # A class to represent a node in a BST 8 | class Node: 9 | # Node's init function 10 | def __init__(self, data=None, left=None, right=None): 11 | self.data = data 12 | self.left = left 13 | self.right = right 14 | 15 | # BST's init function 16 | def __init__(self): 17 | self.root = None 18 | 19 | # BST's insert function 20 | def insert(self, data): 21 | # If the tree is empty, create a new node as the root 22 | if self.root is None: 23 | self.root = BST.Node(data) 24 | # otherwise, find the right place to insert 25 | else: 26 | # curr points to the variable we are currently looking at 27 | curr = self.root 28 | inserted = False 29 | # Keep looping until we find the right place to insert 30 | while not inserted: 31 | # If the data is smaller than the current node's data, go left 32 | if data < curr.data: 33 | if curr.left is not None: 34 | curr = curr.left 35 | else: 36 | curr.left = BST.Node(data) 37 | inserted = True 38 | # otherwise, go right 39 | else: 40 | if curr.right is not None: 41 | curr = curr.right 42 | else: 43 | curr.right = BST.Node(data) 44 | inserted = True 45 | 46 | # BST's search function 47 | def search(self, data): 48 | curr = self.root 49 | 50 | while curr is not None: 51 | if data < curr.data: 52 | curr = curr.left 53 | elif data > curr.data: 54 | curr = curr.right 55 | else: 56 | return curr 57 | 58 | return None 59 | 60 | # BST's breadth first print function 61 | def breadth_first_print(self): 62 | the_nodes = queue.Queue() 63 | 64 | if self.root is not None: 65 | the_nodes.put(self.root) 66 | 67 | while not the_nodes.empty(): 68 | curr = the_nodes.get() 69 | 70 | if curr.left: 71 | the_nodes.put(curr.left) 72 | if curr.right: 73 | the_nodes.put(curr.right) 74 | 75 | print(curr.data, end=" ") 76 | 77 | # BST's (wrapped) recursive search function 78 | def wrapped_recursive_search(self, data, subtree): 79 | # If the subtree is empty, return None 80 | if subtree is None: 81 | return None 82 | # otherwise, recursively search the subtree 83 | else: 84 | if data < subtree.data: 85 | return self.wrapped_recursive_search(data, subtree.left) 86 | elif data > subtree.data: 87 | return self.wrapped_recursive_search(data, subtree.right) 88 | else: 89 | return subtree 90 | 91 | # BST's (wrapper) recursive search function 92 | def recursive_search(self, data): 93 | return self.wrapped_recursive_search(data, self.root) 94 | 95 | # BST's (wrapped) recursive insert function 96 | def wrapped_recursive_insert(self, data, subtree): 97 | # If the subtree is empty, create a new node as the subtree 98 | if subtree is None: 99 | return BST.Node(data) 100 | elif data < subtree.data: 101 | subtree.left = self.wrapped_recursive_insert(data, subtree.left) 102 | else: 103 | subtree.right = self.wrapped_recursive_insert(data, subtree.right) 104 | return subtree 105 | 106 | # BST's (wrapper) recursive insert function 107 | def recursive_insert(self, data): 108 | self.root = self.wrapped_recursive_insert(data, self.root) 109 | 110 | # BST's (wrapped) recursive print_inorder function 111 | def print_inorder(self, subtree): 112 | # If the subtree is not empty, 113 | if subtree is not None: 114 | self.print_inorder(subtree.left) 115 | print(subtree.data, end=" ") 116 | self.print_inorder(subtree.right) 117 | 118 | # BST's (wrapper) print function 119 | def print(self): 120 | self.print_inorder(self.root) # or change to print_postorder or print_preorder 121 | print("") 122 | 123 | # BST's (wrapped) recursive print_preorder function 124 | def print_preorder(self, subtree): 125 | if subtree is not None: 126 | print(subtree.data, end=" ") 127 | self.print_preorder(subtree.left) 128 | self.print_preorder(subtree.right) 129 | 130 | # BST's (wrapped) recursive print_postorder function 131 | def print_postorder(self, subtree): 132 | if subtree is not None: 133 | self.print_postorder(subtree.left) 134 | self.print_postorder(subtree.right) 135 | print(subtree.data, end=" ") 136 | 137 | # BST's (wrapped) recursive print_recursive_between function 138 | def print_recursive_between(self, subtree, min, max): 139 | if subtree is None: 140 | return 141 | elif subtree.data < min: 142 | self.print_recursive_between(subtree.right, min, max) 143 | elif subtree.data > max: 144 | self.print_recursive_between(subtree.left, min, max) 145 | else: 146 | print(subtree.data) 147 | self.print_recursive_between(subtree.left, min, max) 148 | self.print_recursive_between(subtree.right, min, max) 149 | 150 | # BST's (wrapper) print_between function 151 | def print_between(self, min, max): 152 | self.print_recursive_between(self.root, min, max) 153 | 154 | # The following two could be applied on any binary trees in general: 155 | # BST's (wrapped) recursive_height function 156 | def recursive_height(self, subtree): 157 | if subtree is None: 158 | return 0 159 | else: 160 | return 1 + max( 161 | self.recursive_height(subtree.left), 162 | self.recursive_height(subtree.right), 163 | ) 164 | 165 | # BST's (wrapper) height function 166 | def height(self): 167 | return self.recursive_height(self.root) 168 | 169 | # In a Binary Tree, Inorder successor of a node is the next node in Inorder traversal of the Binary Tree. 170 | # Inorder Successor is None for the last node in Inorder traversal. 171 | # In a Binary Search Tree, Inorder Successor of an input node can also be defined as the node 172 | # with the smallest key greater than the key of the input node. 173 | 174 | # BST's (wrapped) recursive inorder_successor function 175 | # O(log(n)) Solution: 176 | def recursive_inorder_successor(self, subtree, value): 177 | if subtree is not None: 178 | print(subtree.data, end="->") 179 | 180 | if value >= subtree.data: 181 | result = self.recursive_inorder_successor(subtree.right, value) 182 | if result is not None: 183 | return result 184 | else: 185 | result = self.recursive_inorder_successor(subtree.left, value) 186 | if result is not None: 187 | return result 188 | 189 | if subtree.data > value: 190 | return subtree.data 191 | 192 | # BST's (wrapped) recursive inorder_successor function 193 | # O(n) Solution: 194 | def not_efficient_inorder_successor(self, subtree, value): 195 | if subtree is not None: 196 | print(subtree.data, end="->") 197 | 198 | result = self.not_efficient_inorder_successor(subtree.left, value) 199 | if result is not None: 200 | return result 201 | 202 | if subtree.data > value: 203 | return subtree.data 204 | 205 | result = self.not_efficient_inorder_successor(subtree.right, value) 206 | if result is not None: 207 | return result 208 | 209 | # BST's (wrapper) inorder_successor function 210 | def inorder_successor(self, value): 211 | node = self.search(value) 212 | 213 | if node is None: 214 | return None 215 | else: 216 | # return self.not_efficient_inorder_successor(self.root, value) 217 | return self.recursive_inorder_successor(self.root, value) 218 | 219 | # # And note why the following solution is wrong!! 220 | # node = self.search(value) 221 | # 222 | # if node is None or node.right is None: 223 | # return None 224 | # else: 225 | # node = node.right 226 | # 227 | # while node.left is not None: 228 | # node = node.left 229 | # 230 | # return node.data 231 | 232 | 233 | bst = BST() 234 | # print(bst.root) 235 | 236 | # bst.insert(2) 237 | # bst.insert(1) 238 | # bst.insert(3) 239 | # print(bst.root.right.data) 240 | # print(bst.search(3).data) 241 | # bst.breadth_first_print() 242 | # print(bst.recursive_search(4)) 243 | 244 | # bst.recursive_insert(3) 245 | # bst.recursive_insert(1) 246 | # bst.recursive_insert(2) 247 | # bst.breadth_first_print() 248 | # print(bst.recursive_search(3).data) 249 | # bst.print() 250 | 251 | # bst.print_between(1, 2) 252 | # bst.print_between(1, 3) 253 | # bst.print_between(1, 1) 254 | # bst.print_between(3, 3) 255 | # bst.print_between(2, 2) 256 | # print(bst.height()) 257 | 258 | bst.recursive_insert(20) 259 | bst.recursive_insert(8) 260 | bst.recursive_insert(22) 261 | bst.recursive_insert(4) 262 | bst.recursive_insert(12) 263 | bst.recursive_insert(10) 264 | bst.recursive_insert(14) 265 | print(bst.inorder_successor(14)) 266 | -------------------------------------------------------------------------------- /core/recursion/hanoi/hanoi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Recursive solution to the Towers of Hanoi problem. 3 | 4 | Parameters: 5 | f (str): Starting rod 6 | s (str): Spare rod 7 | t (str): Ending rod 8 | n (int): Number of disks 9 | 10 | Functionality: 11 | Moves n disks from rod f to rod t using rod s (disk no. 1 is the topmost disk) 12 | If n <= 0, returns. 13 | If n == 1, prints the move from f to t. 14 | Otherwise: 15 | - Moves the top n-1 disks from f to s using t. 16 | - Prints the move from f to t. 17 | - Moves all the n-1 disks from s to t using f. 18 | """ 19 | 20 | 21 | def hanoi(f, s, t, n): 22 | # base case(s) 23 | if n <= 0: 24 | return 25 | if n == 1: 26 | print(f"Move disk {n} from {f} to {t}") 27 | return 28 | # (else) recursive case 29 | # move the top n - 1 disks on f to s (with the help of t) 30 | hanoi(f, t, s, n - 1) 31 | # move the biggest disk from f to t 32 | print(f"Move disk {n} from {f} to {t}") 33 | # move the same n - 1 disks from s to t (with the help of f) 34 | hanoi(s, f, t, n - 1) 35 | 36 | 37 | # take some input from the user 38 | n = int(input("Enter the number of disks: ")) 39 | hanoi("f", "s", "t", n) 40 | -------------------------------------------------------------------------------- /core/recursion/hanoi/same-in-java/Hanoi.java: -------------------------------------------------------------------------------- 1 | // Recursive solution to the Towers of Hanoi problem. 2 | import java.util.Scanner; 3 | 4 | public class Hanoi { 5 | // Parameters: 6 | // f (str): Starting rod 7 | // s (str): Spare rod 8 | // t (str): Ending rod 9 | // n (int): Number of disks 10 | 11 | // Functionality: 12 | // Moves n disks from rod f to rod t using rod s (disk no. 1 is the topmost 13 | // disk) 14 | // If n <= 0, returns. 15 | // If n == 1, prints the move from f to t. 16 | // Otherwise: 17 | // - Moves the top n-1 disks from f to s using t. 18 | // - Prints the move from f to t. 19 | // - Moves all the n-1 disks from s to t using f. 20 | public static void hanoi(char f, char s, char t, int n) { 21 | // Base case(s) 22 | if (n <= 0) 23 | return; 24 | if (n == 1) { 25 | System.out.printf("Move disk %d from %c to %c\n", n, f, t); 26 | return; 27 | } 28 | // recursive case: 29 | hanoi(f, t, s, n - 1); 30 | System.out.printf("Move disk %d from %c to %c\n", n, f, t); 31 | hanoi(s, f, t, n - 1); 32 | } 33 | 34 | public static void main(String[] args) { 35 | Scanner sc = new Scanner(System.in); 36 | System.out.print("Enter the number of disks: "); 37 | int n = sc.nextInt(); 38 | hanoi('f', 's', 't', n); 39 | sc.close(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/recursion/hanoi/same-in-js/hanoi.js: -------------------------------------------------------------------------------- 1 | // Recursive solution to the Towers of Hanoi problem. 2 | 3 | // Parameters: 4 | // f (str): Starting rod 5 | // s (str): Spare rod 6 | // t (str): Ending rod 7 | // n (int): Number of disks 8 | 9 | // Functionality: 10 | // Moves n disks from rod f to rod t using rod s (disk no. 1 is the topmost disk) 11 | // If n <= 0, returns. 12 | // If n == 1, prints the move from f to t. 13 | // Otherwise: 14 | // - Moves the top n-1 disks from f to s using t. 15 | // - Prints the move from f to t. 16 | // - Moves all the n-1 disks from s to t using f. 17 | 18 | function hanoi(f, s, t, n) { 19 | // if (n > 0) { 20 | // hanoi(f, t, s, n - 1); 21 | // console.log(`${f} -> ${t}`); 22 | // hanoi(s, f, t, n - 1); 23 | // } 24 | // OR 25 | // base case (s) 26 | if (n <= 0) 27 | return; 28 | if (n === 1) { 29 | console.log(`Move disk ${n} from ${f} to ${t}`); 30 | return; 31 | } 32 | // recursive case 33 | hanoi(f, t, s, n - 1); 34 | console.log(`Move disk ${n} from ${f} to ${t}`); 35 | hanoi(s, f, t, n - 1); 36 | } 37 | 38 | // take some input from the user 39 | const readline = require('readline').createInterface({ 40 | input: process.stdin, 41 | output: process.stdout 42 | }); 43 | 44 | readline.question('Enter the number of disks: ', (num) => { 45 | const n = parseInt(num); 46 | hanoi('f', 's', 't', n); 47 | readline.close(); 48 | }); 49 | -------------------------------------------------------------------------------- /core/recursion/recursive_code_call_tree_drawer.py: -------------------------------------------------------------------------------- 1 | # Recursive Code Call Tree Drawer 2 | # Author: Reza Khojasteh 3 | # Date: 2025-03-27 4 | # Description: This script visualizes the call tree of a recursive function. 5 | # It uses the RecursiveTracer class to trace the calls and build the tree. 6 | # The user can enter Python code with recursive functions and an initial call. 7 | # The script will add a decorator to the functions and execute the initial call. 8 | # A decorator is a function that takes another function as input and returns a modified version of it. 9 | # The call tree is then drawn using the tkinter library. 10 | # The user can then click on a node to see the code of the function. 11 | 12 | import tkinter as tk 13 | from tkinter import scrolledtext, messagebox, font 14 | import inspect 15 | import re 16 | import ast 17 | import math 18 | import sys 19 | import io # Needed for redirecting stdout if used later 20 | import traceback # For detailed error printing 21 | 22 | # Increase recursion depth limit 23 | try: 24 | sys.setrecursionlimit(2000) 25 | except Exception as e: 26 | print(f"Warning: Could not set recursion depth limit - {e}") 27 | 28 | 29 | # --- 1. Call Tree Node --- 30 | class CallTreeNode: 31 | _id_counter = 0 32 | 33 | def __init__( 34 | self, func_name, args, kwargs, parent_id=None, call_id=None 35 | ): # Added call_id 36 | self.id = call_id if call_id is not None else CallTreeNode._id_counter 37 | if call_id is None: 38 | CallTreeNode._id_counter += 1 39 | self.func_name = func_name 40 | self.args = args 41 | self.kwargs = kwargs 42 | self.parent_id = parent_id 43 | self.children = [] 44 | self.return_value = None 45 | self.display_str = self._create_display_str() 46 | self.x = 0 47 | self.y = 0 48 | self.width = 0 49 | self.subtree_width = 0 50 | 51 | def _create_display_str(self): 52 | try: 53 | arg_strs = [repr(a) for a in self.args] 54 | kwarg_strs = [f"{k}={repr(v)}" for k, v in self.kwargs.items()] 55 | all_args = ", ".join(arg_strs + kwarg_strs) 56 | if len(all_args) > 50: 57 | all_args = all_args[:47] + "..." 58 | base_str = f"{self.func_name}({all_args})" 59 | if self.return_value is not None and not ( 60 | isinstance(self.return_value, str) 61 | and self.return_value.startswith("Error:") 62 | ): 63 | ret_str = repr(self.return_value) 64 | if len(ret_str) > 20: 65 | ret_str = ret_str[:17] + "..." 66 | base_str += f" -> {ret_str}" 67 | return base_str 68 | except Exception as e: 69 | # Handle cases where repr() might fail on complex objects 70 | print(f"Error creating display string: {e}") 71 | return f"{self.func_name}(...)" 72 | 73 | def add_child(self, child_node): 74 | self.children.append(child_node) 75 | 76 | def update_display_str_with_return(self): 77 | """Updates the display string after return value is known.""" 78 | self.display_str = self._create_display_str() 79 | 80 | def __repr__(self): 81 | return f"Node({self.id}, {self.display_str}, parent={self.parent_id})" 82 | 83 | 84 | # --- 2. Recursive Tracer --- 85 | class RecursiveTracer: 86 | """Manages tracing ALL decorated functions.""" 87 | 88 | def __init__(self): 89 | self.call_tree_root = None 90 | self.call_stack = [] # Stores node IDs being executed 91 | self.all_nodes = {} # Map ID to Node object 92 | self.current_id = 0 # Simple ID counter 93 | 94 | def reset(self): 95 | """Resets state for a new visualization run.""" 96 | self.call_tree_root = None 97 | self.call_stack = [] 98 | self.all_nodes = {} 99 | self.current_id = 0 100 | CallTreeNode._id_counter = 0 # Reset node ID gen as well 101 | 102 | def trace_calls(self, func): 103 | """Decorator factory to wrap any function it's applied to.""" 104 | if not callable(func): # Allow decorating methods etc. 105 | raise TypeError("Decorator can only wrap callables.") 106 | 107 | func_name = func.__name__ # Get name at decoration time 108 | 109 | def wrapper(*args, **kwargs): 110 | call_id = self.current_id 111 | self.current_id += 1 112 | 113 | parent_id = self.call_stack[-1] if self.call_stack else None 114 | 115 | # Create the node for this call 116 | # Use the captured func_name from the outer scope 117 | node = CallTreeNode(func_name, args, kwargs, parent_id, call_id=call_id) 118 | self.all_nodes[node.id] = node 119 | 120 | # Link to parent and set root if necessary 121 | if parent_id is not None: 122 | parent_node = self.all_nodes.get(parent_id) 123 | if parent_node: 124 | parent_node.add_child(node) 125 | # else: This case (parent ID on stack but not in all_nodes) shouldn't happen 126 | elif self.call_tree_root is None: 127 | # The first decorated function called when stack is empty is the root 128 | self.call_tree_root = node 129 | 130 | # --- Execute the actual function --- 131 | self.call_stack.append(node.id) 132 | result = None 133 | try: 134 | result = func(*args, **kwargs) 135 | node.return_value = result 136 | except Exception as e: 137 | print(f"Error during execution of {func_name}: {e}") # Debug print 138 | traceback.print_exc() # Print full traceback to console 139 | node.return_value = f"Error: {type(e).__name__}" 140 | # Decide whether to raise or just record error and return 141 | raise # Re-raise the exception by default to stop potentially broken execution 142 | finally: 143 | # Ensure stack is popped correctly even if errors occurred 144 | # We pop the *last* element, assuming it's the current call finishing 145 | if self.call_stack: 146 | finished_id = self.call_stack.pop() 147 | if finished_id != node.id: 148 | # This indicates a potential issue in stack management, maybe due to exceptions? 149 | print( 150 | f"Warning: Call stack mismatch! Expected {node.id}, popped {finished_id}" 151 | ) 152 | # Attempt recovery if possible, or just log 153 | # We might need to search and remove node.id if it's still there? Risky. 154 | # ------------------------------------ 155 | 156 | # Update display string *after* return value is set 157 | node.update_display_str_with_return() 158 | 159 | return result # Return the original result 160 | 161 | # Try to preserve original signature details if possible 162 | try: 163 | wrapper.__signature__ = inspect.signature(func) 164 | except ValueError: 165 | pass # Ignore if signature cannot be created 166 | wrapper.__name__ = func.__name__ # Keep original name if possible 167 | wrapper.__doc__ = func.__doc__ 168 | return wrapper 169 | 170 | def get_tree(self): 171 | return self.call_tree_root 172 | 173 | 174 | # --- 3. Tree Drawing Logic --- 175 | # (layout_tree, position_nodes, draw_tree, constants remain the same as the previous version with CANVAS_TOP_PADDING) 176 | NODE_V_SPACE = 60 177 | NODE_H_SPACE = 20 178 | NODE_PADDING_X = 8 179 | NODE_PADDING_Y = 4 180 | NODE_BORDER_WIDTH = 1 181 | NODE_FONT = ("Courier", 10) 182 | CANVAS_TOP_PADDING = 30 # Space from the top edge 183 | 184 | 185 | def get_text_width(canvas, text, font_obj): 186 | return font_obj.measure(text) 187 | 188 | 189 | def layout_tree(node, current_depth, node_font): 190 | if not node: 191 | return 0 192 | # Update width based on potentially changed display string (with return value) 193 | node.width = get_text_width(None, node.display_str, node_font) + 2 * NODE_PADDING_X 194 | node.y = current_depth * NODE_V_SPACE + CANVAS_TOP_PADDING 195 | if not node.children: 196 | node.subtree_width = node.width 197 | return node.subtree_width 198 | children_subtree_widths = [ 199 | layout_tree(child, current_depth + 1, node_font) for child in node.children 200 | ] 201 | total_children_width = ( 202 | sum(children_subtree_widths) + max(0, len(node.children) - 1) * NODE_H_SPACE 203 | ) 204 | node.subtree_width = max(node.width, total_children_width) 205 | return node.subtree_width 206 | 207 | 208 | def position_nodes(node, current_x, node_font): 209 | if not node: 210 | return 211 | node.x = current_x + (node.subtree_width / 2) 212 | if node.children: 213 | children_total_width = ( 214 | sum(child.subtree_width for child in node.children) 215 | + max(0, len(node.children) - 1) * NODE_H_SPACE 216 | ) 217 | start_x = node.x - (children_total_width / 2) 218 | current_child_x = start_x 219 | for child in node.children: 220 | position_nodes(child, current_child_x, node_font) 221 | current_child_x += child.subtree_width + NODE_H_SPACE 222 | 223 | 224 | def draw_tree(canvas, node, node_font): 225 | if not node: 226 | return 227 | text_height = node_font.metrics("linespace") 228 | half_node_h = text_height / 2 + NODE_PADDING_Y 229 | half_node_w = node.width / 2 230 | x0, y0 = node.x - half_node_w, node.y - half_node_h 231 | x1, y1 = node.x + half_node_w, node.y + half_node_h 232 | canvas.create_rectangle( 233 | x0, y0, x1, y1, fill="lightblue", outline="black", width=NODE_BORDER_WIDTH 234 | ) 235 | canvas.create_text( 236 | node.x, node.y, text=node.display_str, font=node_font, anchor=tk.CENTER 237 | ) 238 | for child in node.children: 239 | if child: 240 | child_text_height = node_font.metrics("linespace") 241 | child_half_h = child_text_height / 2 + NODE_PADDING_Y 242 | child_y0 = child.y - child_half_h 243 | canvas.create_line(node.x, y1, child.x, child_y0, fill="black", width=1) 244 | draw_tree(canvas, child, node_font) 245 | 246 | 247 | # --- Helper Function to Add Decorator --- 248 | def add_decorator_to_defs(code_string, decorator_name): 249 | """ 250 | Parses python code string and adds a decorator before each function definition. 251 | Handles basic indentation. 252 | """ 253 | lines = code_string.splitlines() 254 | modified_lines = [] 255 | func_def_pattern = re.compile(r"^(\s*)def\s+\w+\s*\(") # Matches start of func def 256 | 257 | for line in lines: 258 | match = func_def_pattern.match(line) 259 | if match: 260 | indentation = match.group(1) # Capture existing indentation 261 | modified_lines.append(f"{indentation}@{decorator_name}") 262 | modified_lines.append(line) 263 | 264 | return "\n".join(modified_lines) 265 | 266 | 267 | # --- 4. GUI Application --- 268 | class CallTreeVisualizer(tk.Tk): 269 | def __init__(self): 270 | super().__init__() 271 | self.title("Recursive Call Tree Visualizer") 272 | self.geometry("900x700") 273 | # Tracer instance belongs to the App 274 | self.tracer = RecursiveTracer() 275 | 276 | # Configure grid layout 277 | self.grid_rowconfigure(1, weight=1) # Code editor row 278 | self.grid_rowconfigure(3, weight=3) # Canvas row 279 | self.grid_columnconfigure(0, weight=1) 280 | 281 | # --- Code Input Area --- 282 | tk.Label(self, text="Enter Python Code (including recursive functions):").grid( 283 | row=0, column=0, sticky="w", padx=5, pady=2 284 | ) 285 | self.code_text = scrolledtext.ScrolledText( 286 | self, height=10, width=80, wrap=tk.WORD, font=("Consolas", 10) 287 | ) 288 | self.code_text.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") 289 | # Default text updated 290 | self.code_text.insert( 291 | tk.END, 292 | """# Example: Palindrome Check (with nested function) 293 | def is_palindrome(word): 294 | # Nested recursive helper function 295 | def check_two_sides(left, right): 296 | # Base case: Indices meet or cross 297 | if left >= right: 298 | return True 299 | # Recursive step: Check characters and move inwards 300 | elif word[left] == word[right]: 301 | # Make the recursive call 302 | return check_two_sides(left + 1, right - 1) 303 | # Base case: Mismatch found 304 | else: 305 | return False 306 | 307 | # Initial call to the helper 308 | if not word: # Handle empty string case 309 | return True 310 | return check_two_sides(0, len(word) - 1) 311 | 312 | # You can add other functions here too 313 | # def factorial(n): 314 | # if n == 0: 315 | # return 1 316 | # else: 317 | # return n * factorial(n-1) 318 | 319 | """, 320 | ) 321 | 322 | # --- Initial Call Input --- 323 | control_frame = tk.Frame(self) 324 | control_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5) 325 | 326 | tk.Label( 327 | control_frame, text="Initial Call (e.g., is_palindrome('racecar')):" 328 | ).pack(side=tk.LEFT, padx=5) 329 | self.call_entry = tk.Entry( 330 | control_frame, width=40, font=("Consolas", 10) 331 | ) # Wider entry 332 | self.call_entry.pack(side=tk.LEFT, padx=5) 333 | self.call_entry.insert(tk.END, "is_palindrome('level')") 334 | 335 | # --- Run Button --- 336 | self.run_button = tk.Button( 337 | control_frame, text="Visualize Call Tree", command=self.run_visualization 338 | ) 339 | self.run_button.pack(side=tk.LEFT, padx=10) 340 | 341 | # --- Clear Button --- 342 | self.clear_button = tk.Button( 343 | control_frame, text="Clear Canvas", command=self.clear_canvas 344 | ) 345 | self.clear_button.pack(side=tk.LEFT, padx=5) 346 | 347 | # --- Canvas for Drawing --- 348 | canvas_frame = tk.Frame(self, bd=1, relief=tk.SUNKEN) 349 | canvas_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nsew") 350 | self.canvas = tk.Canvas(canvas_frame, bg="white") 351 | self.v_scrollbar = tk.Scrollbar( 352 | canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview 353 | ) 354 | self.h_scrollbar = tk.Scrollbar( 355 | canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview 356 | ) 357 | self.canvas.configure( 358 | yscrollcommand=self.v_scrollbar.set, xscrollcommand=self.h_scrollbar.set 359 | ) 360 | self.v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) 361 | self.h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) 362 | self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 363 | self.node_font = font.Font(family=NODE_FONT[0], size=NODE_FONT[1]) 364 | 365 | def clear_canvas(self): 366 | self.canvas.delete("all") 367 | self.canvas.configure(scrollregion=(0, 0, 100, 100)) 368 | self.canvas.xview_moveto(0) 369 | self.canvas.yview_moveto(0) 370 | 371 | def run_visualization(self): 372 | self.clear_canvas() 373 | self.tracer.reset() # Reset tracer state for this run 374 | 375 | user_code = self.code_text.get("1.0", tk.END) 376 | initial_call = self.call_entry.get().strip() 377 | 378 | if not user_code or not initial_call: 379 | messagebox.showerror( 380 | "Error", "Please provide both Python code and an initial call." 381 | ) 382 | return 383 | 384 | # Define the name under which the decorator will be available in exec 385 | decorator_internal_name = "__recursive_tracer_decorator" 386 | 387 | # Prepare the execution namespace, injecting the decorator method 388 | exec_namespace = {decorator_internal_name: self.tracer.trace_calls} 389 | 390 | try: 391 | # --- Modify the user code to add the decorator --- 392 | print("--- Original Code ---") 393 | print(user_code) 394 | modified_code = add_decorator_to_defs(user_code, decorator_internal_name) 395 | print("\n--- Modified Code (with decorators) ---") 396 | print(modified_code) 397 | print("-" * 20) 398 | 399 | # --- Execute the modified code (defines decorated functions) --- 400 | # Compile first to potentially catch syntax errors earlier 401 | compiled_code = compile(modified_code, "", "exec") 402 | exec(compiled_code, exec_namespace) 403 | 404 | # --- Execute the initial call (triggers the decorated functions) --- 405 | print(f"--- Executing Initial Call: {initial_call} ---") 406 | # We execute the initial call in the *same namespace* 407 | # so it finds the decorated versions of the functions. 408 | exec(initial_call, exec_namespace) 409 | print("--- Execution Finished ---") 410 | 411 | except SyntaxError as e: 412 | messagebox.showerror( 413 | "Syntax Error", 414 | f"Error parsing code (potentially after adding decorators):\n{e}", 415 | ) 416 | print(f"Syntax Error: {e}") 417 | traceback.print_exc() 418 | return 419 | except Exception as e: 420 | messagebox.showerror( 421 | "Runtime Error", f"Error during execution:\n{type(e).__name__}: {e}" 422 | ) 423 | print(f"Runtime Error: {e}") 424 | traceback.print_exc() 425 | # Continue to attempt drawing partial tree below 426 | 427 | # --- Get the call tree --- 428 | call_tree_root = self.tracer.get_tree() 429 | 430 | if not call_tree_root: 431 | # Check if any nodes were created at all, even without a root link 432 | if not self.tracer.all_nodes: 433 | messagebox.showinfo( 434 | "Info", 435 | "No decorated function calls were traced. Was the initial call correct?", 436 | ) 437 | else: 438 | # This might happen if exec(initial_call) raised an error *before* any decorated func was entered 439 | messagebox.showwarning( 440 | "Warning", 441 | "Execution finished, but no call tree root was established. Drawing may be empty or incomplete.", 442 | ) 443 | return 444 | 445 | # --- Layout and Draw the Tree --- 446 | try: 447 | # 1. Calculate layout (this now updates node widths too) 448 | layout_tree(call_tree_root, 0, self.node_font) 449 | 450 | # 2. Position nodes (X coordinates) 451 | canvas_width_offset = 20 # Start drawing 20px from the left edge 452 | position_nodes( 453 | call_tree_root, canvas_width_offset, self.node_font 454 | ) # Pass offset only once 455 | 456 | # 3. Find overall bounds *after* positioning 457 | min_x, max_x = float("inf"), float("-inf") 458 | min_y, max_y = float("inf"), float("-inf") 459 | if ( 460 | not self.tracer.all_nodes 461 | ): # Handle case where root exists but no nodes (shouldn't happen?) 462 | messagebox.showerror( 463 | "Error", "No nodes found for drawing despite having a root." 464 | ) 465 | return 466 | 467 | for node_id, node in self.tracer.all_nodes.items(): 468 | # Check if node has valid position (might not if disconnected and not reached by position_nodes) 469 | if node.x == 0 and node.y == 0 and node is not call_tree_root: 470 | # This node might be orphaned, skip for bounds calculation? Or log warning. 471 | # print(f"Warning: Node {node.id} ({node.display_str}) has default position, may be disconnected.") 472 | continue # Skip disconnected nodes for bounds calculation 473 | 474 | text_height = self.node_font.metrics("linespace") 475 | half_node_h = text_height / 2 + NODE_PADDING_Y 476 | half_node_w = node.width / 2 # Use the possibly updated width 477 | x0 = node.x - half_node_w 478 | y0 = node.y - half_node_h 479 | x1 = node.x + half_node_w 480 | y1 = node.y + half_node_h 481 | min_x = min(min_x, x0) 482 | max_x = max(max_x, x1) 483 | min_y = min(min_y, y0) 484 | max_y = max(max_y, y1) 485 | 486 | # Handle case where only root exists or bounds are weird 487 | if min_x == float("inf"): 488 | min_x = 0 489 | if max_x == float("-inf"): 490 | max_x = 200 491 | if min_y == float("inf"): 492 | min_y = 0 493 | if max_y == float("-inf"): 494 | max_y = 100 495 | 496 | # 4. Configure scroll region 497 | padding = 30 498 | scroll_x0 = max(0, min_x - padding) 499 | scroll_y0 = max(0, min_y - padding) 500 | scroll_x1 = max_x + padding 501 | scroll_y1 = max_y + padding 502 | 503 | self.canvas.configure( 504 | scrollregion=(scroll_x0, scroll_y0, scroll_x1, scroll_y1) 505 | ) 506 | 507 | # 5. Draw the tree 508 | draw_tree(self.canvas, call_tree_root, self.node_font) 509 | 510 | except Exception as e: 511 | messagebox.showerror( 512 | "Drawing Error", 513 | f"Error laying out or drawing the tree:\n{type(e).__name__}: {e}", 514 | ) 515 | print(f"Drawing Error: {e}") 516 | traceback.print_exc() 517 | 518 | 519 | # --- Main Execution --- 520 | if __name__ == "__main__": 521 | app = CallTreeVisualizer() 522 | app.mainloop() 523 | -------------------------------------------------------------------------------- /core/recursion/recursive_palindrome_checker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks if a string is a palindrome using recursion. 3 | 4 | Parameters: 5 | word (str): The string to check. 6 | 7 | is_palindrome(word) 8 | The main recursive function that checks if word is a palindrome. 9 | 10 | check_two_sides(left, right) 11 | A recursive helper function that checks if the substring from left to right is a palindrome. 12 | 13 | Base case: 14 | if left >= right: 15 | Returns True since an empty or single-character substring is a palindrome. 16 | 17 | General case: 18 | if word[left] == word[right]: 19 | Recursively calls check_two_sides() with left + 1 and right - 1. 20 | 21 | General case: 22 | if word[left] != word[right]: 23 | Returns False since the substring is not a palindrome. 24 | 25 | # testing cases 26 | Prints the result of calling is_palindrome() with various test strings. 27 | """ 28 | 29 | # A recursive solution to check if a string is a palindrome: 30 | 31 | 32 | def is_palindrome(word): 33 | def check_two_sides(left, right): 34 | # Base case: if the left and right indices cross, we have a palindrome, 35 | if left >= right: 36 | return True 37 | # General case: if the letters at the left and right indices are the same, 38 | elif word[left] == word[right]: 39 | return check_two_sides(left + 1, right - 1) 40 | # General case: if the letters at the left and right indices are different, 41 | return False 42 | 43 | # Call the recursive function with the left and right indices. 44 | return check_two_sides(0, len(word) - 1) 45 | 46 | 47 | # testing cases 48 | print(is_palindrome("noon")) 49 | print(is_palindrome("table")) 50 | print(is_palindrome("racecar")) 51 | print(is_palindrome("tacocat")) 52 | print(is_palindrome("")) 53 | print(is_palindrome("a")) 54 | 55 | # Output: 56 | # True 57 | # False 58 | # True 59 | # True 60 | # True 61 | # True 62 | -------------------------------------------------------------------------------- /core/recursion/recursive_power_simulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates the power of a number using recursion. 3 | 4 | Parameters: 5 | value (int): The base number. 6 | number (int): The exponent. 7 | 8 | recursive_power_simulator(value, number) 9 | The main recursive function that calculates value raised to the power of number. 10 | 11 | Base case: 12 | if number == 0: 13 | Returns 1. 14 | 15 | Base case: 16 | if number == 1: 17 | Returns value. 18 | 19 | General case: 20 | Returns value * recursive_power_simulator(value, number - 1). 21 | """ 22 | 23 | 24 | # A reccursive function that calculates the power of a number with O(n) time complexity 25 | def recursive_power_simulator(value, number): 26 | if number == 0: 27 | return 1 28 | elif number == 1: 29 | return value 30 | else: 31 | return value * recursive_power_simulator(value, number - 1) 32 | 33 | 34 | # OR even better, using the following code which gives us O(log(n)) time complexity: 35 | # def recursive_power_simulator(value, number): 36 | # if number == 0: 37 | # return 1 38 | # elif number == 1: 39 | # return value 40 | # else: 41 | # half = number // 2 42 | # result = recursive_power_simulator(value, half) 43 | # if number % 2 == 0: 44 | # return result * result 45 | # else: 46 | # return value * result * result 47 | 48 | 49 | print(recursive_power_simulator(3, 3)) 50 | -------------------------------------------------------------------------------- /core/searching-algorithms/binary-search/binary_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Performs a binary search on the list my_list for the key. 3 | 4 | Parameters: 5 | my_list (list): The list to search. 6 | key (any): The value to search for. 7 | 8 | low_index (int): The lowest index where the key may be found. 9 | high_index (int): The highest index where the key may be found. 10 | 11 | While low_index is less than or equal to high_index, the search space is narrowed by calculating the mid_index. 12 | If key is found at mid_index, its index is returned. 13 | If key is less than my_list[mid_index], the search space becomes low_index to mid_index - 1. 14 | If key is greater than my_list[mid_index], the search space becomes mid_index + 1 to high_index. 15 | 16 | The search continues until low_index and high_index cross, then -1 is returned indicating the key was not found. 17 | """ 18 | 19 | 20 | def binary_search(my_list, key): 21 | # low_index stores lowest index where you might find key 22 | low_index = 0 23 | # high_index stores highest index where you might find key 24 | high_index = len(my_list) - 1 25 | 26 | # when low_index becomes bigger than high index, we stop 27 | while low_index <= high_index: 28 | # we need to find mid point 29 | mid_index = (low_index + high_index) // 2 30 | 31 | if key == my_list[mid_index]: 32 | return mid_index 33 | elif key < my_list[mid_index]: 34 | high_index = mid_index - 1 35 | else: 36 | low_index = mid_index + 1 37 | 38 | return -1 39 | 40 | 41 | print(binary_search([1, 2, 6, 14], 14)) 42 | -------------------------------------------------------------------------------- /core/searching-algorithms/binary-search/binary_search_tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A unit test to test the binary search algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the binary_search function in the binary_search.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | 11 | import unittest 12 | from binary_search import binary_search 13 | 14 | 15 | class TestBinarySearch(unittest.TestCase): 16 | def test_empty_list(self): 17 | list = [] 18 | self.assertEqual(binary_search(list, 1), -1) 19 | 20 | def test_one_element_list(self): 21 | list = [1] 22 | self.assertEqual(binary_search(list, 1), 0) 23 | 24 | def test_two_element_list(self): 25 | list = [1, 2] 26 | self.assertEqual(binary_search(list, 1), 0) 27 | self.assertEqual(binary_search(list, 2), 1) 28 | 29 | def test_three_element_list(self): 30 | list = [1, 2, 3] 31 | self.assertEqual(binary_search(list, 1), 0) 32 | self.assertEqual(binary_search(list, 2), 1) 33 | self.assertEqual(binary_search(list, 3), 2) 34 | 35 | def test_list_with_duplicate_elements(self): 36 | list = [1, 1, 2, 2, 3, 3] 37 | self.assertEqual(binary_search(list, 1), 0) 38 | self.assertEqual(binary_search(list, 2), 2) 39 | self.assertEqual(binary_search(list, 3), 4) 40 | 41 | def test_list_with_negative_elements(self): 42 | list = [-5, -4, -3, -2, -1] 43 | self.assertEqual(binary_search(list, -5), 0) 44 | self.assertEqual(binary_search(list, -4), 1) 45 | self.assertEqual(binary_search(list, -3), 2) 46 | self.assertEqual(binary_search(list, -2), 3) 47 | self.assertEqual(binary_search(list, -1), 4) 48 | 49 | def test_list_with_floating_point_numbers(self): 50 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 51 | self.assertEqual(binary_search(list, 1.1), 0) 52 | self.assertEqual(binary_search(list, 2.2), 1) 53 | self.assertEqual(binary_search(list, 3.3), 2) 54 | self.assertEqual(binary_search(list, 4.4), 3) 55 | self.assertEqual(binary_search(list, 5.5), 4) 56 | 57 | def test_list_with_strings(self): 58 | list = ["a", "b", "c", "d", "e"] 59 | self.assertEqual(binary_search(list, "a"), 0) 60 | self.assertEqual(binary_search(list, "b"), 1) 61 | self.assertEqual(binary_search(list, "c"), 2) 62 | self.assertEqual(binary_search(list, "d"), 3) 63 | self.assertEqual(binary_search(list, "e"), 4) 64 | 65 | def test_best_case(self): 66 | list = [1, 2, 3, 4, 5] 67 | self.assertEqual(binary_search(list, 3), 2) 68 | 69 | def test_worst_case(self): 70 | list = [1, 2, 3, 4, 5] 71 | self.assertEqual(binary_search(list, 6), -1) 72 | 73 | def test_average_case(self): 74 | list = [1, 2, 3, 4, 5] 75 | self.assertEqual(binary_search(list, 4), 3) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /core/searching-algorithms/linear_search/linear_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Reza Khojasteh 3 | Performs a linear search on the list my_list for the key. 4 | 5 | Parameters: 6 | my_list (list): The list to search. 7 | key (any): The value to search for. 8 | 9 | i (int): The index currently being searched. 10 | 11 | The list is iterated over from index 0 to the length of the list. 12 | If the key is found at any index, that index is returned. 13 | If the key is not found, -1 is returned. 14 | """ 15 | 16 | 17 | def linear_search(my_list, key): 18 | n = len(my_list) 19 | # iterate over the list 20 | for i in range(n): 21 | if my_list[i] == key: 22 | return i 23 | 24 | return -1 25 | 26 | 27 | print(linear_search([1, 2, 6, 4], 6)) 28 | -------------------------------------------------------------------------------- /core/searching-algorithms/linear_search/linear_search_tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A unit test to test the linear search algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the linear_search function in the linear_search.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | 11 | import unittest 12 | from linear_search import linear_search 13 | 14 | 15 | class TestLinearSearch(unittest.TestCase): 16 | def test_empty_list(self): 17 | list = [] 18 | self.assertEqual(linear_search(list, 1), -1) 19 | 20 | def test_one_element_list(self): 21 | list = [1] 22 | self.assertEqual(linear_search(list, 1), 0) 23 | 24 | def test_two_element_list(self): 25 | list = [1, 2] 26 | self.assertEqual(linear_search(list, 1), 0) 27 | self.assertEqual(linear_search(list, 2), 1) 28 | 29 | def test_three_element_list(self): 30 | list = [1, 2, 3] 31 | self.assertEqual(linear_search(list, 1), 0) 32 | self.assertEqual(linear_search(list, 2), 1) 33 | self.assertEqual(linear_search(list, 3), 2) 34 | 35 | def test_list_with_duplicate_elements(self): 36 | list = [1, 1, 2, 2, 3, 3] 37 | self.assertEqual(linear_search(list, 1), 0) 38 | self.assertEqual(linear_search(list, 2), 2) 39 | self.assertEqual(linear_search(list, 3), 4) 40 | 41 | def test_list_with_negative_elements(self): 42 | list = [-5, -4, -3, -2, -1] 43 | self.assertEqual(linear_search(list, -5), 0) 44 | self.assertEqual(linear_search(list, -4), 1) 45 | self.assertEqual(linear_search(list, -3), 2) 46 | self.assertEqual(linear_search(list, -2), 3) 47 | self.assertEqual(linear_search(list, -1), 4) 48 | 49 | def test_list_with_floating_point_numbers(self): 50 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 51 | self.assertEqual(linear_search(list, 1.1), 0) 52 | self.assertEqual(linear_search(list, 2.2), 1) 53 | self.assertEqual(linear_search(list, 3.3), 2) 54 | self.assertEqual(linear_search(list, 4.4), 3) 55 | self.assertEqual(linear_search(list, 5.5), 4) 56 | 57 | def test_list_with_strings(self): 58 | list = ["a", "b", "c", "d", "e"] 59 | self.assertEqual(linear_search(list, "a"), 0) 60 | self.assertEqual(linear_search(list, "b"), 1) 61 | self.assertEqual(linear_search(list, "c"), 2) 62 | self.assertEqual(linear_search(list, "d"), 3) 63 | self.assertEqual(linear_search(list, "e"), 4) 64 | 65 | def test_best_case(self): 66 | list = [1, 2, 3, 4, 5] 67 | self.assertEqual(linear_search(list, 1), 0) 68 | 69 | def test_worst_case(self): 70 | list = [1, 2, 3, 4, 5] 71 | self.assertEqual(linear_search(list, 6), -1) 72 | 73 | def test_average_case(self): 74 | list = [1, 2, 3, 4, 5] 75 | self.assertEqual(linear_search(list, 3), 2) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /core/sorting-algorithms/bubble-sort/bubble-sort-tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A unit test to test the bubble sort algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the bubble_sort function in the bubble_sort.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | import unittest 11 | from bubble_sort import bubble_sort 12 | 13 | 14 | # a class to test the bubble sort algorithm 15 | class TestBubbleSort(unittest.TestCase): 16 | # test cases for the empty list 17 | def test_empty_list(self): 18 | list = [] 19 | bubble_sort(list) 20 | self.assertEqual(list, []) 21 | 22 | # test cases for the one element list 23 | def test_one_element_list(self): 24 | list = [1] 25 | bubble_sort(list) 26 | self.assertEqual(list, [1]) 27 | 28 | # test cases for the two element list 29 | def test_two_element_list(self): 30 | list = [2, 1] 31 | bubble_sort(list) 32 | self.assertEqual(list, [1, 2]) 33 | 34 | # test cases for the three element list 35 | def test_three_element_list(self): 36 | list = [3, 2, 1] 37 | bubble_sort(list) 38 | self.assertEqual(list, [1, 2, 3]) 39 | 40 | # test cases for the list with duplicate elements 41 | def test_list_with_duplicate_elements(self): 42 | list = [1, 2, 3, 3, 2, 1] 43 | bubble_sort(list) 44 | self.assertEqual(list, [1, 1, 2, 2, 3, 3]) 45 | 46 | # test cases for the list with negative elements 47 | def test_list_with_negative_elements(self): 48 | list = [-1, -2, -3, -4, -5] 49 | bubble_sort(list) 50 | self.assertEqual(list, [-5, -4, -3, -2, -1]) 51 | 52 | # test cases for the list with floating point numbers 53 | def test_list_with_floating_point_numbers(self): 54 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 55 | bubble_sort(list) 56 | self.assertEqual(list, [1.1, 2.2, 3.3, 4.4, 5.5]) 57 | 58 | # test cases for the list with strings 59 | def test_list_with_strings(self): 60 | list = ["a", "b", "c", "d", "e"] 61 | bubble_sort(list) 62 | self.assertEqual(list, ["a", "b", "c", "d", "e"]) 63 | 64 | # test cases for the best case, when the array is already sorted 65 | def test_best_case(self): 66 | list = [1, 2, 3, 4, 5] 67 | bubble_sort(list) 68 | self.assertEqual(list, [1, 2, 3, 4, 5]) 69 | 70 | # test cases for the worst case, when the array is sorted in the reverse order 71 | def test_worst_case(self): 72 | list = [5, 4, 3, 2, 1] 73 | bubble_sort(list) 74 | self.assertEqual(list, [1, 2, 3, 4, 5]) 75 | 76 | # test cases for the average case, when the array is sorted in the random order 77 | def test_average_case(self): 78 | list = [3, 5, 1, 2, 4] 79 | bubble_sort(list) 80 | self.assertEqual(list, [1, 2, 3, 4, 5]) 81 | 82 | 83 | # run the unit test if the file is executed directly (not imported as a module) 84 | if __name__ == "__main__": 85 | # run the unit test and check the results of the test cases 86 | unittest.main() 87 | -------------------------------------------------------------------------------- /core/sorting-algorithms/bubble-sort/bubble_sort.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A simple program to sort a list of numbers using the bubble sort algorithm with O(n^2) for the worst case scenario 4 | 5 | 6 | # Version 1 7 | 8 | 9 | # def bubble_sort(list): 10 | # n = len(list) 11 | # for i in range(n - 1): 12 | # for j in range(n - 1): 13 | # if list[j] > list[j + 1]: 14 | # list[j + 1], list[j] = list[j], list[j + 1] 15 | # # print(list) 16 | 17 | # Version 2 18 | 19 | 20 | # def bubble_sort(list): 21 | # n = len(list) 22 | # for i in range(n - 1): 23 | # for j in range(n - 1 - i): 24 | # if list[j] > list[j + 1]: 25 | # list[j + 1], list[j] = list[j], list[j + 1] 26 | # # print(list) 27 | 28 | # Version 3 (with Ω(n) for the best case scenario) 29 | 30 | 31 | # def bubble_sort(list): 32 | # n = len(list) 33 | 34 | # for i in range(n - 1): 35 | # swapped = False 36 | 37 | # for j in range(n - 1 - i): 38 | # if list[j] > list[j + 1]: 39 | # list[j + 1], list[j] = list[j], list[j + 1] 40 | # swapped = True 41 | 42 | # if not swapped: 43 | # break 44 | 45 | 46 | # Version 4 (the same as version 3, just in case you want to avoid using the 'break' statement) 47 | 48 | 49 | def bubble_sort(list): 50 | # set the n to the length of the list 51 | n = len(list) 52 | # i is playing the role of a counter which shows the number of passes/iterations we have done so far 53 | i = 0 54 | # set the swapped to True so that we can enter the while loop at least once 55 | swapped = True 56 | # as long as we haven't done the n - 1 steps/iterations and the list is not yet sorted, enter the while loop and do another iteration 57 | while i < n - 1 and swapped: 58 | # set the swapped to False so that we can exit the outer while loop if we don't do any swapping in this iteration 59 | swapped = False 60 | # for each element in the list, compare it with the next element and swap them if they are in the wrong order 61 | # j goes from 0 to n - 2 - i, because in each pass, we will have one less element to compare with 62 | for j in range(n - 1 - i): 63 | # if the current element is greater than the next element, swap them 64 | if list[j] > list[j + 1]: 65 | list[j + 1], list[j] = list[j], list[j + 1] 66 | # set the swapped back to True so that we still continue running the outer while loop 67 | swapped = True 68 | # do the next (potential) step/iteration of the outer while loop 69 | i += 1 70 | -------------------------------------------------------------------------------- /core/sorting-algorithms/insertion-sort/insertion-sort-tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A unit test to test the insertion sort algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the insertion_sort function in the insertion_sort.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | import unittest 11 | from insertion_sort import insertion_sort 12 | 13 | 14 | # a class to test the insertion sort algorithm 15 | class TestInsertionSort(unittest.TestCase): 16 | # a test case for the empty list 17 | def test_empty_list(self): 18 | list = [] 19 | insertion_sort(list) 20 | self.assertEqual(list, []) 21 | 22 | def test_one_element_list(self): 23 | list = [1] 24 | insertion_sort(list) 25 | self.assertEqual(list, [1]) 26 | 27 | def test_two_element_list(self): 28 | list = [2, 1] 29 | insertion_sort(list) 30 | self.assertEqual(list, [1, 2]) 31 | 32 | def test_three_element_list(self): 33 | list = [3, 2, 1] 34 | insertion_sort(list) 35 | self.assertEqual(list, [1, 2, 3]) 36 | 37 | def test_list_with_duplicate_elements(self): 38 | list = [1, 2, 3, 3, 2, 1] 39 | insertion_sort(list) 40 | self.assertEqual(list, [1, 1, 2, 2, 3, 3]) 41 | 42 | def test_list_with_negative_elements(self): 43 | list = [-1, -2, -3, -4, -5] 44 | insertion_sort(list) 45 | self.assertEqual(list, [-5, -4, -3, -2, -1]) 46 | 47 | def test_list_with_floating_point_numbers(self): 48 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 49 | insertion_sort(list) 50 | self.assertEqual(list, [1.1, 2.2, 3.3, 4.4, 5.5]) 51 | 52 | def test_list_with_strings(self): 53 | list = ["a", "b", "c", "d", "e"] 54 | insertion_sort(list) 55 | self.assertEqual(list, ["a", "b", "c", "d", "e"]) 56 | 57 | def test_best_case(self): 58 | list = [1, 2, 3, 4, 5] 59 | insertion_sort(list) 60 | self.assertEqual(list, [1, 2, 3, 4, 5]) 61 | 62 | def test_worst_case(self): 63 | list = [5, 4, 3, 2, 1] 64 | insertion_sort(list) 65 | self.assertEqual(list, [1, 2, 3, 4, 5]) 66 | 67 | def test_average_case(self): 68 | list = [5, 1, 4, 2, 3] 69 | insertion_sort(list) 70 | self.assertEqual(list, [1, 2, 3, 4, 5]) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /core/sorting-algorithms/insertion-sort/insertion_sort.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A simple program to sort a list of numbers using the insertion sort algorithm with O(n^2) for the worst case scenario 4 | 5 | 6 | def insertion_sort(my_list): 7 | n = len(my_list) 8 | for i in range(1, n): 9 | # store the first number/value in the unsorted part of the list into curr 10 | curr = my_list[i] 11 | # store the index of the first number/value in the unsorted part of the list into j 12 | j = i 13 | # this loop might shift values within the sorted part of the list to open a spot for curr 14 | while j > 0 and my_list[j - 1] > curr: 15 | # shift the value at the index to the right 16 | my_list[j] = my_list[j - 1] 17 | j -= 1 18 | # insert curr at the index 19 | my_list[j] = curr 20 | -------------------------------------------------------------------------------- /core/sorting-algorithms/merge-sort/merge-sort-tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-27 3 | # Description: A unit test to test the merge sort algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the merge_sort function in the merge_sort.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | import unittest 11 | from merge_sort import merge_sort 12 | 13 | 14 | class TestmergeSort(unittest.TestCase): 15 | def test_empty_list(self): 16 | list = [] 17 | merge_sort(list) 18 | self.assertEqual(list, []) 19 | 20 | def test_one_element_list(self): 21 | list = [1] 22 | merge_sort(list) 23 | self.assertEqual(list, [1]) 24 | 25 | def test_two_element_list(self): 26 | list = [2, 1] 27 | merge_sort(list) 28 | self.assertEqual(list, [1, 2]) 29 | 30 | def test_three_element_list(self): 31 | list = [3, 2, 1] 32 | merge_sort(list) 33 | self.assertEqual(list, [1, 2, 3]) 34 | 35 | def test_list_with_duplicate_elements(self): 36 | list = [1, 2, 3, 3, 2, 1] 37 | merge_sort(list) 38 | self.assertEqual(list, [1, 1, 2, 2, 3, 3]) 39 | 40 | def test_list_with_negative_elements(self): 41 | list = [-1, -2, -3, -4, -5] 42 | merge_sort(list) 43 | self.assertEqual(list, [-5, -4, -3, -2, -1]) 44 | 45 | def test_list_with_floating_point_numbers(self): 46 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 47 | merge_sort(list) 48 | self.assertEqual(list, [1.1, 2.2, 3.3, 4.4, 5.5]) 49 | 50 | def test_list_with_strings(self): 51 | list = ["a", "b", "c", "d", "e"] 52 | merge_sort(list) 53 | self.assertEqual(list, ["a", "b", "c", "d", "e"]) 54 | 55 | def test_best_case(self): 56 | list = [1, 2, 3, 4, 5] 57 | merge_sort(list) 58 | self.assertEqual(list, [1, 2, 3, 4, 5]) 59 | 60 | def test_worst_case(self): 61 | list = [5, 4, 3, 2, 1] 62 | merge_sort(list) 63 | self.assertEqual(list, [1, 2, 3, 4, 5]) 64 | 65 | def test_average_case(self): 66 | list = [5, 1, 4, 2, 3] 67 | merge_sort(list) 68 | self.assertEqual(list, [1, 2, 3, 4, 5]) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /core/sorting-algorithms/merge-sort/merge_sort.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-27 3 | # Merge sort is a divide and conquer algorithm. It divides the input array into two halves, calls itself for the two halves, and then merges the two sorted halves. The merge() function is used for merging two halves. The merge(arr, l, m, r) is the key process that assumes that arr[l..m] and arr[m+1..r] are sorted and merges the two sorted sub-arrays into one. 4 | # Merge sort requires O(n*log(n)) time for the worst case and O(n*log(n)) extra space. 5 | 6 | # def merge_sort(my_list): 7 | # n = len(my_list) 8 | # if n > 1: 9 | # mid = n // 2 10 | # left = my_list[:mid] 11 | # right = my_list[mid:] 12 | # merge_sort(left) 13 | # merge_sort(right) 14 | # i = j = k = 0 15 | # while i < len(left) and j < len(right): 16 | # if left[i] < right[j]: 17 | # my_list[k] = left[i] 18 | # i += 1 19 | # else: 20 | # my_list[k] = right[j] 21 | # j += 1 22 | # k += 1 23 | # while i < len(left): 24 | # my_list[k] = left[i] 25 | # i += 1 26 | # k += 1 27 | # while j < len(right): 28 | # my_list[k] = right[j] 29 | # j += 1 30 | # k += 1 31 | # # print(my_list) # uncomment this line to see the steps of the merge sort algorithm 32 | 33 | # merge_sort([4, 2, 3, 1]) # uncomment this line to see the steps of the merge sort algorithm 34 | 35 | 36 | # OR even better, with O(n*log(n)) time for the worst case and O(n) extra space: 37 | def merge_sort(my_list): 38 | # create an empty list for merging. doing it once 39 | # is more efficient than repeatedly creating it when merging 40 | empty_list = [0] * len(my_list) 41 | 42 | # call recursive_merge_sort 43 | recursive_merge_sort(my_list, 0, len(my_list) - 1, empty_list) 44 | 45 | 46 | def recursive_merge_sort(my_list, first_index, last_index, empty_list): 47 | # recursive merge sort. the base case occurs when first_index >= last_index 48 | # that situation will only occur if the portion of the list is size 0 or 1. 49 | # in either case, do nothing and exit function. 50 | 51 | if first_index < last_index: 52 | mid_index = (first_index + last_index) // 2 53 | recursive_merge_sort(my_list, first_index, mid_index, empty_list) 54 | recursive_merge_sort(my_list, mid_index + 1, last_index, empty_list) 55 | merge(my_list, first_index, mid_index + 1, last_index, empty_list) 56 | 57 | 58 | # this function will merge two sorted pieces of an array into a single sorted segment. 59 | # We will refer to the two smaller sorted pieces of lists as a and b. 60 | # These are not true lists but rather pieces of my_list 61 | # list a starts at a_first_index and ends at b_first_index - 1 (inclusive) 62 | # list b starts at b_first_index and ends at b_last_index (inclusive) 63 | # this function will merge them together using empty_list as temporary storage 64 | # once it is merged together, it is copied back into my_list, creating a single 65 | # sorted segment that starts at a_first_index to b_last_index (inclusive) 66 | 67 | 68 | def merge(my_list, a_first_index, b_first_index, b_last_index, empty_list): 69 | a_ptr = a_first_index # used to track value from a 70 | b_ptr = b_first_index 71 | empty_list_index = a_ptr 72 | 73 | while (a_ptr < b_first_index) and (b_ptr <= b_last_index): 74 | if my_list[a_ptr] <= my_list[b_ptr]: 75 | empty_list[empty_list_index] = my_list[a_ptr] 76 | empty_list_index += 1 77 | a_ptr += 1 78 | else: 79 | empty_list[empty_list_index] = my_list[b_ptr] 80 | empty_list_index += 1 81 | b_ptr += 1 82 | 83 | while a_ptr < b_first_index: 84 | empty_list[empty_list_index] = my_list[a_ptr] 85 | empty_list_index += 1 86 | a_ptr += 1 87 | 88 | while b_ptr <= b_last_index: 89 | empty_list[empty_list_index] = my_list[b_ptr] 90 | empty_list_index += 1 91 | b_ptr += 1 92 | 93 | for i in range(a_first_index, b_last_index + 1): 94 | my_list[i] = empty_list[i] 95 | -------------------------------------------------------------------------------- /core/sorting-algorithms/quick-sort/quick-sort-tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-30 3 | # Description: A unit test to test the quick sort algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the quick_sort function in the quick_sort.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | import unittest 11 | from quick_sort import quick_sort 12 | 13 | 14 | class TestQuickSort(unittest.TestCase): 15 | def test_empty_list(self): 16 | list = [] 17 | quick_sort(list) 18 | self.assertEqual(list, []) 19 | 20 | def test_one_element_list(self): 21 | list = [1] 22 | quick_sort(list) 23 | self.assertEqual(list, [1]) 24 | 25 | def test_two_element_list(self): 26 | list = [2, 1] 27 | quick_sort(list) 28 | self.assertEqual(list, [1, 2]) 29 | 30 | def test_three_element_list(self): 31 | list = [3, 2, 1] 32 | quick_sort(list) 33 | self.assertEqual(list, [1, 2, 3]) 34 | 35 | def test_list_with_duplicate_elements(self): 36 | list = [1, 2, 3, 3, 2, 1] 37 | quick_sort(list) 38 | self.assertEqual(list, [1, 1, 2, 2, 3, 3]) 39 | 40 | def test_list_with_negative_elements(self): 41 | list = [-1, -2, -3, -4, -5] 42 | quick_sort(list) 43 | self.assertEqual(list, [-5, -4, -3, -2, -1]) 44 | 45 | def test_list_with_floating_point_numbers(self): 46 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 47 | quick_sort(list) 48 | self.assertEqual(list, [1.1, 2.2, 3.3, 4.4, 5.5]) 49 | 50 | def test_list_with_strings(self): 51 | list = ["a", "b", "c", "d", "e"] 52 | quick_sort(list) 53 | self.assertEqual(list, ["a", "b", "c", "d", "e"]) 54 | 55 | def test_best_case(self): 56 | list = [1, 2, 3, 4, 5] 57 | quick_sort(list) 58 | self.assertEqual(list, [1, 2, 3, 4, 5]) 59 | 60 | def test_worst_case(self): 61 | list = [5, 4, 3, 2, 1] 62 | quick_sort(list) 63 | self.assertEqual(list, [1, 2, 3, 4, 5]) 64 | 65 | def test_average_case(self): 66 | list = [5, 1, 4, 2, 3] 67 | quick_sort(list) 68 | self.assertEqual(list, [1, 2, 3, 4, 5]) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /core/sorting-algorithms/quick-sort/quick_sort.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-30 3 | # Quick sort is a divide and conquer algorithm and requires O(n^2) for the worst case and O(n*log(n)) for the best and average case. 4 | # Regarding space complexity, it requires O(log(n)) for the best case and O(n) for the worst and average case. 5 | # The worst case occurs when the partition process always picks greatest or smallest element as pivot. 6 | # The best case occurs when the partition process always picks the middle element as pivot. 7 | # The average case occurs when the pivot is chosen randomly. 8 | # The worst case occurs when the partition process always picks greatest or smallest element as pivot. 9 | 10 | import random 11 | 12 | 13 | # def quick_sort(my_list): 14 | # n = len(my_list) 15 | # if n > 1: 16 | # recursive_quick_sort(my_list, 0, n - 1) 17 | 18 | 19 | # def recursive_quick_sort(my_list, start=0, end=None): 20 | # # uncomment the following two 'print(my_list)' statements and the last commented out ones 21 | # # to see the outcome of the quick sort algorithm, after each swap! 22 | 23 | # if start < end: 24 | # # choose a random index between start and end inclusive, to get the pivot_element 25 | # pivot_location = random.randint(start, end) 26 | 27 | # # move the pivot_element out of the way by swapping it with the last value of the partition 28 | # my_list[pivot_location], my_list[end] = my_list[end], my_list[pivot_location] 29 | 30 | # pivot = end 31 | # pivot_element = my_list[end] 32 | # i = start 33 | # j = end - 1 34 | # while i <= j: 35 | # while i <= j and my_list[i] <= pivot_element: 36 | # i += 1 37 | # while i <= j and my_list[j] > pivot_element: 38 | # j -= 1 39 | # if i < j: 40 | # my_list[i], my_list[j] = my_list[j], my_list[i] 41 | # i += 1 42 | # j -= 1 43 | # # print(my_list) 44 | # if i != pivot: 45 | # my_list[i], my_list[pivot] = my_list[pivot], my_list[i] 46 | # # print(my_list) 47 | # recursive_quick_sort(my_list, 0, i - 1) 48 | # recursive_quick_sort(my_list, i + 1, end) 49 | 50 | 51 | # OR, using insertion sort for small partitions: 52 | def quick_sort(my_list): 53 | # call recursive quicksort 54 | recursive_quick_sort(my_list, 0, len(my_list) - 1) 55 | 56 | 57 | def recursive_quick_sort(my_list, left, right, THRESHOLD=32): 58 | if right - left <= THRESHOLD: 59 | insertion_sort(my_list, left, right) 60 | else: 61 | pivot_position = partition(my_list, left, right) 62 | recursive_quick_sort(my_list, left, pivot_position - 1) 63 | recursive_quick_sort(my_list, pivot_position + 1, right) 64 | 65 | 66 | def insertion_sort(my_list, left, right): 67 | for i in range(left + 1, right + 1): 68 | # store the first number in the unsorted part of array into curr 69 | curr = my_list[i] 70 | j = i 71 | # the following loop shifts value within sorted part of array to open a spot for curr 72 | while j > left and my_list[j - 1] > curr: 73 | my_list[j] = my_list[j - 1] 74 | j = j - 1 75 | my_list[j] = curr 76 | 77 | 78 | def partition(my_list, left, right): 79 | # choose a random index between left and right inclusive 80 | pivot_location = random.randint(left, right) 81 | 82 | # get the pivot 83 | pivot = my_list[pivot_location] 84 | 85 | # move the pivot out of the way by swapping with 86 | # last value of partition. This step is crucial as pivot will 87 | # end up "moving" if we don't get it out of the way which will 88 | # lead to inconsistent results. 89 | my_list[pivot_location] = my_list[right] 90 | my_list[right] = pivot 91 | 92 | end_of_smaller = left - 1 93 | 94 | # note the loop below does not look at pivot which is in my_list[right] 95 | for j in range(left, right): 96 | if my_list[j] <= pivot: 97 | end_of_smaller += 1 98 | my_list[end_of_smaller], my_list[j] = my_list[j], my_list[end_of_smaller] 99 | 100 | # restore the pivot 101 | my_list[end_of_smaller + 1], my_list[right] = ( 102 | my_list[right], 103 | my_list[end_of_smaller + 1], 104 | ) 105 | 106 | # and return its location 107 | return end_of_smaller + 1 108 | 109 | 110 | # my_list = [5, 4, 3, 2, 1] 111 | # print("Before Sorting: ", my_list) 112 | # quick_sort(my_list) 113 | # print("After Sorting: ", my_list) 114 | -------------------------------------------------------------------------------- /core/sorting-algorithms/selection-sort/selection-sort-tester.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A unit test to test the selection sort algorithm 4 | 5 | # This is a unit test composed of different test cases to check the correctness of the the selection_sort function in the selection_sort.py file (in the same directory) 6 | # the test cases cover all the possible scenarios, including the best case, the worst case, and the average case 7 | # the test cases also cover the edge cases, such as an empty list, a list with one element, a list with two elements, and a list with three elements 8 | # the test cases also cover the edge cases, such as a list with duplicate elements, a list with negative elements, a list with floating point numbers, and a list with strings 9 | 10 | import unittest 11 | from selection_sort import selection_sort 12 | 13 | 14 | class TestSelectionSort(unittest.TestCase): 15 | def test_empty_list(self): 16 | list = [] 17 | selection_sort(list) 18 | self.assertEqual(list, []) 19 | 20 | def test_one_element_list(self): 21 | list = [1] 22 | selection_sort(list) 23 | self.assertEqual(list, [1]) 24 | 25 | def test_two_element_list(self): 26 | list = [2, 1] 27 | selection_sort(list) 28 | self.assertEqual(list, [1, 2]) 29 | 30 | def test_three_element_list(self): 31 | list = [3, 2, 1] 32 | selection_sort(list) 33 | self.assertEqual(list, [1, 2, 3]) 34 | 35 | def test_list_with_duplicate_elements(self): 36 | list = [1, 2, 3, 3, 2, 1] 37 | selection_sort(list) 38 | self.assertEqual(list, [1, 1, 2, 2, 3, 3]) 39 | 40 | def test_list_with_negative_elements(self): 41 | list = [-1, -2, -3, -4, -5] 42 | selection_sort(list) 43 | self.assertEqual(list, [-5, -4, -3, -2, -1]) 44 | 45 | def test_list_with_floating_point_numbers(self): 46 | list = [1.1, 2.2, 3.3, 4.4, 5.5] 47 | selection_sort(list) 48 | self.assertEqual(list, [1.1, 2.2, 3.3, 4.4, 5.5]) 49 | 50 | def test_list_with_strings(self): 51 | list = ["a", "b", "c", "d", "e"] 52 | selection_sort(list) 53 | self.assertEqual(list, ["a", "b", "c", "d", "e"]) 54 | 55 | def test_best_case(self): 56 | list = [1, 2, 3, 4, 5] 57 | selection_sort(list) 58 | self.assertEqual(list, [1, 2, 3, 4, 5]) 59 | 60 | def test_worst_case(self): 61 | list = [5, 4, 3, 2, 1] 62 | selection_sort(list) 63 | self.assertEqual(list, [1, 2, 3, 4, 5]) 64 | 65 | def test_average_case(self): 66 | list = [3, 5, 1, 2, 4] 67 | selection_sort(list) 68 | self.assertEqual(list, [1, 2, 3, 4, 5]) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /core/sorting-algorithms/selection-sort/selection_sort.py: -------------------------------------------------------------------------------- 1 | # Author: Reza Khojasteh 2 | # Date: 2023-01-23 3 | # Description: A simple program to sort a list of numbers using the selection sort algorithm with O(n^2) for the worst case scenario 4 | 5 | 6 | def selection_sort(my_list): 7 | n = len(my_list) 8 | for i in range(n - 1): 9 | min_idx = i # record the index of the smallest value, initialized with where the smallest value may be found 10 | for j in range(i + 1, n): # go through list, 11 | if my_list[j] < my_list[min_idx]: # and every time we find a smaller value, 12 | min_idx = j # record its index 13 | # in case there is really a need to swap, swap the smallest value with the value at the current index 14 | if min_idx != i: 15 | my_list[min_idx], my_list[i] = my_list[i], my_list[min_idx] 16 | -------------------------------------------------------------------------------- /core/strings-n-slicing/strings-n-slicing.md: -------------------------------------------------------------------------------- 1 | # Strings and Slicing in Python 2 | 3 | Strings in Python are sequences of characters. We can access substrings within a string using indexing and slicing. 4 | 5 | ## Indexing 6 | We can access individual characters in a string using indexing. Indexes start at 0 for the first character. 7 | 8 | ```python 9 | name = "Python" 10 | first = name[0] # 'P' 11 | second = name[1] # 'y' 12 | ``` 13 | 14 | ## Slicing 15 | Slicing allows accessing a substring by specifying a start and end index separated by a colon. The start index is inclusive and the end index is exclusive. 16 | 17 | ```python 18 | name = "Python" 19 | first_three = name[0:3] # 'Pyt' 20 | last_three = name[3:6] # 'hon' 21 | ``` 22 | The start index is inclusive and the end index is exclusive. Omitting the end index slices to the end of the string. 23 | 24 | ```python 25 | name[2:] # 'thon' 26 | ``` 27 | 28 | Negative indexes slice from the end of the string. So `name[-1]` is the last character. 29 | 30 | ```python 31 | name[-3:] # 'hon' 32 | ``` 33 | 34 | ## Stride 35 | An optional third index specifies the stride (step size) to use when slicing. The default stride is 1. 36 | 37 | ```python 38 | name[::2] # 'Pto' 39 | ``` 40 | Slicing is a useful technique for extracting and manipulating substrings in Python. We'll see it being used in many different contexts as we learn more about Python. -------------------------------------------------------------------------------- /core/strings-n-slicing/strings-n-slicing.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code demonstrates various string operations and slicing in Python. 3 | """ 4 | 5 | sample_string = "Hello World" 6 | 7 | # len() is a built-in function that returns the length of the string 8 | print(len(sample_string)) 9 | # Prints 11 10 | 11 | # __len__() is a magic method that returns the length of the string 12 | print(sample_string.__len__()) 13 | # Prints 11 14 | 15 | print(sample_string[0]) 16 | # Prints H 17 | 18 | print(sample_string[10]) 19 | # Prints d 20 | 21 | # print(sample_string[11]) 22 | # IndexError: string index out of range 23 | 24 | print(sample_string[-1]) 25 | # Prints d 26 | 27 | print(sample_string[-2]) 28 | # Prints l 29 | 30 | print(sample_string[-11]) 31 | # Prints H 32 | 33 | # print(sample_string[-12]) 34 | # IndexError: string index out of range 35 | 36 | print(sample_string[0:2]) 37 | # Prints He 38 | 39 | print(sample_string[0:11]) 40 | # Prints Hello World 41 | 42 | print(sample_string[0:12]) 43 | # Prints Hello World 44 | 45 | print(sample_string[0:100]) 46 | # Prints Hello World 47 | 48 | print(sample_string[0:]) 49 | # Prints Hello World 50 | 51 | print(sample_string[-11:-1]) 52 | # Prints Hello Worl 53 | 54 | print(sample_string[-11:0]) 55 | # Prints nothing 56 | 57 | print(sample_string[-11:11]) 58 | # Prints Hello World 59 | 60 | print(sample_string[-11:12]) 61 | # Prints Hello World 62 | 63 | print(sample_string[-11:100]) 64 | # Prints Hello World 65 | 66 | print(sample_string[-100:100]) 67 | # Prints Hello World 68 | 69 | print(sample_string[: len(sample_string)]) 70 | # Prints Hello World 71 | 72 | print(sample_string[:]) 73 | # Prints Hello World 74 | 75 | print(sample_string[0:11:1]) 76 | # Prints Hello World 77 | 78 | print(sample_string[0:11:2]) 79 | # Prints HloWrd 80 | 81 | print(sample_string[0:11:3]) 82 | # Prints HlWl 83 | 84 | print(sample_string[-11:11:1]) 85 | # Prints Hello World 86 | 87 | print(sample_string[:11:1]) 88 | # Prints Hello World 89 | 90 | print(sample_string[0::1]) 91 | # Prints Hello World 92 | 93 | print(sample_string[::1]) 94 | # Prints Hello World 95 | 96 | print(sample_string[::]) 97 | # Prints Hello World 98 | 99 | print(sample_string[0:11:-1]) 100 | # Prints nothing 101 | 102 | print(sample_string[11:0:-1]) 103 | # Prints dlroW olle 104 | 105 | print(sample_string[11:-1:-1]) 106 | # Prints nothing 107 | 108 | print(sample_string[11::-1]) 109 | # Prints dlroW olleH 110 | 111 | print(sample_string[::-1]) 112 | # Prints dlroW olleH 113 | -------------------------------------------------------------------------------- /core/sum-to-goal/sum-to-goal.py: -------------------------------------------------------------------------------- 1 | # A simple program to find the (first) two (distinct) numbers in a list that add up to a given goal number 2 | # Author: Reza Khojasteh 3 | 4 | # # O(n^2) solution (wrong solution!) 5 | # def sum_to_goal(goal, list_of_numbers): 6 | # for i in list_of_numbers: 7 | # for j in list_of_numbers: 8 | # if i + j == goal: 9 | # return i, j 10 | 11 | # return -1 12 | 13 | 14 | # # O(n^2) solution (still wrong, be careful!) 15 | # def sum_to_goal(goal, list_of_numbers): 16 | # for i in list_of_numbers: 17 | # for j in list_of_numbers: 18 | # if i + j == goal and list_of_numbers.index(i) != list_of_numbers.index(j): 19 | # return i, j 20 | 21 | # return -1 22 | 23 | 24 | # # O(n^2) solution (and still wrong!) 25 | # def sum_to_goal(goal, list_of_numbers): 26 | # n = len(list_of_numbers) 27 | 28 | # for i in range(n): 29 | # for j in range(n): 30 | # if list_of_numbers[i] + list_of_numbers[j] == goal: 31 | # return list_of_numbers[i], list_of_numbers[j] 32 | 33 | # return -1 34 | 35 | 36 | # O(n^2) solution (now, correct!) 37 | # def sum_to_goal(goal, list_of_numbers): 38 | # n = len(list_of_numbers) 39 | 40 | # for i in range(n - 1): 41 | # for j in range(i + 1, n): 42 | # if list_of_numbers[i] + list_of_numbers[j] == goal: 43 | # return list_of_numbers[i], list_of_numbers[j] 44 | 45 | # return -1 46 | 47 | 48 | # O(nlogn) solution 49 | # def sum_to_goal(goal, list_of_numbers): 50 | # list_of_numbers.sort() 51 | 52 | # i = 0 53 | # j = len(list_of_numbers) - 1 54 | 55 | # while i < j: 56 | # if list_of_numbers[i] + list_of_numbers[j] == goal: 57 | # return list_of_numbers[i], list_of_numbers[j] 58 | # elif list_of_numbers[i] + list_of_numbers[j] > goal: 59 | # j -= 1 60 | # else: 61 | # i += 1 62 | 63 | # return -1 64 | 65 | # O(n) solution 66 | # def sum_to_goal(goal, list_of_numbers): 67 | # dictionary_of_numbers = {} 68 | 69 | # for i in list_of_numbers: 70 | # if dictionary_of_numbers.get(goal - i) is not None: 71 | # return dictionary_of_numbers[goal - i], i 72 | # else: 73 | # dictionary_of_numbers[i] = i 74 | 75 | # return -1 76 | 77 | # Or even better (to save space in storing entries as we don't need to store indices here, 78 | # like in a case where we don't want to return the index of those two numbers in the list): 79 | """ 80 | The idea is to use a set to store the numbers we have seen so far. 81 | For each number, we check if the difference between the goal and the current number is in the set. 82 | If it is, we return the difference and the current number. 83 | If it is not, we add the current number to the set. 84 | """ 85 | 86 | 87 | def sum_to_goal(goal, list_of_numbers): 88 | set_of_numbers = ( 89 | set() 90 | ) # and not {} as that is reserved for creating empty dicts only! 91 | 92 | for i in list_of_numbers: 93 | # check if the difference between the goal and the current number is in the set 94 | if goal - i in set_of_numbers: 95 | # if it is, return the difference and the current number as a tuple 96 | return goal - i, i 97 | # if not, add the current number to the set 98 | else: 99 | set_of_numbers.add(i) 100 | 101 | # if we reach here, it means we didn't find a pair of numbers that add up to the goal 102 | return -1 103 | 104 | 105 | print(sum_to_goal(9, [1, 3, 5, 4, 5])) 106 | -------------------------------------------------------------------------------- /core/types/types.md: -------------------------------------------------------------------------------- 1 | # Data Types in Python 2 | 3 | Python has several built-in data types that are an important part of the language. Here are some of the main data types: 4 | 5 | ## Numeric Types 6 | 7 | - **int** - Integer values like 1, 2, 3 etc. 8 | - **float** - Floating point numbers with decimal points like 1.5, 2.25 etc. 9 | - **complex** - Complex numbers with real and imaginary parts like 1+2j. 10 | 11 | ## Sequence Types 12 | 13 | - **str** - Strings of Unicode characters like "hello". 14 | - **list** - Ordered sequence of objects like [1, 2, 3]. 15 | - **tuple** - Immutable ordered sequence of objects like (1, 2, 3). 16 | 17 | ## Set Types 18 | 19 | - **set** - Unordered collection of unique objects like {1, 2, 3}. 20 | - **frozenset** - Immutable form of set. 21 | 22 | ## Mapping Type 23 | 24 | - **dict** - Collection of key-value pairs like {"name": "John", "age": 30}. 25 | 26 | ## Boolean Type 27 | 28 | - **bool** - Logical value indicating True or False. 29 | 30 | ## None Type 31 | 32 | - **NoneType** - Special object indicating null or absence of a value. 33 | 34 | These built-in types provide the basic building blocks for data manipulation and storage in Python. Understanding how to use them effectively is key to writing good Python code. -------------------------------------------------------------------------------- /core/types/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Domonstration of the different types in Python. 3 | """ 4 | print(type(None)) 5 | # Prints 6 | 7 | print(type(True)) 8 | # Prints 9 | 10 | print(type(5)) 11 | # Prints 12 | 13 | print(type(5.5)) 14 | # Prints 15 | 16 | print(type(5 + 5j)) 17 | # Prints 18 | 19 | print(type('Hello World!')) 20 | # Prints 21 | 22 | print(type([1, 2, 3])) 23 | # Prints 24 | 25 | print(type((1, 2, 3))) 26 | # Prints 27 | 28 | print(type({1, 2, 3})) 29 | # Prints 30 | 31 | print(type(frozenset({1, 2, 3}))) 32 | # Prints 33 | 34 | print(type({'name': 'John Doe'})) 35 | # Prints 36 | 37 | print(type(range(5))) 38 | # Prints 39 | 40 | print(type(type)) 41 | # Prints -------------------------------------------------------------------------------- /core/working-with-csv-files/csv_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reads data from a CSV file and prints the row for Albert Einstein. 3 | 4 | Parameters: 5 | - laureates.csv: The CSV file containing the data. 6 | 7 | Returns: None 8 | 9 | Functionality: 10 | - Opens the CSV file "laureates.csv" for reading. 11 | - Creates a DictReader object to read the data into dictionaries. 12 | - Converts the DictReader into a list of dictionaries, one per row. 13 | - Loops through the rows, checking if the "surname" is "Einstein". 14 | - If a match is found, prints that row's dictionary. 15 | - Exits the loop after printing the match. 16 | """ 17 | 18 | import csv 19 | import pprint 20 | 21 | try: 22 | # open the file for reading 23 | with open("laureates.csv", "r") as f: 24 | # create a DictReader object to read the data into dictionaries 25 | reader = csv.DictReader(f) 26 | # convert the DictReader into a list of dictionaries, one per row 27 | laureates = list(reader) 28 | # loop through the rows 29 | for laureate in laureates: 30 | # check if the "surname" is "Einstein" 31 | if laureate["surname"] == "Einstein": 32 | # print the row's dictionary using pprint 33 | pprint.pprint(laureate) 34 | break 35 | except FileNotFoundError: 36 | print("The file 'laureates.csv' does not exist!") 37 | -------------------------------------------------------------------------------- /core/working-with-datetime/using_datetime.py: -------------------------------------------------------------------------------- 1 | """ 2 | Opens the laureates.csv file and reads its contents into a list of dictionaries. 3 | 4 | Parameters: 5 | f (file object): The opened laureates.csv file. 6 | 7 | Returns: None 8 | 9 | Functionality: 10 | reader (csv.DictReader): Reads the rows of the CSV file into dictionaries. 11 | laureates (list): A list of the dictionaries, one per row. 12 | 13 | Loops through the rows, checking if the "surname" is "Einstein". 14 | If a match is found, it calculates and prints Einstein's age when he received the prize. 15 | 16 | year_date (datetime): The year the prize was awarded, parsed from the "year" column. 17 | born_date (datetime): Einstein's birth date, parsed from the "born" column. 18 | age (int): Einstein's age when he received the prize, calculated by subtracting born_date.year from year_date.year. 19 | 20 | Exits the loop after finding the first match. 21 | """ 22 | 23 | import csv 24 | from datetime import datetime 25 | 26 | # Open the laureates.csv file and read its contents into a list of dictionaries 27 | with open("laureates.csv", "r") as f: 28 | reader = csv.DictReader(f) 29 | laureates = list(reader) 30 | # Loop through the rows, checking if the "surname" is "Einstein" 31 | for laureate in laureates: 32 | if laureate["surname"] == "Einstein": 33 | print((laureate)) 34 | # datetime.strptime() converts a string into a datetime object according to the format specifier you provide. 35 | year_date = datetime.strptime(laureate["year"], "%Y") 36 | print((year_date)) 37 | born_date = datetime.strptime(laureate["born"], "%Y-%m-%d") 38 | print((born_date)) 39 | # Calculate and print Einstein's age when he received the prize 40 | print( 41 | f"(Roughly) Age at which they received the prize: {year_date.year - born_date.year}" 42 | ) 43 | break 44 | --------------------------------------------------------------------------------