├── .github ├── flake8_matcher.json ├── lint-requirements.txt └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── chapter_01 ├── p01_is_unique.py ├── p02_check_permutation.py ├── p03_urlify.py ├── p04_palindrome_permutation.py ├── p05_one_away.py ├── p06_string_compression.py ├── p07_rotate_matrix.py ├── p08_zero_matrix.py └── p09_string_rotation.py ├── chapter_02 ├── __init__.py ├── linked_list.py ├── p01_remove_dups.py ├── p02_return_kth_to_last.py ├── p03_delete_middle_node.py ├── p04_partition.py ├── p05_sum_lists.py ├── p06_palindrome.py ├── p07_intersection.py └── p08_loop_detection.py ├── chapter_03 ├── __init__.py ├── p01_three_in_one.py ├── p02_stack_min.py ├── p03_stack_of_plates.py ├── p04_queue_via_stacks.py ├── p05_sort_stack.py ├── p06_animal_shelter.py └── stack.py ├── chapter_04 ├── binary_search_tree.py ├── binary_tree.py ├── p01_route_between_nodes.py ├── p02_minimal_tree.py ├── p03_list_of_depths.py ├── p04_check_balanced.py ├── p05_validate_bst.py ├── p06_successor.py ├── p07_build_order.py ├── p08_first_common_ancestor.py ├── p09_bst_sequences.py ├── p10_check_subtree.py ├── p11_random_node.py └── p12_paths_with_sum.py ├── chapter_05 ├── p01_insertion.py ├── p02_binary_to_string.py ├── p03_flip_bit_to_win.py ├── p04_next_number.py ├── p06_conversion.py ├── p07_pairwise_swap.py └── p08_draw_line.py ├── chapter_06 ├── p07_the_apocalypse.py └── p10_poison.py ├── chapter_07 └── p04_parking_lot.py ├── chapter_08 ├── p01_triple_step.py ├── p02_robot_grid.py ├── p03_magic_index.py ├── p04_power_set.py ├── p05_recursive_multiply.py ├── p06_towers_of_hanoi.py ├── p07_permutations_without_dups.py ├── p08_permutations_with_dups.py ├── p09_parens.py ├── p10_paint_fill.py ├── p11_coins.py ├── p12_eight_queens.py ├── p13_tallest_stack.py └── p14_boolean_evaluation.py ├── chapter_10 ├── p01_sorted_merge.py ├── p02_group_anagrams.py ├── p03_search_in_rotated_array.py ├── p04_search_sorted_no_size_array.py └── p05_sparse_search.py ├── chapter_16 ├── p01_number_swapper.py ├── p02_word_frequencies.py ├── p06_smallest_difference.py ├── p08_english_int.py ├── p19_pond_sizes.py └── p26_calculator.py ├── chapter_17 ├── p01_add_without_plus.py ├── p02_shuffle.py ├── p07_baby_names.py ├── p08_circus_tower.py ├── p09_kth_multiple.py ├── p15_longest_word.py ├── p16_the_masseuse.py ├── p17_multi_search.py ├── p18_shortest_supersequence.py ├── p21_volume_of_histogram.py └── p22_word_transformer.py └── tox.ini /.github/flake8_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "lint-error", 5 | "severity": "error", 6 | "pattern": [ 7 | { 8 | "regexp": "^([^:]*):(\\d+):(\\d+): ([A-Z]{1,3}\\d\\d\\d) (.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "column": 3, 12 | "code": 4, 13 | "message": 5 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.github/lint-requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | flake8-builtins 3 | flake8-comprehensions 4 | flake8-eradicate 5 | flake8-isort 6 | flake8-mutable 7 | flake8-printf-formatting 8 | flake8-pytest-style 9 | flake8-use-fstring 10 | flake8-variables-names 11 | isort 12 | pep8-naming 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python Checks 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.7 16 | - name: Install test dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | python -m pip install -r .github/lint-requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | echo "::add-matcher::.github/flake8_matcher.json" 23 | flake8 --count --statistics --show-source --append-config=tox.ini . 24 | autoformat: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-python@v2 29 | - name: Install test dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install black==22.3.0 33 | - name: Autoformatter 34 | run: | 35 | black --diff --check . 36 | test: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | python-version: [3.7, 3.9] 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | - name: Install test dependencies 48 | run: | 49 | python -m pip install --upgrade pip 50 | python -m pip install pytest pytest-randomly 51 | - name: Test with pytest 52 | run: | 53 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 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 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | .idea 65 | .python-version 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Solutions to *Cracking the Coding Interview, 6th Edition* 2 | 3 | These are **Python** solutions for the book [Cracking the Coding Interview, 6th Edition](https://www.careercup.com/book) by *Gayle Laakmann McDowell*. 4 | 5 | ## How to use? 6 | 7 | To run the programs, just use the `python chapter_X/filename.py` command. 8 | 9 | To run the tests: `pip install pytest` and `pytest` 10 | 11 | ## Contributions 12 | 13 | Contributions welcome! Please submit separate pull requests for each solution you work on. 14 | 15 | In general solutions should fall into one of the following three categories: 16 | - *algorithm demonstration*. This is the primary type of problem and solution that the text is concerned with. As 17 | such, solutions should not use standard library functions in cases that would make it unnecessary to implement the 18 | algorithm. The goal of these solutions should be to have an easy to understand solution that demonstrates 19 | understanding of the algorithm. 20 | - *python demonstration*. We also accept solutions that solve the problem in a more practical way, using whatever 21 | standard library functions are available. Please do not use any third party dependencies. These solutions should 22 | also be easy to understand and good examples of pythonic ways of doing things. 23 | - *speed demonstration*. These alternative solutions may be accepted if the fastest way to do something is not very 24 | readable or intuitive and thus it doesn't fit into the first two categories. 25 | 26 | If you want to do everything really well, here are some guidelines. Solutions should: 27 | - work with Python 3.6 or greater 28 | - not depend on third-party libraries (like `numpy`) 29 | - follow [python style conventions](https://www.python.org/dev/peps/pep-0008/) 30 | - lower_case_with_underscores for everything except classes 31 | - descriptive, longer variable names 32 | - be formatted using the [`black`](https://black.readthedocs.io/en/stable/) code formatter 33 | - include tests to prove they work. [pytest](https://docs.pytest.org/en/stable/) is supported 34 | - have a clean commit history ideally following the 35 | [angular commit message convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#type) 36 | and including the problem being worked on in parenthesis. For example `feature(C01_P04): added solution`. The C01_P04 37 | referring to Chapter 1, Problem 4. Look at our [commit history](https://github.com/careercup/CtCI-6th-Edition-Python/commits/master) for more examples: 38 | 39 | We'll still work with your contributions even if they don't follow these guidelines so don't let that stop you. 40 | -------------------------------------------------------------------------------- /chapter_01/p01_is_unique.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from collections import defaultdict 4 | 5 | 6 | def is_unique_chars_algorithmic(string): 7 | # Assuming character set is ASCII (128 characters) 8 | if len(string) > 128: 9 | return False 10 | 11 | # this is a pythonic and faster way to initialize an array with a fixed value. 12 | # careful though it won't work for a doubly nested array 13 | char_set = [False] * 128 14 | for char in string: 15 | val = ord(char) 16 | if char_set[val]: 17 | # Char already found in string 18 | return False 19 | char_set[val] = True 20 | 21 | return True 22 | 23 | 24 | def is_unique_chars_pythonic(string): 25 | return len(set(string)) == len(string) 26 | 27 | 28 | def is_unique_bit_vector(string): 29 | """Uses bitwise operation instead of extra data structures.""" 30 | # Assuming character set is ASCII (128 characters) 31 | if len(string) > 128: 32 | return False 33 | 34 | checker = 0 35 | for c in string: 36 | val = ord(c) 37 | if (checker & (1 << val)) > 0: 38 | return False 39 | checker |= 1 << val 40 | return True 41 | 42 | 43 | def is_unique_chars_using_dictionary(string: str) -> bool: 44 | character_counts = {} 45 | for char in string: 46 | if char in character_counts: 47 | return False 48 | character_counts[char] = 1 49 | return True 50 | 51 | 52 | def is_unique_chars_using_set(string: str) -> bool: 53 | characters_seen = set() 54 | for char in string: 55 | if char in characters_seen: 56 | return False 57 | characters_seen.add(char) 58 | return True 59 | 60 | 61 | # O(NlogN) 62 | def is_unique_chars_sorting(string: str) -> bool: 63 | sorted_string = sorted(string) 64 | last_character = None 65 | for char in sorted_string: 66 | if char == last_character: 67 | return False 68 | last_character = char 69 | return True 70 | 71 | 72 | # Sorting without extra variable. TC: O(NlogN) SC: O(1) Con: Modifies input string 73 | def is_unique_chars_sort(string: str) -> bool: 74 | string = sorted(string) 75 | for i in range(len(string) - 1): 76 | if string[i] == string[i + 1]: 77 | return False 78 | return True 79 | 80 | 81 | class Test(unittest.TestCase): 82 | test_cases = [ 83 | ("abcd", True), 84 | ("s4fad", True), 85 | ("", True), 86 | ("23ds2", False), 87 | ("hb 627jh=j ()", False), 88 | ("".join([chr(val) for val in range(128)]), True), # unique 128 chars 89 | ("".join([chr(val // 2) for val in range(129)]), False), # non-unique 129 chars 90 | ] 91 | test_functions = [ 92 | is_unique_chars_pythonic, 93 | is_unique_chars_algorithmic, 94 | is_unique_bit_vector, 95 | is_unique_chars_using_dictionary, 96 | is_unique_chars_using_set, 97 | is_unique_chars_sorting, 98 | is_unique_chars_sort, 99 | ] 100 | 101 | def test_is_unique_chars(self): 102 | num_runs = 1000 103 | function_runtimes = defaultdict(float) 104 | 105 | for _ in range(num_runs): 106 | for text, expected in self.test_cases: 107 | for is_unique_chars in self.test_functions: 108 | start = time.perf_counter() 109 | assert ( 110 | is_unique_chars(text) == expected 111 | ), f"{is_unique_chars.__name__} failed for value: {text}" 112 | function_runtimes[is_unique_chars.__name__] += ( 113 | time.perf_counter() - start 114 | ) * 1000 115 | 116 | print(f"\n{num_runs} runs") 117 | for function_name, runtime in function_runtimes.items(): 118 | print(f"{function_name}: {runtime:.1f}ms") 119 | 120 | 121 | if __name__ == "__main__": 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /chapter_01/p02_check_permutation.py: -------------------------------------------------------------------------------- 1 | # O(N) 2 | import unittest 3 | from collections import Counter 4 | 5 | 6 | def check_permutation_by_sort(s1, s2): 7 | if len(s1) != len(s2): 8 | return False 9 | s1, s2 = sorted(s1), sorted(s2) 10 | for i in range(len(s1)): 11 | if s1[i] != s2[i]: 12 | return False 13 | return True 14 | 15 | 16 | def check_permutation_by_count(str1, str2): 17 | if len(str1) != len(str2): 18 | return False 19 | counter = [0] * 256 20 | for c in str1: 21 | counter[ord(c)] += 1 22 | for c in str2: 23 | if counter[ord(c)] == 0: 24 | return False 25 | counter[ord(c)] -= 1 26 | return True 27 | 28 | 29 | def check_permutation_pythonic(str1, str2): 30 | # short-circuit to avoid instantiating a Counter which for big strings 31 | # may be an expensive operation 32 | if len(str1) != len(str2): 33 | return False 34 | 35 | return Counter(str1) == Counter(str2) 36 | 37 | 38 | class Test(unittest.TestCase): 39 | # str1, str2, is_permutation 40 | test_cases = ( 41 | ("dog", "god", True), 42 | ("abcd", "bacd", True), 43 | ("3563476", "7334566", True), 44 | ("wef34f", "wffe34", True), 45 | ("dogx", "godz", False), 46 | ("abcd", "d2cba", False), 47 | ("2354", "1234", False), 48 | ("dcw4f", "dcw5f", False), 49 | ("DOG", "dog", False), 50 | ("dog ", "dog", False), 51 | ("aaab", "bbba", False), 52 | ) 53 | 54 | testable_functions = [ 55 | check_permutation_by_sort, 56 | check_permutation_by_count, 57 | check_permutation_pythonic, 58 | ] 59 | 60 | def test_cp(self): 61 | # true check 62 | for check_permutation in self.testable_functions: 63 | for str1, str2, expected in self.test_cases: 64 | assert check_permutation(str1, str2) == expected 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /chapter_01/p03_urlify.py: -------------------------------------------------------------------------------- 1 | # O(N) 2 | import unittest 3 | 4 | 5 | def urlify_algo(string, length): 6 | """replace spaces with %20 and removes trailing spaces""" 7 | # convert to list because Python strings are immutable 8 | char_list = list(string) 9 | new_index = len(char_list) 10 | 11 | for i in reversed(range(length)): 12 | if char_list[i] == " ": 13 | # Replace spaces 14 | char_list[new_index - 3 : new_index] = "%20" 15 | new_index -= 3 16 | else: 17 | # Move characters 18 | char_list[new_index - 1] = char_list[i] 19 | new_index -= 1 20 | # convert back to string 21 | return "".join(char_list[new_index:]) 22 | 23 | 24 | def urlify_pythonic(text, length): 25 | """solution using standard library""" 26 | return text[:length].replace(" ", "%20") 27 | 28 | 29 | class Test(unittest.TestCase): 30 | """Test Cases""" 31 | 32 | test_cases = { 33 | ("much ado about nothing ", 22): "much%20ado%20about%20nothing", 34 | ("Mr John Smith ", 13): "Mr%20John%20Smith", 35 | (" a b ", 4): "%20a%20b", 36 | (" a b ", 5): "%20a%20b%20", 37 | } 38 | testable_functions = [urlify_algo, urlify_pythonic] 39 | 40 | def test_urlify(self): 41 | for urlify in self.testable_functions: 42 | for args, expected in self.test_cases.items(): 43 | actual = urlify(*args) 44 | assert actual == expected, f"Failed {urlify.__name__} for: {[*args]}" 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /chapter_01/p04_palindrome_permutation.py: -------------------------------------------------------------------------------- 1 | # O(N) 2 | import string 3 | import unittest 4 | from collections import Counter 5 | 6 | 7 | def clean_phrase(phrase): 8 | return [c for c in phrase.lower() if c in string.ascii_lowercase] 9 | 10 | 11 | def is_palindrome_permutation(phrase): 12 | """checks if a string is a permutation of a palindrome""" 13 | table = [0 for _ in range(ord("z") - ord("a") + 1)] 14 | countodd = 0 15 | for c in phrase: 16 | x = char_number(c) 17 | if x != -1: 18 | table[x] += 1 19 | if table[x] % 2: 20 | countodd += 1 21 | else: 22 | countodd -= 1 23 | 24 | return countodd <= 1 25 | 26 | 27 | def char_number(c): 28 | a = ord("a") 29 | z = ord("z") 30 | upper_a = ord("A") 31 | upper_z = ord("Z") 32 | val = ord(c) 33 | 34 | if a <= val <= z: 35 | return val - a 36 | 37 | if upper_a <= val <= upper_z: 38 | return val - upper_a 39 | return -1 40 | 41 | 42 | def is_palindrome_bit_vector(phrase): 43 | """checks if a string is a permutation of a palindrome""" 44 | r = 0 45 | for c in clean_phrase(phrase): 46 | val = ord(c) 47 | mask = 1 << val 48 | if r & mask: 49 | r &= ~mask 50 | else: 51 | r |= mask 52 | return (r - 1) & r == 0 53 | 54 | 55 | def is_palindrome_bit_vector2(phrase): 56 | """checks if a string is a permutation of a palindrome using XOR operation""" 57 | count_odd = 0 58 | for c in phrase: 59 | val = char_number(c) 60 | if val == -1: 61 | continue 62 | count_odd ^= 1 << val 63 | 64 | return count_odd & count_odd - 1 == 0 65 | 66 | 67 | def is_palindrome_permutation_pythonic(phrase): 68 | """function checks if a string is a permutation of a palindrome or not""" 69 | counter = Counter(clean_phrase(phrase)) 70 | return sum(val % 2 for val in counter.values()) <= 1 71 | 72 | 73 | class Test(unittest.TestCase): 74 | test_cases = [ 75 | ("aba", True), 76 | ("aab", True), 77 | ("abba", True), 78 | ("aabb", True), 79 | ("a-bba", True), 80 | ("a-bba!", True), 81 | ("Tact Coa", True), 82 | ("jhsabckuj ahjsbckj", True), 83 | ("Able was I ere I saw Elba", True), 84 | ("So patient a nurse to nurse a patient so", False), 85 | ("Random Words", False), 86 | ("Not a Palindrome", False), 87 | ("no x in nixon", True), 88 | ("azAZ", True), 89 | ] 90 | testable_functions = [ 91 | is_palindrome_permutation, 92 | is_palindrome_bit_vector, 93 | is_palindrome_permutation_pythonic, 94 | is_palindrome_bit_vector2, 95 | ] 96 | 97 | def test_pal_perm(self): 98 | for f in self.testable_functions: 99 | for [test_string, expected] in self.test_cases: 100 | assert f(test_string) == expected 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /chapter_01/p05_one_away.py: -------------------------------------------------------------------------------- 1 | # O(N) 2 | import time 3 | import unittest 4 | 5 | 6 | def are_one_edit_different(s1, s2): 7 | """Check if a string can converted to another string with a single edit""" 8 | if len(s1) == len(s2): 9 | return one_edit_replace(s1, s2) 10 | if len(s1) + 1 == len(s2): 11 | return one_edit_insert(s1, s2) 12 | if len(s1) - 1 == len(s2): 13 | return one_edit_insert(s2, s1) # noqa 14 | return False 15 | 16 | 17 | def one_edit_replace(s1, s2): 18 | edited = False 19 | for c1, c2 in zip(s1, s2): 20 | if c1 != c2: 21 | if edited: 22 | return False 23 | edited = True 24 | return True 25 | 26 | 27 | def one_edit_insert(s1, s2): 28 | edited = False 29 | i, j = 0, 0 30 | while i < len(s1) and j < len(s2): 31 | if s1[i] != s2[j]: 32 | if edited: 33 | return False 34 | edited = True 35 | j += 1 36 | else: 37 | i += 1 38 | j += 1 39 | return True 40 | 41 | 42 | class Test(unittest.TestCase): 43 | test_cases = [ 44 | # no changes 45 | ("pale", "pale", True), 46 | ("", "", True), 47 | # one insert 48 | ("pale", "ple", True), 49 | ("ple", "pale", True), 50 | ("pales", "pale", True), 51 | ("ples", "pales", True), 52 | ("pale", "pkle", True), 53 | ("paleabc", "pleabc", True), 54 | ("", "d", True), 55 | ("d", "de", True), 56 | # one replace 57 | ("pale", "bale", True), 58 | ("a", "b", True), 59 | ("pale", "ble", False), 60 | # multiple replace 61 | ("pale", "bake", False), 62 | # insert and replace 63 | ("pale", "pse", False), 64 | ("pale", "pas", False), 65 | ("pas", "pale", False), 66 | ("pkle", "pable", False), 67 | ("pal", "palks", False), 68 | ("palks", "pal", False), 69 | # permutation with insert shouldn't match 70 | ("ale", "elas", False), 71 | ] 72 | 73 | testable_functions = [are_one_edit_different] 74 | 75 | def test_one_away(self): 76 | 77 | for f in self.testable_functions: 78 | start = time.perf_counter() 79 | for _ in range(100): 80 | for [text_a, text_b, expected] in self.test_cases: 81 | assert f(text_a, text_b) == expected 82 | duration = time.perf_counter() - start 83 | print(f"{f.__name__} {duration * 1000:.1f}ms") 84 | 85 | 86 | if __name__ == "__main__": 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /chapter_01/p06_string_compression.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | 5 | def compress_string(string): 6 | compressed = [] 7 | counter = 0 8 | 9 | for i in range(len(string)): # noqa 10 | if i != 0 and string[i] != string[i - 1]: 11 | compressed.append(string[i - 1] + str(counter)) 12 | counter = 0 13 | counter += 1 14 | 15 | # add last repeated character 16 | if counter: 17 | compressed.append(string[-1] + str(counter)) 18 | 19 | # returns original string if compressed string isn't smaller 20 | return min(string, "".join(compressed), key=len) 21 | 22 | 23 | class Test(unittest.TestCase): 24 | test_cases = [ 25 | ("aabcccccaaa", "a2b1c5a3"), 26 | ("abcdef", "abcdef"), 27 | ("aabb", "aabb"), 28 | ("aaa", "a3"), 29 | ("a", "a"), 30 | ("", ""), 31 | ] 32 | testable_functions = [ 33 | compress_string, 34 | ] 35 | 36 | def test_string_compression(self): 37 | for f in self.testable_functions: 38 | start = time.perf_counter() 39 | for _ in range(1000): 40 | for test_string, expected in self.test_cases: 41 | assert f(test_string) == expected 42 | duration = time.perf_counter() - start 43 | print(f"{f.__name__} {duration * 1000:.1f}ms") 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /chapter_01/p07_rotate_matrix.py: -------------------------------------------------------------------------------- 1 | # O(NxN) 2 | import unittest 3 | from copy import deepcopy 4 | 5 | 6 | def rotate_matrix(matrix): 7 | """rotates a matrix 90 degrees clockwise""" 8 | n = len(matrix) 9 | for layer in range(n // 2): 10 | first, last = layer, n - layer - 1 11 | for i in range(first, last): 12 | # save top 13 | top = matrix[layer][i] 14 | 15 | # left -> top 16 | matrix[layer][i] = matrix[-i - 1][layer] 17 | 18 | # bottom -> left 19 | matrix[-i - 1][layer] = matrix[-layer - 1][-i - 1] 20 | 21 | # right -> bottom 22 | matrix[-layer - 1][-i - 1] = matrix[i][-layer - 1] 23 | 24 | # top -> right 25 | matrix[i][-layer - 1] = top 26 | return matrix 27 | 28 | 29 | def rotate_matrix_double_swap(matrix): 30 | n = len(matrix) 31 | for i in range(n): 32 | for j in range(i, n): 33 | temp = matrix[i][j] 34 | matrix[i][j] = matrix[j][i] 35 | matrix[j][i] = temp 36 | 37 | for i in range(n): 38 | for j in range(int(n / 2)): 39 | temp = matrix[i][j] 40 | matrix[i][j] = matrix[i][n - 1 - j] 41 | matrix[i][n - 1 - j] = temp 42 | return matrix 43 | 44 | 45 | def rotate_matrix_pythonic(matrix): 46 | """rotates a matrix 90 degrees clockwise""" 47 | n = len(matrix) 48 | result = [[0] * n for i in range(n)] # empty list of 0s 49 | for i, j in zip(range(n), range(n - 1, -1, -1)): # i counts up, j counts down 50 | for k in range(n): 51 | result[k][i] = matrix[j][k] 52 | return result 53 | 54 | 55 | def rotate_matrix_pythonic_alternate(matrix): 56 | """rotates a matrix 90 degrees clockwise""" 57 | return [list(reversed(row)) for row in zip(*matrix)] 58 | 59 | 60 | class Test(unittest.TestCase): 61 | 62 | test_cases = [ 63 | ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[7, 4, 1], [8, 5, 2], [9, 6, 3]]), 64 | ( 65 | [ 66 | [1, 2, 3, 4, 5], 67 | [6, 7, 8, 9, 10], 68 | [11, 12, 13, 14, 15], 69 | [16, 17, 18, 19, 20], 70 | [21, 22, 23, 24, 25], 71 | ], 72 | [ 73 | [21, 16, 11, 6, 1], 74 | [22, 17, 12, 7, 2], 75 | [23, 18, 13, 8, 3], 76 | [24, 19, 14, 9, 4], 77 | [25, 20, 15, 10, 5], 78 | ], 79 | ), 80 | ] 81 | testable_functions = [ 82 | rotate_matrix_pythonic, 83 | rotate_matrix, 84 | rotate_matrix_pythonic_alternate, 85 | rotate_matrix_double_swap, 86 | ] 87 | 88 | def test_rotate_matrix(self): 89 | for f in self.testable_functions: 90 | for [test_matrix, expected] in self.test_cases: 91 | test_matrix = deepcopy(test_matrix) 92 | assert f(test_matrix) == expected 93 | 94 | 95 | if __name__ == "__main__": 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /chapter_01/p08_zero_matrix.py: -------------------------------------------------------------------------------- 1 | # O(MxN) 2 | import unittest 3 | from copy import deepcopy 4 | 5 | 6 | def zero_matrix(matrix): 7 | m = len(matrix) 8 | n = len(matrix[0]) 9 | rows = set() 10 | cols = set() 11 | 12 | for x in range(m): 13 | for y in range(n): 14 | if matrix[x][y] == 0: 15 | rows.add(x) 16 | cols.add(y) 17 | 18 | for x in range(m): 19 | for y in range(n): 20 | if (x in rows) or (y in cols): 21 | matrix[x][y] = 0 22 | 23 | return matrix 24 | 25 | 26 | def zero_matrix_pythonic(matrix): 27 | matrix = [["X" if x == 0 else x for x in row] for row in matrix] 28 | indices = [] 29 | for idx, row in enumerate(matrix): 30 | if "X" in row: 31 | indices = indices + [i for i, j in enumerate(row) if j == "X"] 32 | matrix[idx] = [0] * len(matrix[0]) 33 | matrix = [[0 if row.index(i) in indices else i for i in row] for row in matrix] 34 | return matrix 35 | 36 | 37 | class Test(unittest.TestCase): 38 | 39 | test_cases = [ 40 | ( 41 | [ 42 | [1, 2, 3, 4, 0], 43 | [6, 0, 8, 9, 10], 44 | [11, 12, 13, 14, 15], 45 | [16, 0, 18, 19, 20], 46 | [21, 22, 23, 24, 25], 47 | ], 48 | [ 49 | [0, 0, 0, 0, 0], 50 | [0, 0, 0, 0, 0], 51 | [11, 0, 13, 14, 0], 52 | [0, 0, 0, 0, 0], 53 | [21, 0, 23, 24, 0], 54 | ], 55 | ) 56 | ] 57 | testable_functions = [zero_matrix, zero_matrix_pythonic] 58 | 59 | def test_zero_matrix(self): 60 | for f in self.testable_functions: 61 | for [test_matrix, expected] in self.test_cases: 62 | test_matrix = deepcopy(test_matrix) 63 | assert f(test_matrix) == expected 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /chapter_01/p09_string_rotation.py: -------------------------------------------------------------------------------- 1 | # O(N) 2 | import unittest 3 | 4 | 5 | def string_rotation(s1, s2): 6 | if len(s1) == len(s2) != 0: 7 | return s2 in s1 * 2 8 | return False 9 | 10 | 11 | class Test(unittest.TestCase): 12 | 13 | test_cases = [ 14 | ("waterbottle", "erbottlewat", True), 15 | ("foo", "bar", False), 16 | ("foo", "foofoo", False), 17 | ] 18 | 19 | def test_string_rotation(self): 20 | for [s1, s2, expected] in self.test_cases: 21 | actual = string_rotation(s1, s2) 22 | assert actual == expected 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /chapter_02/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/careercup/CtCI-6th-Edition-Python/653e50ccfa927179526879273062628996f392c1/chapter_02/__init__.py -------------------------------------------------------------------------------- /chapter_02/linked_list.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class LinkedListNode: 5 | def __init__(self, value, next_node=None, prev_node=None): 6 | self.value = value 7 | self.next = next_node 8 | self.prev = prev_node 9 | 10 | def __str__(self): 11 | return str(self.value) 12 | 13 | 14 | class LinkedList: 15 | def __init__(self, values=None): 16 | self.head = None 17 | self.tail = None 18 | if values is not None: 19 | self.add_multiple(values) 20 | 21 | def __iter__(self): 22 | current = self.head 23 | while current: 24 | yield current 25 | current = current.next 26 | 27 | def __str__(self): 28 | values = [str(x) for x in self] 29 | return " -> ".join(values) 30 | 31 | def __len__(self): 32 | result = 0 33 | node = self.head 34 | while node: 35 | result += 1 36 | node = node.next 37 | return result 38 | 39 | def values(self): 40 | return [x.value for x in self] 41 | 42 | def add(self, value): 43 | if self.head is None: 44 | self.tail = self.head = LinkedListNode(value) 45 | else: 46 | self.tail.next = LinkedListNode(value) 47 | self.tail = self.tail.next 48 | return self.tail 49 | 50 | def add_to_beginning(self, value): 51 | if self.head is None: 52 | self.tail = self.head = LinkedListNode(value) 53 | else: 54 | self.head = LinkedListNode(value, self.head) 55 | return self.head 56 | 57 | def add_multiple(self, values): 58 | for v in values: 59 | self.add(v) 60 | 61 | @classmethod 62 | def generate(cls, k, min_value, max_value): 63 | return cls(random.choices(range(min_value, max_value), k=k)) 64 | 65 | 66 | class DoublyLinkedList(LinkedList): 67 | def add(self, value): 68 | if self.head is None: 69 | self.tail = self.head = LinkedListNode(value) 70 | else: 71 | self.tail.next = LinkedListNode(value, None, self.tail) 72 | self.tail = self.tail.next 73 | return self 74 | -------------------------------------------------------------------------------- /chapter_02/p01_remove_dups.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from chapter_02.linked_list import LinkedList 4 | 5 | 6 | def remove_dups(ll): 7 | current = ll.head 8 | previous = None 9 | seen = set() 10 | 11 | while current: 12 | if current.value in seen: 13 | previous.next = current.next 14 | else: 15 | seen.add(current.value) 16 | previous = current 17 | current = current.next 18 | ll.tail = previous 19 | return ll 20 | 21 | 22 | def remove_dups_followup(ll): 23 | runner = current = ll.head 24 | while current: 25 | runner = current 26 | while runner.next: 27 | if runner.next.value == current.value: 28 | runner.next = runner.next.next 29 | else: 30 | runner = runner.next 31 | current = current.next 32 | ll.tail = runner 33 | return ll 34 | 35 | 36 | testable_functions = (remove_dups, remove_dups_followup) 37 | test_cases = ( 38 | ([], []), 39 | ([1, 1, 1, 1, 1, 1], [1]), 40 | ([1, 2, 3, 2], [1, 2, 3]), 41 | ([1, 2, 2, 3], [1, 2, 3]), 42 | ([1, 1, 2, 3], [1, 2, 3]), 43 | ([1, 2, 3], [1, 2, 3]), 44 | ) 45 | 46 | 47 | def test_remove_dupes(): 48 | for f in testable_functions: 49 | start = time.perf_counter() 50 | for _ in range(100): 51 | for values, expected in test_cases: 52 | expected = expected.copy() 53 | deduped = f(LinkedList(values)) 54 | assert deduped.values() == expected 55 | 56 | deduped.add(5) 57 | expected.append(5) 58 | assert deduped.values() == expected 59 | 60 | duration = time.perf_counter() - start 61 | print(f"{f.__name__} {duration * 1000:.1f}ms") 62 | 63 | 64 | def example(): 65 | ll = LinkedList.generate(100, 0, 9) 66 | print(ll) 67 | remove_dups(ll) 68 | print(ll) 69 | 70 | ll = LinkedList.generate(100, 0, 9) 71 | print(ll) 72 | remove_dups_followup(ll) 73 | print(ll) 74 | 75 | 76 | if __name__ == "__main__": 77 | example() 78 | -------------------------------------------------------------------------------- /chapter_02/p02_return_kth_to_last.py: -------------------------------------------------------------------------------- 1 | from chapter_02.linked_list import LinkedList 2 | 3 | 4 | def kth_to_last(ll, k): 5 | leader = follower = ll.head 6 | count = 0 7 | 8 | while leader: 9 | if count >= k: 10 | follower = follower.next 11 | count += 1 12 | leader = leader.next 13 | return follower 14 | 15 | 16 | # O(N) space 17 | def kth_last_recursive(ll, k): 18 | head = ll.head 19 | counter = 0 20 | 21 | def helper(head, k): 22 | nonlocal counter 23 | if not head: 24 | return None 25 | helper_node = helper(head.next, k) 26 | counter = counter + 1 27 | if counter == k: 28 | return head 29 | return helper_node 30 | 31 | return helper(head, k) 32 | 33 | 34 | test_cases = ( 35 | # list, k, expected 36 | ((10, 20, 30, 40, 50), 1, 50), 37 | ((10, 20, 30, 40, 50), 5, 10), 38 | ) 39 | 40 | 41 | def test_kth_to_last(): 42 | for linked_list_values, k, expected in test_cases: 43 | ll = LinkedList(linked_list_values) 44 | assert kth_to_last(ll, k).value == expected 45 | assert kth_last_recursive(ll, k).value == expected 46 | 47 | 48 | if __name__ == "__main__": 49 | test_kth_to_last() 50 | -------------------------------------------------------------------------------- /chapter_02/p03_delete_middle_node.py: -------------------------------------------------------------------------------- 1 | from chapter_02.linked_list import LinkedList 2 | 3 | 4 | def delete_middle_node(node): 5 | node.value = node.next.value 6 | node.next = node.next.next 7 | 8 | 9 | if __name__ == "__main__": 10 | ll = LinkedList() 11 | ll.add_multiple([1, 2, 3, 4]) 12 | middle_node = ll.add(5) 13 | ll.add_multiple([7, 8, 9]) 14 | 15 | print(ll) 16 | delete_middle_node(middle_node) 17 | print(ll) 18 | -------------------------------------------------------------------------------- /chapter_02/p04_partition.py: -------------------------------------------------------------------------------- 1 | from chapter_02.linked_list import LinkedList 2 | 3 | 4 | def partition(ll, x): 5 | current = ll.tail = ll.head 6 | 7 | while current: 8 | next_node = current.next 9 | current.next = None 10 | if current.value < x: 11 | current.next = ll.head 12 | ll.head = current 13 | else: 14 | ll.tail.next = current 15 | ll.tail = current 16 | current = next_node 17 | 18 | # Error check in case all nodes are less than x 19 | if ll.tail.next is not None: 20 | ll.tail.next = None 21 | return ll 22 | 23 | 24 | def lr_partition(_ll: LinkedList, p: int) -> LinkedList: 25 | """ 26 | Create 2 LinkedList (left and right), and return a combined LinkedList 27 | """ 28 | left = LinkedList() 29 | right = LinkedList() 30 | 31 | current = _ll.head 32 | while current: 33 | if current.value < p: 34 | left.add(current.value) 35 | else: 36 | right.add(current.value) 37 | 38 | current = current.next 39 | 40 | left.tail.next = right.head 41 | return left 42 | 43 | 44 | def test_lr_partition(): 45 | partitioners = [partition, lr_partition] 46 | for partition_func in partitioners: 47 | # book example 48 | ll = LinkedList([3, 5, 8, 5, 10, 2, 1]) 49 | assert not is_partitioned(ll, x=5) 50 | ll = partition_func(ll, 5) 51 | assert is_partitioned(ll, x=5), f"{partition_func} did not partition {ll}" 52 | 53 | # random example 54 | ll = LinkedList.generate(10, 0, 99) 55 | x = ll.head.value 56 | ll = partition_func(ll, x) 57 | assert is_partitioned(ll, x=x), f"{partition_func} did not partition" 58 | 59 | 60 | def is_partitioned(ll, x): 61 | seen_gt_partition = False 62 | for node in ll: 63 | if node.value >= x: 64 | seen_gt_partition = True 65 | continue 66 | if seen_gt_partition and node.value < x: 67 | return False 68 | return True 69 | 70 | 71 | if __name__ == "__main__": 72 | test_lr_partition() 73 | -------------------------------------------------------------------------------- /chapter_02/p05_sum_lists.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from chapter_02.linked_list import LinkedList 4 | 5 | 6 | def sum_lists(ll_a, ll_b): 7 | n1, n2 = ll_a.head, ll_b.head 8 | ll = NumericLinkedList() 9 | carry = 0 10 | while n1 or n2: 11 | result = carry 12 | if n1: 13 | result += n1.value 14 | n1 = n1.next 15 | if n2: 16 | result += n2.value 17 | n2 = n2.next 18 | 19 | ll.add(result % 10) 20 | carry = result // 10 21 | 22 | if carry: 23 | ll.add(carry) 24 | 25 | return ll 26 | 27 | 28 | def sum_lists_recursive(ll_a, ll_b) -> "NumericLinkedList": 29 | def sum_lists_helper(ll1_head, ll2_head, remainder, summed_list): 30 | if ll1_head is None and ll2_head is None: 31 | if remainder != 0: 32 | summed_list.add(remainder) 33 | return summed_list 34 | elif ll1_head is None: 35 | result = ll2_head.value + remainder 36 | summed_list.add(result % 10) 37 | return sum_lists_helper(ll1_head, ll2_head.next, result // 10, summed_list) 38 | elif ll2_head is None: 39 | result = ll1_head.value + remainder 40 | summed_list.add(result % 10) 41 | return sum_lists_helper(ll1_head.next, ll2_head, result // 10, summed_list) 42 | else: 43 | result = ll1_head.value + ll2_head.value + remainder 44 | summed_list.add(result % 10) 45 | return sum_lists_helper( 46 | ll1_head.next, ll2_head.next, result // 10, summed_list 47 | ) 48 | 49 | return sum_lists_helper(ll_a.head, ll_b.head, 0, NumericLinkedList()) 50 | 51 | 52 | class NumericLinkedList(LinkedList): 53 | def __init__(self, values=None): 54 | """handle integer as input""" 55 | if isinstance(values, int): 56 | values = [int(c) for c in str(values)] 57 | values.reverse() 58 | elif isinstance(values, list): 59 | values = values.copy() 60 | 61 | super().__init__(values) 62 | 63 | def numeric_value(self): 64 | number = 0 65 | for place, node in enumerate(self): 66 | number += node.value * 10**place 67 | return number 68 | 69 | 70 | def test_numeric_linked_list(): 71 | ll = NumericLinkedList(321) 72 | assert ll.numeric_value() == 321 73 | assert ll.values() == [1, 2, 3] 74 | 75 | 76 | testable_functions = (sum_lists, sum_lists_recursive) 77 | 78 | 79 | @pytest.fixture(params=testable_functions) 80 | def linked_list_summing_function(request): 81 | return request.param 82 | 83 | 84 | test_cases = ( 85 | # inputs can either be list of integer or integers 86 | # a, b, expected_sum 87 | pytest.param([1], [2], [3], id="single_digit"), 88 | pytest.param([0], [0], [0], id="single_digit_zero"), 89 | pytest.param([], [], [], id="empty"), 90 | pytest.param([7, 1, 6], [5, 9, 2], [2, 1, 9], id="3-digit equal length A"), 91 | pytest.param([3, 2, 1], [3, 2, 1], [6, 4, 2], id="3-digit equal length B"), 92 | pytest.param(123, 1, [4, 2, 1], id="3-digit and single digit"), 93 | pytest.param([9, 9, 9], [1], [0, 0, 0, 1], id="carry end"), 94 | pytest.param([9, 9, 9], [9, 9, 9], [8, 9, 9, 1], id="multiple carry"), 95 | ) 96 | 97 | 98 | @pytest.mark.parametrize("a, b, expected", test_cases) 99 | def test_linked_list_addition(linked_list_summing_function, a, b, expected): 100 | ll_a = NumericLinkedList(a) 101 | ll_b = NumericLinkedList(b) 102 | ll_result = linked_list_summing_function(ll_a, ll_b) 103 | assert ll_result.values() == expected 104 | assert ( 105 | ll_a.numeric_value() + ll_b.numeric_value() 106 | == NumericLinkedList(expected).numeric_value() 107 | ) 108 | 109 | ll_result_reverse = linked_list_summing_function(ll_b, ll_a) 110 | assert ll_result_reverse.values() == expected 111 | 112 | 113 | def example(): 114 | ll_a = LinkedList.generate(4, 0, 9) 115 | ll_b = LinkedList.generate(3, 0, 9) 116 | print(ll_a) 117 | print(ll_b) 118 | print(sum_lists(ll_a, ll_b)) 119 | 120 | 121 | if __name__ == "__main__": 122 | example() 123 | pytest.main(args=[__file__]) 124 | -------------------------------------------------------------------------------- /chapter_02/p06_palindrome.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from chapter_02.linked_list import LinkedList 4 | 5 | 6 | def is_palindrome(ll): 7 | fast = slow = ll.head 8 | stack = [] 9 | 10 | while fast and fast.next: 11 | stack.append(slow.value) 12 | slow = slow.next 13 | fast = fast.next.next 14 | 15 | if fast: 16 | slow = slow.next 17 | 18 | while slow: 19 | top = stack.pop() 20 | 21 | if top != slow.value: 22 | return False 23 | 24 | slow = slow.next 25 | 26 | return True 27 | 28 | 29 | def is_palindrome_constant_space(ll): 30 | """ 31 | Constant(O(1)) space solution 32 | """ 33 | # find the list center via the runner technique 34 | slow = ll.head 35 | if not slow or not slow.next: 36 | return True 37 | 38 | fast = slow.next 39 | while fast and fast.next: 40 | slow = slow.next 41 | fast = fast.next.next 42 | # unlink left and right halves of the list 43 | right_head = slow.next 44 | slow.next_node = None 45 | # reverse the right half of the list 46 | tail = reverse(right_head) 47 | # iterate over nodes from the outside in 48 | left, right = ll.head, tail 49 | result = True 50 | while left and right: 51 | if left.value != right.value: 52 | result = False 53 | break 54 | left = left.next 55 | right = right.next 56 | # undo state changes 57 | reverse(tail) 58 | slow.next_node = right_head 59 | return result 60 | 61 | 62 | def reverse(node): 63 | """ 64 | reverses a linked list, 65 | returns the input list's 66 | tail node as the new head 67 | 68 | Time : O(N) 69 | Space: O(1) 70 | """ 71 | previous_node = None 72 | while node: 73 | # keep track of the next node 74 | next_node = node.next 75 | # point the current node backwards 76 | node.next = previous_node 77 | # advance pointers 78 | previous_node = node 79 | node = next_node 80 | return previous_node 81 | 82 | 83 | def is_palindrome_recursive(ll): 84 | def get_len(node): 85 | if not node: 86 | return 0 87 | else: 88 | return 1 + get_len(node.next) 89 | 90 | def recursive_transverse(node, length): 91 | if not node or length == 0: # even list 92 | return True, node 93 | elif length == 1: # odd list 94 | return True, node.next 95 | 96 | _is_palindrome, fwd_node = recursive_transverse(node.next, length - 2) 97 | 98 | if not _is_palindrome or not fwd_node: 99 | return False, None 100 | 101 | if node.value == fwd_node.value: 102 | return True, fwd_node.next 103 | else: 104 | return False, None 105 | 106 | return recursive_transverse(ll.head, get_len(ll.head))[0] 107 | 108 | 109 | test_cases = [ 110 | ([1, 2, 3, 4, 3, 2, 1], True), 111 | ([1], True), 112 | (["a", "a"], True), 113 | ("aba", True), 114 | ([], True), 115 | ([1, 2, 3, 4, 5], False), 116 | ([1, 2], False), 117 | ] 118 | 119 | testable_functions = [ 120 | is_palindrome, 121 | is_palindrome_constant_space, 122 | is_palindrome_recursive, 123 | ] 124 | 125 | 126 | def test_is_palindrome(): 127 | for f in testable_functions: 128 | start = time.perf_counter() 129 | for values, expected in test_cases: 130 | print(f"{f.__name__}: {values}") 131 | for _ in range(100): 132 | assert f(LinkedList(values)) == expected 133 | 134 | duration = time.perf_counter() - start 135 | print(f"{f.__name__} {duration * 1000:.1f}ms") 136 | 137 | 138 | if __name__ == "__main__": 139 | test_is_palindrome() 140 | -------------------------------------------------------------------------------- /chapter_02/p07_intersection.py: -------------------------------------------------------------------------------- 1 | from chapter_02.linked_list import LinkedList 2 | 3 | 4 | def intersection(list1, list2): 5 | if list1.tail is not list2.tail: 6 | return False 7 | 8 | shorter = list1 if len(list1) < len(list2) else list2 9 | longer = list2 if len(list1) < len(list2) else list1 10 | 11 | diff = len(longer) - len(shorter) 12 | 13 | shorter_node, longer_node = shorter.head, longer.head 14 | 15 | for _ in range(diff): 16 | longer_node = longer_node.next 17 | 18 | while shorter_node is not longer_node: 19 | shorter_node = shorter_node.next 20 | longer_node = longer_node.next 21 | 22 | return longer_node 23 | 24 | 25 | def test_linked_list_intersection(): 26 | shared = LinkedList() 27 | shared.add_multiple([1, 2, 3, 4]) 28 | 29 | a = LinkedList([10, 11, 12, 13, 14, 15]) 30 | b = LinkedList([20, 21, 22]) 31 | 32 | a.tail.next = shared.head 33 | a.tail = shared.tail 34 | b.tail.next = shared.head 35 | b.tail = shared.tail 36 | 37 | # should be 1 38 | assert intersection(a, b).value == 1 39 | -------------------------------------------------------------------------------- /chapter_02/p08_loop_detection.py: -------------------------------------------------------------------------------- 1 | from chapter_02.linked_list import LinkedList 2 | 3 | 4 | def loop_detection(ll): 5 | fast = slow = ll.head 6 | 7 | while fast and fast.next: 8 | fast = fast.next.next 9 | slow = slow.next 10 | if fast is slow: 11 | break 12 | 13 | if fast is None or fast.next is None: 14 | return None 15 | 16 | slow = ll.head 17 | while fast is not slow: 18 | fast = fast.next 19 | slow = slow.next 20 | 21 | return fast 22 | 23 | 24 | def test_loop_detection(): 25 | looped_list = LinkedList(["A", "B", "C", "D", "E"]) 26 | loop_start_node = looped_list.head.next.next 27 | looped_list.tail.next = loop_start_node 28 | tests = [ 29 | (LinkedList(), None), 30 | ((LinkedList((1, 2, 3))), None), 31 | (looped_list, loop_start_node), 32 | ] 33 | 34 | for ll, expected in tests: 35 | assert loop_detection(ll) == expected 36 | -------------------------------------------------------------------------------- /chapter_03/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/careercup/CtCI-6th-Edition-Python/653e50ccfa927179526879273062628996f392c1/chapter_03/__init__.py -------------------------------------------------------------------------------- /chapter_03/p01_three_in_one.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class MultiStack: 5 | def __init__(self, stack_size, number_of_stacks): 6 | self.number_of_stacks = number_of_stacks 7 | self.array = [0] * (stack_size * self.number_of_stacks) 8 | self.sizes = [0] * self.number_of_stacks 9 | self.stack_size = stack_size 10 | 11 | def push(self, value, stack_num): 12 | self._assert_valid_stack_num(stack_num) 13 | if self.is_full(stack_num): 14 | raise StackFullError(f"Push failed: stack #{stack_num} is full") 15 | self.sizes[stack_num] += 1 16 | self.array[self.index_of_top(stack_num)] = value 17 | 18 | def pop(self, stack_num): 19 | self._assert_valid_stack_num(stack_num) 20 | if self.is_empty(stack_num): 21 | raise StackEmptyError(f"Cannot pop from empty stack #{stack_num}") 22 | value = self.array[self.index_of_top(stack_num)] 23 | self.array[self.index_of_top(stack_num)] = 0 24 | self.sizes[stack_num] -= 1 25 | return value 26 | 27 | def peek(self, stack_num): 28 | self._assert_valid_stack_num(stack_num) 29 | if self.is_empty(stack_num): 30 | raise StackEmptyError(f"Cannot peek at empty stack #{stack_num}") 31 | return self.array[self.index_of_top(stack_num)] 32 | 33 | def is_empty(self, stack_num): 34 | self._assert_valid_stack_num(stack_num) 35 | return self.sizes[stack_num] == 0 36 | 37 | def is_full(self, stack_num): 38 | self._assert_valid_stack_num(stack_num) 39 | return self.sizes[stack_num] == self.stack_size 40 | 41 | def index_of_top(self, stack_num): 42 | self._assert_valid_stack_num(stack_num) 43 | offset = stack_num * self.stack_size 44 | return offset + self.sizes[stack_num] - 1 45 | 46 | def _assert_valid_stack_num(self, stack_num): 47 | if stack_num >= self.number_of_stacks: 48 | raise StackDoesNotExistError(f"Stack #{stack_num} does not exist") 49 | 50 | 51 | class MultiStackError(Exception): 52 | """multistack operation error""" 53 | 54 | 55 | class StackFullError(MultiStackError): 56 | """the stack is full""" 57 | 58 | 59 | class StackEmptyError(MultiStackError): 60 | """the stack is empty""" 61 | 62 | 63 | class StackDoesNotExistError(ValueError): 64 | """stack does not exist""" 65 | 66 | 67 | def test_multistack(): 68 | num_stacks = 3 69 | stack_size = 6 70 | s = MultiStack(stack_size=stack_size, number_of_stacks=num_stacks) 71 | 72 | for stack_num in range(num_stacks): 73 | assert s.is_empty(stack_num) 74 | assert not s.is_full(stack_num) 75 | with pytest.raises(StackEmptyError): 76 | s.pop(stack_num) 77 | 78 | for i in range(stack_size - 1): 79 | s.push(i, stack_num=stack_num) 80 | assert s.peek(stack_num) == i 81 | assert not s.is_empty(stack_num) 82 | assert not s.is_full(stack_num) 83 | 84 | s.push(999, stack_num=stack_num) 85 | with pytest.raises(StackFullError): 86 | s.push(777, stack_num=stack_num) 87 | 88 | assert not s.is_empty(stack_num) 89 | assert s.is_full(stack_num) 90 | 91 | assert s.peek(stack_num) == 999 92 | assert s.pop(stack_num) == 999 93 | assert not s.is_empty(stack_num) 94 | assert not s.is_full(stack_num) 95 | 96 | for i in range(stack_size - 2, 0, -1): 97 | assert s.peek(stack_num) == i 98 | assert s.pop(stack_num) == i 99 | assert not s.is_empty(stack_num) 100 | assert not s.is_full(stack_num) 101 | 102 | assert s.peek(stack_num) == 0 103 | assert s.pop(stack_num) == 0 104 | assert s.is_empty(stack_num) 105 | assert not s.is_full(stack_num) 106 | 107 | with pytest.raises(StackEmptyError): 108 | s.peek(stack_num) 109 | with pytest.raises(StackEmptyError): 110 | s.pop(stack_num) 111 | 112 | 113 | def test_stack_does_not_exist(): 114 | s = MultiStack(stack_size=3, number_of_stacks=1) 115 | with pytest.raises(StackDoesNotExistError): 116 | s.push(1, 1) 117 | 118 | 119 | if __name__ == "__main__": 120 | newstack = MultiStack(2, 2) 121 | print(newstack.is_empty(1)) 122 | newstack.push(3, 1) 123 | print(newstack.peek(1)) 124 | print(newstack.is_empty(1)) 125 | newstack.push(2, 1) 126 | print(newstack.peek(1)) 127 | print(newstack.pop(1)) 128 | print(newstack.peek(1)) 129 | newstack.push(3, 1) 130 | -------------------------------------------------------------------------------- /chapter_03/p02_stack_min.py: -------------------------------------------------------------------------------- 1 | from chapter_03.stack import Stack 2 | 3 | 4 | class MinStack(Stack): 5 | def __init__(self): 6 | super().__init__() 7 | self.minvals = Stack() 8 | 9 | def push(self, value): 10 | super().push(value) 11 | if not self.minvals or value <= self.minimum(): 12 | self.minvals.push(value) 13 | 14 | def pop(self): 15 | value = super().pop() 16 | if value == self.minimum(): 17 | self.minvals.pop() 18 | return value 19 | 20 | def minimum(self): 21 | return self.minvals.peek() 22 | 23 | 24 | def test_min_stack(): 25 | newstack = MinStack() 26 | assert newstack.minimum() is None 27 | 28 | newstack.push(5) 29 | assert newstack.minimum() == 5 30 | 31 | newstack.push(6) 32 | assert newstack.minimum() == 5 33 | 34 | newstack.push(3) 35 | assert newstack.minimum() == 3 36 | 37 | newstack.push(7) 38 | assert newstack.minimum() == 3 39 | 40 | newstack.push(3) 41 | assert newstack.minimum() == 3 42 | 43 | newstack.pop() 44 | assert newstack.minimum() == 3 45 | 46 | newstack.pop() 47 | assert newstack.minimum() == 3 48 | 49 | newstack.pop() 50 | assert newstack.minimum() == 5 51 | 52 | newstack.push(1) 53 | assert newstack.minimum() == 1 54 | 55 | 56 | if __name__ == "__main__": 57 | test_min_stack() 58 | -------------------------------------------------------------------------------- /chapter_03/p03_stack_of_plates.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class Node: 5 | def __init__(self, value): 6 | self.value = value 7 | self.above = None 8 | self.below = None 9 | 10 | 11 | class Stack: 12 | def __init__(self, capacity): 13 | self.capacity = capacity 14 | self.size = 0 15 | self.top = None 16 | self.bottom = None 17 | 18 | def is_full(self): 19 | return self.size == self.capacity 20 | 21 | def is_empty(self): 22 | return self.size == 0 23 | 24 | def join(self, above, below): 25 | if below: 26 | below.above = above 27 | if above: 28 | above.below = below 29 | 30 | def push(self, v): 31 | if self.size >= self.capacity: 32 | return False 33 | self.size += 1 34 | n = Node(v) 35 | if self.size == 1: 36 | self.bottom = n 37 | self.join(n, self.top) 38 | self.top = n 39 | return True 40 | 41 | def pop(self): 42 | if not self.top: 43 | return None 44 | t = self.top 45 | self.top = self.top.below 46 | self.size -= 1 47 | return t.value 48 | 49 | def remove_bottom(self): 50 | b = self.bottom 51 | self.bottom = self.bottom.above 52 | if self.bottom: 53 | self.bottom.below = None 54 | self.size -= 1 55 | return b.value 56 | 57 | 58 | class SetOfStacks: 59 | def __init__(self, capacity): 60 | self.capacity = capacity 61 | self.stacks = [] 62 | 63 | def get_last_stack(self): 64 | if not self.stacks: 65 | return None 66 | return self.stacks[-1] 67 | 68 | def is_empty(self): 69 | last = self.get_last_stack() 70 | return not last or last.is_empty() 71 | 72 | def push(self, v): 73 | last = self.get_last_stack() 74 | if last and not last.is_full(): 75 | last.push(v) 76 | else: 77 | stack = Stack(self.capacity) 78 | stack.push(v) 79 | self.stacks.append(stack) 80 | 81 | def pop(self): 82 | last = self.get_last_stack() 83 | if not last: 84 | return None 85 | v = last.pop() 86 | if last.size == 0: 87 | del self.stacks[-1] 88 | return v 89 | 90 | def pop_at(self, index): 91 | return self.left_shift(index, True) 92 | 93 | def left_shift(self, index, remove_top): 94 | stack = self.stacks[index] 95 | removed_item = stack.pop() if remove_top else stack.remove_bottom() 96 | if stack.is_empty(): 97 | del self.stacks[index] 98 | elif len(self.stacks) > index + 1: 99 | v = self.left_shift(index + 1, False) 100 | stack.push(v) 101 | return removed_item 102 | 103 | 104 | class Tests(unittest.TestCase): 105 | def test_stacks(self): 106 | stacks = SetOfStacks(5) 107 | for i in range(35): 108 | stacks.push(i) 109 | lst = [] 110 | for _ in range(35): 111 | lst.append(stacks.pop()) 112 | assert lst == list(reversed(range(35))) 113 | 114 | def test_pop_at(self): 115 | stacks = SetOfStacks(5) 116 | for i in range(35): 117 | stacks.push(i) 118 | lst = [] 119 | for _ in range(31): 120 | lst.append(stacks.pop_at(0)) 121 | assert lst == list(range(4, 35)) 122 | 123 | 124 | if __name__ == "__main__": 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /chapter_03/p04_queue_via_stacks.py: -------------------------------------------------------------------------------- 1 | # 3.4 Queue Via Stacks 2 | import unittest 3 | 4 | from chapter_03.stack import Stack 5 | 6 | 7 | class MyQueue: 8 | def __init__(self): 9 | self.new_stack = Stack() 10 | self.old_stack = Stack() 11 | 12 | def _shift_stacks(self): 13 | if self.old_stack.is_empty(): 14 | while not self.new_stack.is_empty(): 15 | self.old_stack.push(self.new_stack.pop()) 16 | 17 | def add(self, value): 18 | return self.new_stack.push(value) 19 | 20 | def peek(self): 21 | if self.is_empty(): 22 | return False 23 | self._shift_stacks() 24 | return self.old_stack.peek() 25 | 26 | def remove(self): 27 | if self.is_empty(): 28 | return False 29 | self._shift_stacks() 30 | return self.old_stack.pop() 31 | 32 | def is_empty(self): 33 | return len(self) == 0 34 | 35 | def __len__(self): 36 | return len(self.new_stack) + len(self.old_stack) 37 | 38 | 39 | class Tests(unittest.TestCase): 40 | test_cases = [([1, 2, 3]), ([-1, 0, 1]), (["a", "b", "c", "d", "e", "f"])] 41 | 42 | def test_size(self): 43 | for sequence in self.test_cases: 44 | q = MyQueue() 45 | for index, val in enumerate(sequence, start=1): 46 | q.add(val) 47 | assert len(q) == index 48 | for index, val in enumerate(sequence, start=1): 49 | q.remove() 50 | assert len(q) == len(sequence) - index 51 | 52 | def test_add(self): 53 | for sequence in self.test_cases: 54 | q = MyQueue() 55 | for val in sequence: 56 | q.add(val) 57 | assert q.peek() == sequence[0] 58 | assert len(q) == len(sequence) 59 | 60 | def test_shift_stacks(self): 61 | for sequence in self.test_cases: 62 | q = MyQueue() 63 | for val in sequence: 64 | q.add(val) 65 | assert len(q.old_stack) == 0 66 | assert len(q.new_stack) == len(sequence) 67 | assert q.new_stack.peek() == sequence[-1] 68 | q._shift_stacks() 69 | assert len(q.old_stack) == len(sequence) 70 | assert len(q.new_stack) == 0 71 | assert q.old_stack.peek() == sequence[0] 72 | 73 | def test_peek(self): 74 | for sequence in self.test_cases: 75 | q = MyQueue() 76 | for val in sequence: 77 | q.add(val) 78 | assert q.peek() == sequence[0] 79 | q.remove() 80 | assert q.peek() == sequence[1] 81 | 82 | def test_remove(self): 83 | for sequence in self.test_cases: 84 | q = MyQueue() 85 | for val in sequence: 86 | q.add(val) 87 | for i in range(len(sequence)): # noqa 88 | assert q.remove() == sequence[i] 89 | 90 | def test_peek_simple(self): 91 | q = MyQueue() 92 | q.add(4) 93 | q.add(6) 94 | assert q.peek() == 4 95 | 96 | def test_add_simple(self): 97 | q = MyQueue() 98 | q.add(4) 99 | q.add(6) 100 | assert q.peek() == 4 101 | q.add(101) 102 | assert q.peek() != 101 103 | 104 | def test_remove_simple(self): 105 | q = MyQueue() 106 | q.add(4) 107 | q.add(6) 108 | assert len(q) == 2 109 | assert q.remove() == 4 110 | assert q.remove() == 6 111 | assert len(q) == 0 112 | assert not q.remove() 113 | -------------------------------------------------------------------------------- /chapter_03/p05_sort_stack.py: -------------------------------------------------------------------------------- 1 | # 3.5 Sort Stacks 2 | import unittest 3 | 4 | from chapter_03.stack import Stack 5 | 6 | 7 | class SortedStack(Stack): 8 | def __init__(self): 9 | super().__init__() 10 | self.temp_stack = Stack() 11 | 12 | def push(self, item): 13 | if self.is_empty() or item < self.peek(): 14 | super().push(item) 15 | else: 16 | while self.peek() is not None and item > self.peek(): 17 | self.temp_stack.push(self.pop()) 18 | super().push(item) 19 | while not self.temp_stack.is_empty(): 20 | super().push(self.temp_stack.pop()) 21 | 22 | 23 | class Tests(unittest.TestCase): 24 | def test_push_one(self): 25 | queue = SortedStack() 26 | queue.push(1) 27 | assert len(queue) == 1 28 | 29 | def test_push_two(self): 30 | queue = SortedStack() 31 | queue.push(1) 32 | queue.push(2) 33 | assert len(queue) == 2 34 | 35 | def test_push_three(self): 36 | queue = SortedStack() 37 | queue.push(1) 38 | queue.push(2) 39 | queue.push(3) 40 | assert len(queue) == 3 41 | 42 | def test_pop_one(self): 43 | queue = SortedStack() 44 | queue.push(1) 45 | assert queue.pop() == 1 46 | 47 | def test_pop_two(self): 48 | queue = SortedStack() 49 | queue.push(1) 50 | queue.push(2) 51 | assert queue.pop() == 1 52 | assert queue.pop() == 2 53 | 54 | def test_pop_three(self): 55 | queue = SortedStack() 56 | queue.push(1) 57 | queue.push(2) 58 | queue.push(3) 59 | assert queue.pop() == 1 60 | assert queue.pop() == 2 61 | assert queue.pop() == 3 62 | 63 | def test_push_mixed(self): 64 | queue = SortedStack() 65 | queue.push(3) 66 | queue.push(2) 67 | queue.push(1) 68 | queue.push(4) 69 | assert queue.pop() == 1 70 | assert queue.pop() == 2 71 | assert queue.pop() == 3 72 | assert queue.pop() == 4 73 | -------------------------------------------------------------------------------- /chapter_03/p06_animal_shelter.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Node: 5 | def __init__(self, data, next_node=None): 6 | self.data = data 7 | self.next_node = next_node 8 | 9 | def __str__(self): 10 | return str(self.data) 11 | 12 | 13 | class LinkedList: 14 | def __init__(self, head=None): 15 | self.head = head 16 | 17 | def insert(self, node): 18 | if self.head is None: 19 | self.head = node 20 | return 21 | current_node = self.head 22 | while current_node.next_node is not None: 23 | current_node = current_node.next_node 24 | current_node.next_node = node 25 | 26 | def pop_head(self): 27 | if self.head is not None: 28 | head_to_pop = self.head 29 | self.head = self.head.next_node 30 | return head_to_pop 31 | 32 | return None 33 | 34 | def size(self): 35 | current_node = self.head 36 | size = 0 37 | while current_node is not None: 38 | size += 1 39 | current_node = current_node.next_node 40 | return size 41 | 42 | 43 | # Animal Definitions 44 | 45 | 46 | class Animal: 47 | def __init__(self, name): 48 | self.time_admitted = time.time() 49 | self.name = name 50 | 51 | 52 | class Cat(Animal): 53 | pass 54 | 55 | 56 | class Dog(Animal): 57 | pass 58 | 59 | 60 | class AnimalShelter(LinkedList): 61 | def enqueue(self, animal): 62 | animal_node = Node(animal) 63 | self.insert(animal_node) 64 | 65 | def dequeue_any(self): 66 | return super().pop_head() 67 | 68 | def dequeue_cat(self): 69 | previous_node = None 70 | current_node = self.head 71 | while current_node is not None: 72 | if isinstance(current_node.data, Cat): 73 | previous_node.next_node = current_node.next_node 74 | return current_node.data 75 | previous_node = current_node 76 | current_node = current_node.next_node 77 | return None 78 | 79 | def dequeue_dog(self): 80 | previous_node = None 81 | current_node = self.head 82 | while current_node is not None: 83 | if isinstance(current_node.data, Dog): 84 | previous_node.next_node = current_node.next_node 85 | return current_node.data 86 | previous_node = current_node 87 | current_node = current_node.next_node 88 | return None 89 | 90 | 91 | def test_enqueue(): 92 | animal_shelter = AnimalShelter() 93 | animal_shelter.enqueue(Cat("Fluffy")) 94 | animal_shelter.enqueue(Dog("Sparky")) 95 | animal_shelter.enqueue(Cat("Sneezy")) 96 | assert animal_shelter.size() == 3 97 | -------------------------------------------------------------------------------- /chapter_03/stack.py: -------------------------------------------------------------------------------- 1 | class Stack: 2 | def __init__(self): 3 | self.items = [] 4 | 5 | def is_empty(self): 6 | return len(self.items) == 0 7 | 8 | def push(self, item): 9 | self.items.append(item) 10 | 11 | def pop(self): 12 | return self.items.pop() 13 | 14 | def peek(self): 15 | if self.items: 16 | return self.items[-1] 17 | return None 18 | 19 | def __len__(self): 20 | # in python the `len` function is preferred to `size` methods 21 | return len(self.items) 22 | 23 | def __bool__(self): 24 | # lets us use the stack as a conditional 25 | return bool(self.items) 26 | -------------------------------------------------------------------------------- /chapter_04/binary_search_tree.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, key): 3 | self.key = key 4 | self.parent = None 5 | self.left = None 6 | self.right = None 7 | 8 | 9 | class BinarySearchTree: 10 | def __init__(self): 11 | self.root = None 12 | 13 | def insert(self, key): 14 | new = Node(key) 15 | if self.root is None: 16 | self.root = new 17 | return 18 | 19 | current = self.root 20 | while current: 21 | if current.key > key: 22 | if current.left is None: 23 | current.left = new 24 | new.parent = current 25 | return 26 | current = current.left 27 | else: 28 | if current.right is None: 29 | current.right = new 30 | new.parent = current 31 | return 32 | current = current.right 33 | 34 | def get_node(self, key): 35 | current = self.root 36 | while current: 37 | if current.key == key: 38 | return current 39 | 40 | if current.key > key: 41 | current = current.left 42 | else: 43 | current = current.right 44 | raise Exception("No such value in the tree") 45 | 46 | 47 | if __name__ == "__main__": 48 | bst = BinarySearchTree() 49 | bst.insert(20) 50 | bst.insert(9) 51 | bst.insert(25) 52 | bst.insert(5) 53 | bst.insert(12) 54 | bst.insert(11) 55 | bst.insert(14) 56 | -------------------------------------------------------------------------------- /chapter_04/binary_tree.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, key): 3 | self.key = key 4 | self.parent = None 5 | self.left = None 6 | self.right = None 7 | 8 | 9 | class BinaryTree: 10 | NodeCls = Node 11 | 12 | def __init__(self): 13 | self.root = None 14 | 15 | def insert(self, key, parent): 16 | new = self.NodeCls(key) 17 | if parent is None: 18 | if self.root is None: 19 | self.root = new 20 | return new 21 | raise Exception("a root already exists") 22 | 23 | if not parent.left: 24 | parent.left = new 25 | new.parent = parent 26 | elif not parent.right: 27 | parent.right = new 28 | new.parent = parent 29 | else: 30 | raise Exception("a node cannot have more than two children") 31 | return new 32 | 33 | 34 | def example(): 35 | t = BinaryTree() 36 | n1 = t.insert(1, None) 37 | n2 = t.insert(2, n1) 38 | n3 = t.insert(3, n1) 39 | n4 = t.insert(4, n2) 40 | t.insert(5, n2) 41 | t.insert(7, n3) 42 | t.insert(8, n4) 43 | 44 | print(t.root.left.left.left.key) 45 | 46 | 47 | if __name__ == "__main__": 48 | example() 49 | -------------------------------------------------------------------------------- /chapter_04/p01_route_between_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections import deque 3 | 4 | # VISUAL OF TEST GRAPH: 5 | 6 | # A -- B 7 | # | | 8 | # C -- D 9 | # | 10 | # E -- F -- G -- H 11 | # | \ 12 | # O I -- J -- K 13 | # | 14 | # L 15 | 16 | # P -- Q 17 | # | / 18 | # R 19 | 20 | 21 | def is_route(graph, start, end, visited=None): 22 | if visited is None: 23 | visited = set() 24 | for node in graph[start]: 25 | if node not in visited: 26 | visited.add(node) 27 | if node == end or is_route(graph, node, end, visited): 28 | return True 29 | return False 30 | 31 | 32 | def is_route_bfs(graph, start, end): 33 | if start == end: 34 | return True 35 | visited = set() 36 | queue = deque() 37 | queue.append(start) 38 | while queue: 39 | node = queue.popleft() 40 | for adjacent in graph[node]: 41 | if adjacent not in visited: 42 | if adjacent == end: 43 | return True 44 | else: 45 | queue.append(adjacent) 46 | visited.add(node) 47 | return False 48 | 49 | 50 | def is_route_bidirectional(graph, start, end): 51 | to_visit = deque() 52 | to_visit.append(start) 53 | to_visit.append(end) 54 | visited_start = set() 55 | visited_start.add(start) 56 | visited_end = set() 57 | visited_end.add(end) 58 | while to_visit: 59 | node = to_visit.popleft() 60 | 61 | if node in visited_start and node in visited_end: 62 | return True 63 | 64 | for y in graph[node]: 65 | if node in visited_start and y not in visited_start: 66 | visited_start.add(y) 67 | to_visit.append(y) 68 | if node in visited_end and y not in visited_end: 69 | visited_end.add(y) 70 | to_visit.append(y) 71 | return False 72 | 73 | 74 | class Test(unittest.TestCase): 75 | 76 | graph = { 77 | "A": ["B", "C"], 78 | "B": ["D"], 79 | "C": ["D", "E"], 80 | "D": ["B", "C"], 81 | "E": ["C", "F"], 82 | "F": ["E", "O", "I", "G"], 83 | "G": ["F", "H"], 84 | "H": ["G"], 85 | "I": ["F", "J"], 86 | "O": ["F"], 87 | "J": ["K", "L", "I"], 88 | "K": ["J"], 89 | "L": ["J"], 90 | "P": ["Q", "R"], 91 | "Q": ["P", "R"], 92 | "R": ["P", "Q"], 93 | } 94 | 95 | tests = [ 96 | ("A", "L", True), 97 | ("A", "B", True), 98 | ("H", "K", True), 99 | ("L", "D", True), 100 | ("P", "Q", True), 101 | ("Q", "P", True), 102 | ("Q", "G", False), 103 | ("R", "A", False), 104 | ("P", "B", False), 105 | ] 106 | 107 | def test_is_route(self): 108 | for [start, end, expected] in self.tests: 109 | actual = is_route(self.graph, start, end) 110 | assert actual == expected 111 | 112 | def test_is_route_bfs(self): 113 | for [start, end, expected] in self.tests: 114 | actual = is_route_bfs(self.graph, start, end) 115 | assert actual == expected 116 | 117 | def test_is_route_bidirectional(self): 118 | for [start, end, expected] in self.tests: 119 | actual = is_route_bidirectional(self.graph, start, end) 120 | assert actual == expected 121 | 122 | 123 | if __name__ == "__main__": 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /chapter_04/p02_minimal_tree.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, item): 3 | self.right = None 4 | self.left = None 5 | self.val = item 6 | 7 | def disp(self, nesting=0): 8 | indent = " " * nesting * 2 9 | output = f"{self.val}\n" 10 | if self.left is not None: 11 | output += f"{indent}L:" 12 | output += self.left.disp(nesting + 1) 13 | if self.right is not None: 14 | output += f"{indent}R:" 15 | output += self.right.disp(nesting + 1) 16 | return output 17 | 18 | def __str__(self): 19 | return self.disp() 20 | 21 | 22 | def array_to_binary_tree(array, start, end): 23 | if start > end: 24 | return None 25 | mid = ( 26 | start + end 27 | ) // 2 # This must be floor division, otherwise you get a slice error 28 | # TypeError: list indices must be integers or slices, not float 29 | root = Node(array[mid]) 30 | root.left = array_to_binary_tree(array, start, mid - 1) 31 | root.right = array_to_binary_tree(array, mid + 1, end) 32 | return root 33 | 34 | 35 | if __name__ == "__main__": 36 | test_array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 22, 43, 144, 515, 4123] 37 | print(array_to_binary_tree(test_array, 0, len(test_array) - 1)) 38 | -------------------------------------------------------------------------------- /chapter_04/p03_list_of_depths.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | from chapter_02.linked_list import LinkedList 4 | 5 | 6 | class BinaryNode: 7 | def __init__(self, name, left=None, right=None): 8 | self.name = name 9 | self.left = left 10 | self.right = right 11 | 12 | 13 | def create_node_list_by_depth(tree_root): 14 | # BFS. 15 | levels = {} 16 | q = deque() 17 | q.append((tree_root, 0)) 18 | 19 | while len(q) > 0: 20 | node, level = q.popleft() 21 | if level not in levels: 22 | # First node in the level 23 | levels[level] = LinkedList() 24 | # Nodes already exist 25 | levels[level].add(node) 26 | 27 | # Push onto queue 28 | if node.left: 29 | q.append((node.left, level + 1)) 30 | if node.right: 31 | q.append((node.right, level + 1)) 32 | return levels 33 | 34 | 35 | def create_node_list_by_depth_b(tree): 36 | if not tree: 37 | return [] 38 | 39 | curr = tree 40 | result = [LinkedList([curr])] 41 | level = 0 42 | 43 | while result[level]: 44 | result.append(LinkedList()) 45 | for linked_list_node in result[level]: 46 | n = linked_list_node.value 47 | if n.left: 48 | result[level + 1].add(n.left) 49 | if n.right: 50 | result[level + 1].add(n.right) 51 | level += 1 52 | return result 53 | 54 | 55 | testable_functions = [create_node_list_by_depth, create_node_list_by_depth_b] 56 | 57 | 58 | def test_create_node_list_by_depth(): 59 | for f in testable_functions: 60 | node_h = BinaryNode("H") 61 | node_g = BinaryNode("G") 62 | node_f = BinaryNode("F") 63 | node_e = BinaryNode("E", node_g) 64 | node_d = BinaryNode("D", node_h) 65 | node_c = BinaryNode("C", None, node_f) 66 | node_b = BinaryNode("B", node_d, node_e) 67 | node_a = BinaryNode("A", node_b, node_c) 68 | lists = f(node_a) 69 | 70 | assert lists[0].values() == LinkedList([node_a]).values() 71 | assert lists[1].values() == LinkedList([node_b, node_c]).values() 72 | assert lists[2].values() == LinkedList([node_d, node_e, node_f]).values() 73 | assert lists[3].values() == LinkedList([node_h, node_g]).values() 74 | 75 | 76 | def example(): 77 | root = BinaryNode(0) 78 | root.left = BinaryNode(1) 79 | root.right = BinaryNode(2) 80 | root.left.left = BinaryNode(3) 81 | root.left.right = BinaryNode(4) 82 | root.right.left = BinaryNode(5) 83 | root.right.right = BinaryNode(6) 84 | 85 | levels = create_node_list_by_depth(root) 86 | print(levels) 87 | 88 | 89 | if __name__ == "__main__": 90 | example() 91 | -------------------------------------------------------------------------------- /chapter_04/p04_check_balanced.py: -------------------------------------------------------------------------------- 1 | class BinaryNode: 2 | def __init__(self, name): 3 | self.name = name 4 | self.left = None 5 | self.right = None 6 | 7 | 8 | # Version 2: 9 | # Traverse the tree and track the largest and smallest depth of each leaf node. 10 | # Then compare the largest and smallest depth. 11 | def is_balanced_v2(node): 12 | min_depth = 10**100 13 | max_depth = -(10**100) 14 | queue = [(node, 0)] 15 | visited = [node] 16 | 17 | while len(queue) > 0: 18 | curr_node, curr_depth = queue.pop(0) 19 | 20 | if curr_node.left is None and curr_node.right is None: 21 | if curr_depth > max_depth: 22 | max_depth = curr_depth 23 | if curr_depth < min_depth: 24 | min_depth = curr_depth 25 | else: 26 | if curr_node.left and curr_node.left not in visited: 27 | visited.append(curr_node.left) 28 | queue.append((curr_node.left, curr_depth + 1)) 29 | if curr_node.right and curr_node.right not in visited: 30 | visited.append(curr_node.right) 31 | queue.append((curr_node.right, curr_depth + 1)) 32 | 33 | return max_depth - min_depth < 2 34 | 35 | 36 | def find_max_depth(node, level=0): 37 | if node is None: 38 | return level 39 | if not node.left: 40 | return find_max_depth(node.right, level + 1) 41 | if not node.right: 42 | return find_max_depth(node.left, level + 1) 43 | return max( 44 | find_max_depth(node.left, level + 1), find_max_depth(node.right, level + 1) 45 | ) 46 | 47 | 48 | def find_min_depth(node, level=0): 49 | if node is None: 50 | return level 51 | if not node.left: 52 | return find_min_depth(node.right, level + 1) 53 | if not node.right: 54 | return find_min_depth(node.left, level + 1) 55 | return min( 56 | find_min_depth(node.left, level + 1), find_min_depth(node.right, level + 1) 57 | ) 58 | 59 | 60 | # Version 1: 61 | # Find the max tree depth and min tree depth independently. 62 | # Then compare their values. 63 | def is_balanced_v1(node): 64 | return find_max_depth(node) - find_min_depth(node) < 2 65 | 66 | 67 | # Alternative Recursive Approach 68 | def _find_height(root): 69 | if root is None: 70 | return 0 71 | left_height = _find_height(root.left) 72 | if left_height == -1: 73 | return -1 74 | 75 | right_height = _find_height(root.right) 76 | if right_height == -1: 77 | return -1 78 | 79 | if abs(left_height - right_height) > 1: 80 | return -1 81 | 82 | return max(left_height, right_height) + 1 83 | 84 | 85 | def is_balanced_v3(root): 86 | return _find_height(root) > -1 87 | 88 | 89 | def _gen_balanced_1(): 90 | root = BinaryNode(1) 91 | root.left = BinaryNode(2) 92 | return root 93 | 94 | 95 | def _gen_balanced_2(): 96 | root = BinaryNode(7) 97 | root.left = BinaryNode(2) 98 | root.left.left = BinaryNode(4) 99 | root.right = BinaryNode(3) 100 | root.right.left = BinaryNode(8) 101 | root.right.right = BinaryNode(9) 102 | root.right.right.right = BinaryNode(10) 103 | return root 104 | 105 | 106 | def _gen_unbalanced_1(): 107 | root = BinaryNode(1) 108 | root.left = BinaryNode(2) 109 | root.left.left = BinaryNode(4) 110 | root.left.right = BinaryNode(5) 111 | root.left.right.right = BinaryNode(6) 112 | root.left.right.right.right = BinaryNode(7) 113 | root.right = BinaryNode(3) 114 | root.right.left = BinaryNode(8) 115 | root.right.right = BinaryNode(9) 116 | root.right.right.right = BinaryNode(10) 117 | root.right.right.right.right = BinaryNode(11) 118 | return root 119 | 120 | 121 | def _gen_unbalanced_2(): 122 | tree = BinaryNode(1) 123 | tree.left = BinaryNode(2) 124 | tree.right = BinaryNode(9) 125 | tree.right.left = BinaryNode(10) 126 | tree.left.left = BinaryNode(3) 127 | tree.left.right = BinaryNode(7) 128 | tree.left.right.right = BinaryNode(5) 129 | tree.left.left.left = BinaryNode(6) 130 | tree.left.right.left = BinaryNode(12) 131 | tree.left.right.left.left = BinaryNode(16) 132 | tree.left.right.left.right = BinaryNode(0) 133 | return tree 134 | 135 | 136 | test_cases = [ 137 | (_gen_balanced_1, True), 138 | (_gen_balanced_2, True), 139 | (_gen_unbalanced_1, False), 140 | (_gen_unbalanced_2, False), 141 | ] 142 | 143 | testable_functions = [is_balanced_v1, is_balanced_v2, is_balanced_v3] 144 | 145 | 146 | def test_is_balanced(): 147 | for tree_gen, expected in test_cases: 148 | for is_balanced in testable_functions: 149 | error_msg = f"{is_balanced.__name__} failed on {tree_gen.__name__}" 150 | assert is_balanced(tree_gen()) == expected, error_msg 151 | 152 | 153 | if __name__ == "__main__": 154 | test_is_balanced() 155 | -------------------------------------------------------------------------------- /chapter_04/p05_validate_bst.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_search_tree import BinarySearchTree 2 | from chapter_04.binary_tree import BinaryTree 3 | 4 | 5 | def is_binary_search_tree(tree): 6 | return _is_bst(tree.root) 7 | 8 | 9 | def _is_bst(node, min_val=None, max_val=None): 10 | if not node: 11 | return True 12 | if (min_val and node.key < min_val) or (max_val and node.key >= max_val): 13 | return False 14 | return _is_bst(node.left, min_val, node.key) and _is_bst( 15 | node.right, node.key, max_val 16 | ) 17 | 18 | 19 | def test_is_binary_search_tree(): 20 | bst = BinarySearchTree() 21 | bst.insert(20) 22 | bst.insert(9) 23 | bst.insert(25) 24 | bst.insert(5) 25 | bst.insert(12) 26 | bst.insert(11) 27 | bst.insert(14) 28 | 29 | t = BinaryTree() 30 | n1 = t.insert(5, None) 31 | n2 = t.insert(4, n1) 32 | n3 = t.insert(6, n1) 33 | n4 = t.insert(3, n2) 34 | t.insert(6, n2) 35 | t.insert(5, n3) 36 | t.insert(2, n4) 37 | 38 | assert not is_binary_search_tree(t) 39 | assert is_binary_search_tree(bst) 40 | -------------------------------------------------------------------------------- /chapter_04/p06_successor.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_search_tree import BinarySearchTree 2 | 3 | 4 | def in_order_successor(input_node): 5 | if input_node is None: 6 | return None 7 | 8 | if input_node.right: 9 | current = input_node.right 10 | while current.left: 11 | current = current.left 12 | return current 13 | 14 | ancestor = input_node.parent 15 | child = input_node 16 | while ancestor and ancestor.right == child: 17 | child = ancestor 18 | ancestor = ancestor.parent 19 | return ancestor 20 | 21 | 22 | def test_in_order_successor(): 23 | bst = BinarySearchTree() 24 | bst.insert(20) 25 | bst.insert(9) 26 | bst.insert(25) 27 | bst.insert(5) 28 | bst.insert(12) 29 | bst.insert(11) 30 | bst.insert(14) 31 | 32 | # Test all nodes 33 | inputs = [5, 9, 11, 12, 14, 20, 25] 34 | outputs = inputs[1:] 35 | outputs.append(None) 36 | 37 | for x, y in zip(inputs, outputs): 38 | test = bst.get_node(x) 39 | succ = in_order_successor(test) 40 | if succ is not None: 41 | assert succ.key == y 42 | else: 43 | assert succ == y 44 | 45 | 46 | if __name__ == "__main__": 47 | test_in_order_successor() 48 | -------------------------------------------------------------------------------- /chapter_04/p07_build_order.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def determine_build_order(projects, dependencies): 5 | dependency_tree = {p: set() for p in projects} 6 | build_order = [] 7 | unbuilt_projects = set(projects) 8 | for dependency, project in dependencies: 9 | dependency_tree[project].add(dependency) 10 | 11 | while unbuilt_projects: 12 | something_built = False 13 | for project in list(unbuilt_projects): 14 | dependencies = dependency_tree[project] 15 | if not unbuilt_projects.intersection(dependencies): 16 | build_order.append(project) 17 | unbuilt_projects.remove(project) 18 | something_built = True 19 | if not something_built: 20 | raise NoValidBuildOrderError("No valid build order exists") 21 | 22 | return build_order 23 | 24 | 25 | class NoValidBuildOrderError(Exception): 26 | pass 27 | 28 | 29 | def test_determine_build_order(): 30 | projects = ["a", "b", "c", "d", "e", "f", "g"] 31 | dependencies = [ 32 | ("d", "g"), 33 | ("a", "e"), 34 | ("b", "e"), 35 | ("c", "a"), 36 | ("f", "a"), 37 | ("b", "a"), 38 | ("f", "c"), 39 | ("f", "b"), 40 | ] 41 | build_order = determine_build_order(projects, dependencies) 42 | for dependency, project in dependencies: 43 | assert build_order.index(dependency) < build_order.index(project) 44 | 45 | 46 | def test_impossible_build_order(): 47 | projects = ["a", "b"] 48 | dependencies = [("a", "b"), ("b", "a")] 49 | with pytest.raises(NoValidBuildOrderError): 50 | determine_build_order(projects, dependencies) 51 | -------------------------------------------------------------------------------- /chapter_04/p08_first_common_ancestor.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_tree import BinaryTree 2 | 3 | 4 | def first_common_ancestor(p, q): 5 | if not p or not q: 6 | return None 7 | depth_p = get_depth(p) 8 | depth_q = get_depth(q) 9 | delta = abs(depth_p - depth_q) 10 | if depth_p < depth_q: 11 | for _ in range(delta): 12 | q = q.parent 13 | elif depth_q < depth_p: 14 | for _ in range(delta): 15 | p = p.parent 16 | ancestor_p = p 17 | ancestor_q = q 18 | while ancestor_p != ancestor_q: 19 | ancestor_p = ancestor_p.parent 20 | ancestor_q = ancestor_q.parent 21 | return ancestor_p 22 | 23 | 24 | def get_depth(node): 25 | depth = 0 26 | while node is not None: 27 | node = node.parent 28 | depth += 1 29 | return depth 30 | 31 | 32 | if __name__ == "__main__": 33 | t = BinaryTree() 34 | n1 = t.insert(1, None) 35 | n2 = t.insert(2, n1) 36 | n3 = t.insert(3, n1) 37 | n4 = t.insert(4, n2) 38 | n5 = t.insert(5, n2) 39 | n7 = t.insert(7, n3) 40 | n8 = t.insert(8, n4) 41 | 42 | ancestor = first_common_ancestor(n3, n4) 43 | print(ancestor.key) 44 | -------------------------------------------------------------------------------- /chapter_04/p09_bst_sequences.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_search_tree import BinarySearchTree 2 | 3 | 4 | def find_bst_sequences(bst): 5 | if not bst.root: 6 | return [] 7 | return helper(bst.root) 8 | 9 | 10 | def helper(node): 11 | if not node: 12 | return [[]] 13 | 14 | right_sequences = helper(node.right) 15 | left_sequences = helper(node.left) 16 | sequences = [] 17 | for right in right_sequences: 18 | for left in left_sequences: 19 | sequences = weave(left, right, [node.key], sequences) 20 | return sequences 21 | 22 | 23 | def weave(first, second, prefix, results): 24 | if len(first) == 0 or len(second) == 0: 25 | result = prefix.copy() 26 | result.extend(first) 27 | result.extend(second) 28 | results.append(result) 29 | return results 30 | 31 | head = first[0] 32 | prefix.append(head) 33 | results = weave(first[1:], second, prefix, results) 34 | prefix.pop() 35 | head = second[0] 36 | prefix.append(head) 37 | results = weave(first, second[1:], prefix, results) 38 | prefix.pop() 39 | return results 40 | 41 | 42 | def find_bst_sequences_backtracking(bst): 43 | if not bst.root: 44 | return [] 45 | 46 | ret_backtracking = [] 47 | 48 | def backtracking(choices, weave): 49 | if not len(choices): 50 | ret_backtracking.append(weave) 51 | return 52 | 53 | for i in range(len(choices)): 54 | nextchoices = choices[:i] + choices[i + 1 :] 55 | if choices[i].left: 56 | nextchoices += [choices[i].left] 57 | if choices[i].right: 58 | nextchoices += [choices[i].right] 59 | backtracking(nextchoices, weave + [choices[i].key]) 60 | 61 | backtracking([bst.root], []) 62 | return ret_backtracking 63 | 64 | 65 | testable_functions = [find_bst_sequences, find_bst_sequences_backtracking] 66 | 67 | 68 | def test_find_bst_sequences(): 69 | for find_bst in testable_functions: 70 | bst = BinarySearchTree() 71 | bst.insert(2) 72 | bst.insert(1) 73 | bst.insert(3) 74 | sequences = find_bst(bst) 75 | assert [2, 1, 3] in sequences 76 | assert [2, 3, 1] in sequences 77 | assert len(sequences) == 2 78 | 79 | 80 | def example(): 81 | bst = BinarySearchTree() 82 | bst.insert(20) 83 | bst.insert(9) 84 | bst.insert(25) 85 | bst.insert(5) 86 | bst.insert(12) 87 | # bst.insert(11); 88 | # bst.insert(14); 89 | 90 | sequences = find_bst_sequences(bst) 91 | print(sequences) 92 | 93 | sequences = find_bst_sequences_backtracking(bst) 94 | print(sequences) 95 | 96 | 97 | if __name__ == "__main__": 98 | example() 99 | -------------------------------------------------------------------------------- /chapter_04/p10_check_subtree.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_tree import BinaryTree, Node 2 | 3 | 4 | class ComparableTreeNode(Node): 5 | def __eq__(self, other): 6 | if not isinstance(other, ComparableTreeNode): 7 | return False 8 | return ( 9 | self.key == other.key 10 | and self.left == other.left 11 | and self.right == other.right 12 | ) 13 | 14 | 15 | class ComparableBinaryTree(BinaryTree): 16 | NodeCls = ComparableTreeNode 17 | 18 | 19 | # "needle in a haystack" 20 | # haystack == the thing we're searching inside 21 | # needle == the thing we're looking for 22 | def is_subtree(haystack_tree, needle_tree): 23 | if not haystack_tree or not needle_tree: 24 | return False 25 | return _is_subtree(haystack_tree.root, needle_tree.root) 26 | 27 | 28 | def _is_subtree(haystack_node, needle_node): 29 | if haystack_node is None or needle_node is None: 30 | return False 31 | if haystack_node == needle_node: 32 | return True 33 | 34 | return _is_subtree(haystack_node.left, needle_node) or _is_subtree( 35 | haystack_node.right, needle_node 36 | ) 37 | 38 | 39 | if __name__ == "__main__": 40 | t1 = ComparableBinaryTree() 41 | n1 = t1.insert(1, None) 42 | n2 = t1.insert(2, n1) 43 | n3 = t1.insert(3, n1) 44 | n4 = t1.insert(4, n2) 45 | n5 = t1.insert(5, n2) 46 | n7 = t1.insert(7, n3) 47 | n8 = t1.insert(8, n4) 48 | 49 | t2 = ComparableBinaryTree() 50 | n40 = t2.insert(4, None) 51 | n80 = t2.insert(8, n40) 52 | # n90 = t2.insert(9, n40) 53 | 54 | print(is_subtree(t1, t2)) 55 | -------------------------------------------------------------------------------- /chapter_04/p11_random_node.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections import defaultdict 3 | 4 | 5 | class Node: 6 | def __init__(self, key): 7 | self.key = key 8 | self.parent = None 9 | self.left = None 10 | self.right = None 11 | self.size = 1 12 | 13 | @property 14 | def left_size(self): 15 | if self.left: 16 | return self.left.size 17 | return 0 18 | 19 | @property 20 | def right_size(self): 21 | if self.right: 22 | return self.right.size 23 | return 0 24 | 25 | 26 | class BinarySearchTree: 27 | def __init__(self): 28 | self.root = None 29 | 30 | def insert(self, key): 31 | new = Node(key) 32 | if self.root is None: 33 | self.root = new 34 | return 35 | 36 | current = self.root 37 | while current: 38 | current.size += 1 39 | if current.key >= key: 40 | if current.left is None: 41 | current.left = new 42 | new.parent = current 43 | return 44 | current = current.left 45 | else: 46 | if current.right is None: 47 | current.right = new 48 | new.parent = current 49 | return 50 | current = current.right 51 | 52 | def delete_helper(self, node, key): 53 | 54 | if node is None: 55 | return node 56 | 57 | if key < node.key: 58 | node.left = self.delete_helper(node.left, key) 59 | 60 | elif key > node.key: 61 | node.right = self.delete_helper(node.right, key) 62 | 63 | else: 64 | if node.left is None: 65 | temp, node = node.right, None 66 | return temp 67 | 68 | elif node.right is None: 69 | temp, node = node.left, None 70 | return temp 71 | 72 | temp = min_val_node(node.right) 73 | node.key = temp.key 74 | node.right = self.delete_helper(node.right, temp.key) 75 | 76 | node.size -= 1 77 | return node 78 | 79 | def delete(self, key): 80 | self.delete_helper(self.root, key) 81 | 82 | def get_node(self, key): 83 | current = self.root 84 | while current: 85 | if current.key == key: 86 | return current 87 | 88 | if current.key > key: 89 | current = current.left 90 | else: 91 | current = current.right 92 | raise Exception("No such value in the tree") 93 | 94 | def get_random_node(self): 95 | current = self.root 96 | # with probability 1/N = 1/(1+l+r) return node 97 | # with probability l/N go down left 98 | # with probability r/N go down right 99 | 100 | while current: 101 | choices = ["self", "left", "right"] 102 | choice_weights = [1, current.left_size, current.right_size] 103 | decision = random.choices(choices, choice_weights)[0] 104 | 105 | if decision == "self": 106 | return current 107 | 108 | if decision == "left": 109 | current = current.left 110 | elif decision == "right": 111 | current = current.right 112 | else: 113 | raise RuntimeError("Should not be possible") 114 | 115 | 116 | def min_val_node(node): 117 | current = node 118 | # loop down to find the leftmost leaf 119 | while current.left is not None: 120 | current = current.left 121 | 122 | return current 123 | 124 | 125 | def example(): 126 | bst = BinarySearchTree() 127 | bst.insert(20) 128 | bst.insert(9) 129 | bst.insert(25) 130 | bst.insert(5) 131 | bst.insert(12) 132 | bst.insert(11) 133 | bst.insert(14) 134 | bst.delete(12) 135 | 136 | chosen_counts = defaultdict(int) 137 | for _ in range(6000): 138 | node = bst.get_random_node() 139 | chosen_counts[node.key] += 1 140 | 141 | print(chosen_counts.values()) 142 | 143 | 144 | if __name__ == "__main__": 145 | example() 146 | -------------------------------------------------------------------------------- /chapter_04/p12_paths_with_sum.py: -------------------------------------------------------------------------------- 1 | from chapter_04.binary_tree import BinaryTree 2 | 3 | # Brute Force O(NlogN) 4 | 5 | 6 | def count_sum_paths(tree, target): 7 | if not isinstance(tree, BinaryTree): 8 | return None 9 | return _count_sum_paths(tree.root, target) 10 | 11 | 12 | def _count_sum_paths(node, target_sum): 13 | if not node: 14 | return 0 15 | return ( 16 | pathsfrom(node, target_sum) 17 | + _count_sum_paths(node.left, target_sum) 18 | + _count_sum_paths(node.right, target_sum) 19 | ) 20 | 21 | 22 | def pathsfrom(node, target_sum): 23 | if not node: 24 | return 0 25 | target_sum -= node.key 26 | counter = 0 27 | if target_sum == 0: 28 | counter += 1 29 | return ( 30 | counter + pathsfrom(node.left, target_sum) + pathsfrom(node.right, target_sum) 31 | ) 32 | 33 | 34 | # Optimized O(N) 35 | 36 | 37 | def count_sum_paths_optimized(tree, target_sum): 38 | if not isinstance(tree, BinaryTree): 39 | return None 40 | return _count_sum_paths_optimizied(tree.root, target_sum) 41 | 42 | 43 | def _count_sum_paths_optimizied(node, target_sum, running=0, hashtable=None): 44 | if hashtable is None: 45 | hashtable = {} 46 | if not node: 47 | return 0 48 | running += node.key 49 | total = hashtable.get(running - target_sum, 0) 50 | if running == target_sum: 51 | total += 1 52 | increment(hashtable, running, 1) 53 | left_count = _count_sum_paths_optimizied(node.left, target_sum, running, hashtable) 54 | right_count = _count_sum_paths_optimizied( 55 | node.right, target_sum, running, hashtable 56 | ) 57 | total += left_count + right_count 58 | increment(hashtable, running, -1) 59 | return total 60 | 61 | 62 | def increment(hashmap, key, delta): 63 | hashmap.setdefault(key, 0) 64 | hashmap[key] += delta 65 | if hashmap[key] == 0: 66 | hashmap.pop(key) 67 | 68 | 69 | if __name__ == "__main__": 70 | t1 = BinaryTree() 71 | n1 = t1.insert(10, None) 72 | n2 = t1.insert(5, n1) 73 | n3 = t1.insert(-3, n1) 74 | n4 = t1.insert(3, n2) 75 | n5 = t1.insert(2, n2) 76 | n6 = t1.insert(3, n4) 77 | n7 = t1.insert(-2, n4) 78 | n8 = t1.insert(1, n5) 79 | n9 = t1.insert(11, n3) 80 | n10 = t1.insert(8, n9) 81 | n11 = t1.insert(-8, n10) 82 | 83 | print(count_sum_paths(t1, 8)) 84 | print(count_sum_paths(t1, 6)) 85 | print(count_sum_paths_optimized(t1, 8)) 86 | print(count_sum_paths_optimized(t1, 6)) 87 | -------------------------------------------------------------------------------- /chapter_05/p01_insertion.py: -------------------------------------------------------------------------------- 1 | def bits_insertion(n, m, i, j): 2 | ones_left = -1 << (j + 1) # shift 1s over to the left, before position j 3 | ones_right = (1 << i) - 1 # place 1s to the right of position i 4 | mask = ones_left | ones_right # encapsulate 0s with the 1s from above 5 | cleared = n & mask # zero bits in positions j through i 6 | moved = m << i # shift m over i places, prepped for n insertion 7 | return cleared | moved # answer is the value after insertion 8 | 9 | 10 | def bits_insertion_easy_to_understand(n, m, i, j): 11 | # do a liner search through the bits of M (from tail to head) 12 | # and if you find 1, do a bit insertion to N 13 | # if you find 0, clear idxth bit of N using a mask 14 | for idx in range(j - i + 1): 15 | if (m >> idx) & 1 != 0: 16 | # set bit 17 | n |= 1 << (idx + i) 18 | else: 19 | # clear bit 20 | mask = ~(1 << (idx + i)) 21 | n &= mask 22 | 23 | return n 24 | 25 | 26 | test_cases = [ 27 | ((int("10000000000", 2), int("10011", 2), 2, 6), int("10001001100", 2)), 28 | ((int("11111111111", 2), int("10011", 2), 2, 6), int("11111001111", 2)), 29 | ] 30 | 31 | testable_functions = [bits_insertion, bits_insertion_easy_to_understand] 32 | 33 | 34 | def test_bits_insertion(): 35 | for bits_insert in testable_functions: 36 | for (n, m, i, j), expected in test_cases: 37 | calculated = bits_insert(n, m, i, j) 38 | error_msg = f"{bits_insert.__name__}: {calculated:b} != {expected:b}" 39 | assert bits_insert(n, m, i, j) == expected, error_msg 40 | 41 | 42 | if __name__ == "__main__": 43 | test_bits_insertion() 44 | -------------------------------------------------------------------------------- /chapter_05/p02_binary_to_string.py: -------------------------------------------------------------------------------- 1 | def bin_to_string(number): 2 | bit_str = "." 3 | if number >= 1.0 or number <= 0.0: 4 | good_nums = ["1"] + ["1.0" + "0" * i for i in range(32)] 5 | good_nums += ["0"] + ["0.0" + "0" * i for i in range(32)] 6 | if str(number) not in good_nums: 7 | return "ERROR" 8 | while number > 0: 9 | if len(bit_str) > 32: 10 | return bit_str 11 | two = number * 2 12 | # Testing if 1 after decimal point 13 | if two >= 1: 14 | bit_str += "1" 15 | number = two - 1 16 | else: 17 | bit_str += "0" 18 | number = two 19 | return bit_str.ljust(33, "0") 20 | 21 | 22 | def bin_to_string_alt(number): 23 | number_s31 = int(number * (2**32)) 24 | if number_s31 > 2**32: 25 | return "ERROR" 26 | 27 | if number_s31 == (2**32): 28 | number_s31 = 2**32 - 1 29 | 30 | ans = "" 31 | for _ in range(32): 32 | ans = str(number_s31 % 2) + ans 33 | number_s31 = number_s31 // 2 34 | 35 | return "." + ans 36 | 37 | 38 | def example(): 39 | for number in [0.625, 0, 0.1, 0.101, 0.2, 0.5, 1, 2]: 40 | bit_str = bin_to_string(number) 41 | response = bit_str if len(bit_str) <= 33 else "ERROR" 42 | print(f"Number: {number}, Binary String: {response}") 43 | bit_str = bin_to_string_alt(number) 44 | response = bit_str if len(bit_str) <= 33 else "ERROR" 45 | print(f"Number: {number}, Binary String: {response}") 46 | assert bin_to_string(number) == bin_to_string_alt(number) 47 | 48 | 49 | if __name__ == "__main__": 50 | example() 51 | -------------------------------------------------------------------------------- /chapter_05/p03_flip_bit_to_win.py: -------------------------------------------------------------------------------- 1 | def flip_bit_to_win(number): 2 | number_str = bin(number)[2:] 3 | max_cnt, cnt, cnt0 = 0, 0, 0 4 | i = len(number_str) # start index 5 | while i: 6 | if int(number_str[i - 1]): 7 | cnt += 1 8 | else: 9 | if cnt0 == 0: 10 | temp_i = i 11 | cnt0 = 1 12 | else: # second 0 13 | max_cnt = max(cnt, max_cnt) 14 | i = temp_i # rewind 15 | cnt0 = 0 16 | cnt = 0 17 | i -= 1 18 | 19 | max_cnt = max(cnt, max_cnt) 20 | 21 | return max_cnt + 1 22 | 23 | 24 | def flip_bit_to_win_alt(num): 25 | longest, current_segment, past_segment = 1, 0, 0 26 | while num != 0: 27 | if num & 1: # Current bit is 1 28 | current_segment += 1 29 | else: # Current bit is 0 30 | past_segment = 0 if (num & 2 is True) else current_segment 31 | current_segment = 0 32 | longest = max(current_segment + past_segment + 1, longest) 33 | num >>= 1 # Move 1 bit to the right 34 | return longest 35 | 36 | 37 | test_cases = [ 38 | (0b0, 1), 39 | (0b111, 4), 40 | (0b10011100111, 4), 41 | (0b10110110111, 6), 42 | (0b11011101111, 8), 43 | ] 44 | testable_functions = [flip_bit_to_win, flip_bit_to_win_alt] 45 | 46 | 47 | def test_flip_bit_to_win(): 48 | for fli_bit in testable_functions: 49 | for num, expected in test_cases: 50 | assert fli_bit(num) == expected 51 | 52 | 53 | if __name__ == "__main__": 54 | test_flip_bit_to_win() 55 | -------------------------------------------------------------------------------- /chapter_05/p04_next_number.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | def _bit(x: int, i: int) -> int: 5 | return x & (1 << i) 6 | 7 | 8 | def next_smaller(x: int) -> Optional[int]: 9 | if not x & (x + 1): 10 | return None 11 | zeros = ones = 0 12 | while _bit(x, i=ones): 13 | ones += 1 14 | while not _bit(x, i=ones + zeros): 15 | zeros += 1 16 | return x - (1 << ones) - (1 << zeros - 1) + 1 17 | 18 | 19 | def next_larger(x: int) -> int: 20 | zeros = ones = 0 21 | while not _bit(x, i=zeros): 22 | zeros += 1 23 | while _bit(x, i=zeros + ones): 24 | ones += 1 25 | return x + (1 << zeros) + (1 << ones - 1) - 1 26 | 27 | 28 | def test_next_smaller_than_0b11111() -> None: 29 | assert next_smaller(0b11111) is None 30 | 31 | 32 | def test_next_smaller_than_0b10110() -> None: 33 | assert next_smaller(0b10110) == 0b10101 34 | 35 | 36 | def test_next_larger_than_0b10110() -> None: 37 | assert next_larger(0b10110) == 0b11001 38 | 39 | 40 | if __name__ == "__main__": 41 | x = int(input("Enter a positive integer: ")) 42 | while x < 0: 43 | x = int(input(str(x) + " is not positive. Please try again: ")) 44 | print("Next smaller: ", next_smaller(x)) 45 | print("Next larger: ", next_larger(x)) 46 | -------------------------------------------------------------------------------- /chapter_05/p06_conversion.py: -------------------------------------------------------------------------------- 1 | def bit_swap_required(a: int, b: int) -> int: 2 | count, c = 0, a ^ b 3 | while c: 4 | count, c = count + 1, c & (c - 1) 5 | return count 6 | 7 | 8 | def test_29_15() -> None: 9 | a = 0b11101 # 29 10 | b = 0b01111 # 15 11 | assert bit_swap_required(a, b) == 2 12 | -------------------------------------------------------------------------------- /chapter_05/p07_pairwise_swap.py: -------------------------------------------------------------------------------- 1 | def pairwise_swap(number): 2 | mask_10 = 0xAAAAAAAA # 32 bits 3 | mask_01 = 0x55555555 # 32 bits 4 | num_evn = number & mask_10 5 | num_odd = number & mask_01 6 | swp_num = (num_evn >> 1) | (num_odd << 1) 7 | return swp_num 8 | 9 | 10 | def test_pairwise_swap(): 11 | view_output = 1 12 | for number, exp_output in zip([123, 781, 278], [183, 782, 553]): 13 | swap_num = pairwise_swap(number) 14 | if view_output: 15 | print(f"Number: {bin(number)}") 16 | print(f"Swapped: {bin(swap_num)}") 17 | assert swap_num == exp_output 18 | 19 | 20 | if __name__ == "__main__": 21 | test_pairwise_swap() 22 | -------------------------------------------------------------------------------- /chapter_05/p08_draw_line.py: -------------------------------------------------------------------------------- 1 | def draw_line(screen: bytearray, width: int, x1: int, x2: int, y: int) -> None: 2 | left_byte, right_byte = (y * width + x1) // 8, (y * width + x2) // 8 3 | left_mask, right_mask = 0xFF >> x1 % 8, (0xFF >> x2 % 8 + 1) ^ 0xFF 4 | if left_byte == right_byte: 5 | screen[left_byte] |= left_mask & right_mask 6 | else: 7 | screen[left_byte] |= left_mask 8 | for i in range(left_byte + 1, right_byte): 9 | screen[i] = 0xFF 10 | screen[right_byte] |= right_mask 11 | 12 | 13 | def test_0b11111111_0b11111111() -> None: 14 | screen = bytearray(2) 15 | draw_line(screen, width=16, x1=0, x2=15, y=0) 16 | assert screen == bytearray([0b11111111, 0b11111111]) 17 | 18 | 19 | def test_0b01111100() -> None: 20 | screen = bytearray(1) 21 | draw_line(screen, width=8, x1=1, x2=5, y=0) 22 | assert screen == bytearray([0b01111100]) 23 | 24 | 25 | def test_0b01111100_with_y_equals_1() -> None: 26 | screen = bytearray(3) 27 | draw_line(screen, width=8, x1=1, x2=5, y=1) 28 | assert screen == bytearray([0b00000000, 0b01111100, 0b000000000]) 29 | 30 | 31 | def test_0b00000011_0b11111111_0b11000000() -> None: 32 | screen = bytearray(3) 33 | draw_line(screen, width=24, x1=6, x2=17, y=0) 34 | assert screen == bytearray([0b00000011, 0b11111111, 0b11000000]) 35 | -------------------------------------------------------------------------------- /chapter_06/p07_the_apocalypse.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import random 3 | 4 | 5 | class Gender(enum.Enum): 6 | BOY = enum.auto() 7 | GIRL = enum.auto() 8 | 9 | 10 | def simulate_child() -> Gender: 11 | return random.choice([Gender.BOY, Gender.GIRL]) 12 | 13 | 14 | class Family: 15 | def __init__(self, num_boys: int, num_girls: int) -> None: 16 | self.num_boys = num_boys 17 | self.num_girls = num_girls 18 | 19 | 20 | def simulate_family() -> Family: 21 | num_boys = sum(1 for _ in iter(simulate_child, Gender.GIRL)) 22 | return Family(num_boys=num_boys, num_girls=1) 23 | 24 | 25 | def simulate_apocalypse(num_families: int) -> float: 26 | total_boys = total_girls = 0 27 | for _ in range(num_families): 28 | family = simulate_family() 29 | total_boys += family.num_boys 30 | total_girls += family.num_girls 31 | return total_boys / (total_boys + total_girls) 32 | 33 | 34 | def test_apocalypse(): 35 | # ratio should be really close to 0.5 36 | ratio = simulate_apocalypse(30000) 37 | assert abs(0.5 - ratio) < 0.01 38 | 39 | 40 | if __name__ == "__main__": 41 | n = int(input("How many families should we simulate? ")) 42 | print("Proportion of children that are boys:", simulate_apocalypse(n)) 43 | -------------------------------------------------------------------------------- /chapter_06/p10_poison.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List, Optional 3 | 4 | DAYS_FOR_RESULT = 7 5 | 6 | 7 | class _TestStrip: 8 | def __init__(self) -> None: 9 | self.has_poison = False 10 | self.day_poisoned: Optional[int] = None 11 | 12 | 13 | class World: 14 | def __init__( 15 | self, num_test_strips: int, num_bottles: int, poisoned_bottle_num: int 16 | ) -> None: 17 | self._num_test_strips = num_test_strips 18 | self._test_strips = [_TestStrip() for i in range(num_test_strips)] 19 | self._num_bottles = num_bottles 20 | self._poisoned_bottle_num = poisoned_bottle_num 21 | self._day = 0 22 | 23 | @property 24 | def num_bottles(self) -> int: 25 | return self._num_bottles 26 | 27 | @property 28 | def num_test_strips(self) -> int: 29 | return self._num_test_strips 30 | 31 | @property 32 | def day(self) -> int: 33 | return self._day 34 | 35 | @day.setter 36 | def day(self, day: int) -> None: 37 | if day < self._day: 38 | raise ValueError("day cannot be decreased") 39 | self._day = day 40 | 41 | def add_drop(self, bottle_num: int, test_strip_num: int) -> None: 42 | test_strip = self._test_strips[test_strip_num] 43 | if bottle_num == self._poisoned_bottle_num and not test_strip.has_poison: 44 | test_strip.has_poison, test_strip.day_poisoned = True, self.day 45 | 46 | def positive_test_strips(self) -> List[int]: 47 | res: List[int] = [] 48 | for test_strip_num, test_strip in enumerate(self._test_strips): 49 | if ( 50 | test_strip.has_poison 51 | and self.day - test_strip.day_poisoned >= DAYS_FOR_RESULT 52 | ): 53 | res.append(test_strip_num) 54 | return res 55 | 56 | 57 | def find_poison(world: World) -> int: 58 | for i in range(world.num_bottles): 59 | for j in range(world.num_test_strips): 60 | if i & (1 << j): 61 | world.add_drop(bottle_num=i, test_strip_num=j) 62 | world.day += DAYS_FOR_RESULT 63 | return sum(1 << i for i in world.positive_test_strips()) 64 | 65 | 66 | def test_find_poison(): 67 | poisoned_bottle_num = random.randrange(1000) 68 | world = World( 69 | num_bottles=1000, num_test_strips=10, poisoned_bottle_num=poisoned_bottle_num 70 | ) 71 | assert find_poison(world) == poisoned_bottle_num 72 | return poisoned_bottle_num, world.day 73 | 74 | 75 | def example(): 76 | poisoned_bottle_num, days = test_find_poison() 77 | print("Found poison in bottle number", poisoned_bottle_num, "in", days, "days.") 78 | 79 | 80 | if __name__ == "__main__": 81 | example() 82 | -------------------------------------------------------------------------------- /chapter_07/p04_parking_lot.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from random import randrange 3 | 4 | import pytest 5 | 6 | 7 | class Vehicle: 8 | types = [] 9 | 10 | def __init__(self, model, size, number): 11 | self.model = model 12 | self.size = size 13 | self.number = number 14 | self.parked = False 15 | 16 | def is_parked(self): 17 | if self.parked: 18 | print("Vehicle is parked") 19 | return True 20 | 21 | print("Vehicle is not parked") 22 | return False 23 | 24 | 25 | class Bike(Vehicle): 26 | pass 27 | 28 | 29 | class Scooter(Vehicle): 30 | pass 31 | 32 | 33 | class Car(Vehicle): 34 | pass 35 | 36 | 37 | class Bus(Vehicle): 38 | pass 39 | 40 | 41 | class ParkZone: 42 | def __init__(self): 43 | self.space_available = 10 44 | self.parked = {} 45 | 46 | def park(self, vehicle): 47 | if self.is_space_available(vehicle.size): 48 | token = self.register(vehicle) 49 | self.space_available -= vehicle.size 50 | vehicle.parked = True 51 | print(vehicle.model, " has been parked.") 52 | print("Token: ", token, ", Space available ", self.space_available) 53 | return token 54 | print("No space available") 55 | return None 56 | 57 | def is_space_available(self, size): 58 | return (self.space_available - size) >= 0 59 | 60 | def register(self, vehicle): 61 | token = self.generate_token() 62 | while token in self.parked: 63 | token = self.generate_token() 64 | self.parked[token] = vehicle 65 | return token 66 | 67 | def generate_token(self): 68 | return randrange(1111, 9999) 69 | 70 | def depark(self, token): 71 | if token in self.parked: 72 | parked_vehicle = self.parked[token] 73 | parked_vehicle.parked = False 74 | self.space_available += parked_vehicle.size 75 | print(parked_vehicle.model, "has been deparked") 76 | print("Space Available: ", self.space_available) 77 | return self.parked.pop(token) 78 | raise ValueError("Invalid token or vehicle not found") 79 | 80 | def list_parked_vehicles(self): 81 | print("------Parked Vehicles------") 82 | for vehicle in self.parked.values(): 83 | print(vehicle.model, vehicle.size, vehicle.number) 84 | 85 | 86 | def test_parking_lot(): 87 | bike = Bike("Suzuki Access", 1, "MH14AB1234") 88 | assert not bike.is_parked() 89 | park_zone = ParkZone() 90 | token = park_zone.park(bike) 91 | assert bike.is_parked() 92 | assert park_zone.depark(token) == bike 93 | assert not bike.is_parked() 94 | 95 | car = Car("Honda Jazz", 5, "MU268A") 96 | assert not car.is_parked() 97 | car_token = park_zone.park(car) 98 | assert car.is_parked() 99 | with pytest.raises(ValueError, match="Invalid token or vehicle not found"): 100 | park_zone.depark(token) 101 | 102 | assert park_zone.depark(car_token) == car 103 | assert not car.is_parked() 104 | 105 | bus = Bus("Volvo", 5, "AN657") 106 | park_zone.park(bus) 107 | 108 | scooter = Scooter("Honda Activa", 1, "GI653") 109 | park_zone.park(scooter) 110 | park_zone.list_parked_vehicles() 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /chapter_08/p01_triple_step.py: -------------------------------------------------------------------------------- 1 | def triple_hop(x): 2 | if x < 0: 3 | return 0 4 | if x == 0: 5 | return 1 6 | if x == 1: 7 | return 1 8 | return triple_hop(x - 1) + triple_hop(x - 2) + triple_hop(x - 3) 9 | 10 | 11 | def method_2(x): 12 | memo = [-1] * (x + 1) 13 | return triple_hop_recursive(x, memo) 14 | 15 | 16 | def triple_hop_recursive(x, memo): 17 | if x < 0: 18 | return 0 19 | memo[0] = 1 20 | if x >= 1: 21 | memo[1] = 1 22 | if x >= 2: 23 | memo[2] = memo[1] + memo[0] 24 | if x > 2: 25 | for i in range(3, x + 1): 26 | memo[i] = memo[i - 1] + memo[i - 2] + memo[i - 3] 27 | return memo[x] 28 | 29 | 30 | if __name__ == "__main__": 31 | print(triple_hop(1)) 32 | print(triple_hop(2)) 33 | print(triple_hop(3)) 34 | print(triple_hop(4)) 35 | print(triple_hop(5)) 36 | print(triple_hop(6)) 37 | 38 | print(method_2(1)) 39 | print(method_2(2)) 40 | print(method_2(3)) 41 | print(method_2(4)) 42 | print(method_2(5)) 43 | print(method_2(6)) 44 | -------------------------------------------------------------------------------- /chapter_08/p02_robot_grid.py: -------------------------------------------------------------------------------- 1 | # Solution with recursion O(2^r+c) 2 | def get_path(maze): 3 | if not maze: 4 | return None 5 | path = [] 6 | if is_path(maze, len(maze) - 1, len(maze[0]) - 1, path): 7 | return path 8 | return None 9 | 10 | 11 | def is_path(maze, row, col, path): 12 | # if out of bounds or not available, return 13 | if col < 0 or row < 0 or not maze[row][col]: 14 | return False 15 | 16 | is_at_origin = (row == 0) and (col == 0) 17 | 18 | # if there's a path from the start to here, add my location 19 | if ( 20 | is_at_origin 21 | or is_path(maze, row, col - 1, path) 22 | or is_path(maze, row - 1, col, path) 23 | ): 24 | point = (row, col) 25 | path.append(point) 26 | return True 27 | 28 | return False 29 | 30 | 31 | # Solution with memoization 32 | def get_path_memoized(maze): 33 | if not maze: 34 | return None 35 | path = [] 36 | failed_points = set() 37 | if is_path_memoized(maze, len(maze) - 1, len(maze[0]) - 1, path, failed_points): 38 | return path 39 | return None 40 | 41 | 42 | def is_path_memoized(maze, row, col, path, failed_points): 43 | # If out of bounds or not availabe, return 44 | if col < 0 or row < 0 or not maze[row][col]: 45 | return False 46 | 47 | point = (row, col) 48 | 49 | # if we've already visisted this cell, return 50 | if point in failed_points: 51 | return False 52 | 53 | is_at_origin = (row == 0) and (col == 0) 54 | 55 | # If there's a path from start to my current location, add my location 56 | if ( 57 | is_at_origin 58 | or is_path_memoized(maze, row, col - 1, path, failed_points) 59 | or is_path_memoized(maze, row - 1, col, path, failed_points) 60 | ): 61 | path.append(point) 62 | return True 63 | 64 | failed_points.add(point) 65 | return False 66 | 67 | 68 | if __name__ == "__main__": 69 | print(get_path([[True, True], [True, True]])) 70 | print(get_path_memoized([[True, True], [False, True]])) 71 | -------------------------------------------------------------------------------- /chapter_08/p03_magic_index.py: -------------------------------------------------------------------------------- 1 | NOT_FOUND = -1 2 | 3 | 4 | def magic_index(array, min_index=0, max_index=None): 5 | if max_index is None: 6 | max_index = len(array) - 1 7 | 8 | if max_index < min_index: 9 | return NOT_FOUND 10 | 11 | mid = (max_index + min_index) // 2 12 | if array[mid] == mid: 13 | return mid 14 | if array[mid] < mid: 15 | return magic_index(array, mid + 1, max_index) 16 | if array[mid] > mid: 17 | return magic_index(array, min_index, mid - 1) 18 | 19 | 20 | def magic_index_non_distinct(array, min_index=0, max_index=None): 21 | if max_index is None: 22 | max_index = len(array) - 1 23 | 24 | if max_index < min_index: 25 | return NOT_FOUND 26 | 27 | mid = (max_index + min_index) // 2 28 | if array[mid] == mid: 29 | return mid 30 | 31 | # Search left recursively 32 | left_index = min(mid - 1, array[mid]) 33 | left = magic_index_non_distinct(array, min_index, left_index) 34 | if left >= 0: 35 | return left 36 | 37 | # Search right recursively 38 | right_index = max(mid + 1, array[mid]) 39 | return magic_index_non_distinct(array, right_index, max_index) 40 | 41 | 42 | test_cases = [ 43 | ([-14, -12, 0, 1, 2, 5, 9, 10, 23, 25], 5), 44 | ([-14, -12, 0, 1, 2, 7, 9, 10, 23, 25], NOT_FOUND), 45 | ([0, 1, 2, 3, 4], 2), 46 | ([], NOT_FOUND), 47 | ] 48 | 49 | followup_test_cases = test_cases + [ 50 | ([-10, -5, 2, 2, 2, 3, 4, 7, 9, 12, 13], 2), 51 | ] 52 | 53 | 54 | def test_magic_index(): 55 | for array, expected in test_cases: 56 | assert magic_index(array) == expected 57 | 58 | 59 | def test_magic_index_non_distinct(): 60 | for array, expected in followup_test_cases: 61 | assert magic_index_non_distinct(array) == expected 62 | 63 | 64 | if __name__ == "__main__": 65 | test_magic_index() 66 | test_magic_index_non_distinct() 67 | -------------------------------------------------------------------------------- /chapter_08/p04_power_set.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | # Solution using recursion 5 | def get_subsets_a(setz, index=None): 6 | if index is None: 7 | index = len(setz) - 1 8 | if index == -1: 9 | return [[]] 10 | 11 | old_subs = get_subsets_a(setz, index - 1) 12 | new_subs = [] 13 | item = setz[index] 14 | for val in old_subs: 15 | new_subs.append(val) 16 | # List is mutable 17 | entry = copy.deepcopy(val) 18 | entry.append(item) 19 | new_subs.append(entry) 20 | 21 | return new_subs 22 | 23 | 24 | # Combinatorics Solution 25 | def get_subsets_b(aset): 26 | all_subsets = [] 27 | max_n = 1 << len(aset) 28 | for k in range(max_n): 29 | subset = convert_int_to_set(k, aset) 30 | all_subsets.append(subset) 31 | return all_subsets 32 | 33 | 34 | def convert_int_to_set(x, aset): 35 | subset = [] 36 | index = 0 37 | k = x 38 | while k > 0: 39 | if k & 1 == 1 and aset[index] not in subset: 40 | subset.append(aset[index]) 41 | index += 1 42 | k >>= 1 43 | return subset 44 | 45 | 46 | # alternative recursive solution. 47 | def get_subsets_c(_set): 48 | subsets = [[]] 49 | 50 | def recurse(current_set, remaining_set): 51 | if len(remaining_set) == 0: # base case 52 | return 53 | 54 | for i in range(len(remaining_set)): 55 | if current_set + [remaining_set[i]] not in subsets: 56 | subsets.append(current_set + [remaining_set[i]]) 57 | recurse(current_set + [remaining_set[i]], remaining_set[i + 1 :]) 58 | 59 | recurse([], _set) 60 | return subsets 61 | 62 | 63 | testable_functions = [get_subsets_a, get_subsets_b, get_subsets_c] 64 | 65 | test_cases = [({1, 2, 3}, {(), (1,), (1, 2), (1, 2, 3), (1, 3), (2,), (2, 3), (3,)})] 66 | 67 | 68 | def test_get_subsets(): 69 | for input_set, expected in test_cases: 70 | for get_subsets in testable_functions: 71 | results = get_subsets(list(input_set)) 72 | results = {tuple(s) for s in results} 73 | assert results == expected 74 | 75 | 76 | if __name__ == "__main__": 77 | print(get_subsets_a([1, 2, 3])) 78 | print("") 79 | print(get_subsets_b([1, 2, 3])) 80 | print("") 81 | print(get_subsets_c([1, 2, 3])) 82 | -------------------------------------------------------------------------------- /chapter_08/p05_recursive_multiply.py: -------------------------------------------------------------------------------- 1 | # naive implementation 2 | def multiply(a, b, answer): 3 | if answer == 0 and a != 0 and b != 0: 4 | answer = a 5 | if a == 1: 6 | return answer 7 | 8 | if b == 1: 9 | return answer 10 | 11 | answer += a 12 | return multiply(a, b - 1, answer) 13 | 14 | 15 | # Solution 1 16 | def min_product(a, b): 17 | bigger = b if a < b else a # a < b ? b : a 18 | smaller = a if a < b else b # a < b ? a : b 19 | return min_product_helper(smaller, bigger) 20 | 21 | 22 | def min_product_helper(smaller, bigger): 23 | if smaller == 0: 24 | return 0 25 | 26 | if smaller == 1: 27 | return bigger 28 | 29 | # Compute half. If uneven, compute other half. If even, double it 30 | s = smaller >> 1 # divide by 2 31 | side1 = min_product(s, bigger) 32 | side2 = side1 33 | if smaller % 2 == 1: 34 | side2 = min_product_helper(smaller - s, bigger) 35 | 36 | return side1 + side2 37 | 38 | 39 | # Solution 2 40 | def min_product_2(a, b): 41 | bigger = b if a < b else a # a < b ? b : a 42 | smaller = a if a < b else b # a < b ? a : b 43 | return min_product_2_helper(smaller, bigger, {}) 44 | 45 | 46 | def min_product_2_helper(smaller, bigger, memo): 47 | if smaller == 0: 48 | return 0 49 | 50 | if smaller == 1: 51 | return bigger 52 | 53 | if smaller in memo: 54 | return memo[smaller] 55 | 56 | # Compute half. If uneven, compute other half. If even double it 57 | s = smaller >> 1 58 | side1 = min_product_2_helper(s, bigger, memo) 59 | side2 = side1 60 | if smaller % 2 == 1: 61 | side2 = min_product_2_helper(smaller - s, bigger, memo) 62 | # sum and cache 63 | memo[smaller] = side1 + side2 64 | return memo[smaller] 65 | 66 | 67 | # Solution 3 68 | def min_product_3(a, b): 69 | bigger = b if a < b else a # a < b ? b : a 70 | smaller = a if a < b else b # a < b ? a : b 71 | return min_product_3_helper(smaller, bigger) 72 | 73 | 74 | def min_product_3_helper(smaller, bigger): 75 | if smaller == 0: 76 | return 0 77 | 78 | if smaller == 1: 79 | return bigger 80 | s = smaller >> 1 81 | half_prod = min_product_3_helper(s, bigger) 82 | if smaller % 2 == 0: 83 | return half_prod + half_prod 84 | 85 | return half_prod + half_prod + bigger 86 | 87 | 88 | # solution 4 # non-recursive 89 | def multiply_bit_based(a, b): 90 | b_bin = bin(b) 91 | b_bin = b_bin[2:] 92 | prod = 0 93 | for i in range(len(b_bin)): # O(len_b) 94 | if int(b_bin[-i - 1]): 95 | prod = prod + (a << i) 96 | 97 | return prod 98 | 99 | 100 | test_cases = [(5, 6), (28, 89), (1234, 245334)] 101 | testable_functions = [multiply_bit_based, min_product, min_product_2, min_product_3] 102 | 103 | 104 | def test_min_product(): 105 | for min_prod in testable_functions: 106 | for a, b in test_cases: 107 | assert min_prod(a, b) == a * b 108 | 109 | 110 | if __name__ == "__main__": 111 | test_min_product() 112 | -------------------------------------------------------------------------------- /chapter_08/p06_towers_of_hanoi.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | # Custom Exceptions 5 | class StackTooBigError(Exception): 6 | pass 7 | 8 | 9 | class Stack: 10 | def __init__(self, stack_size) -> None: 11 | self.stack_size = stack_size 12 | self._stack = deque() 13 | 14 | def __str__(self): 15 | return " ".join(reversed([str(val) for val in self._stack])) 16 | 17 | def push(self, val): 18 | if len(self._stack) == self.stack_size: 19 | raise StackTooBigError("stack already reached max size") 20 | self._stack.append(val) 21 | 22 | def pop(self): 23 | try: 24 | return self._stack.pop() 25 | except IndexError: 26 | raise IndexError("pop attempted from an empty stack") 27 | 28 | def top(self): 29 | try: 30 | return self._stack[-1] 31 | except IndexError: 32 | raise IndexError("top attempted from an empty stack") 33 | 34 | 35 | class MultiStack: 36 | def __init__(self, stack_size, num_stacks=3): 37 | self.stack_size = stack_size 38 | self.num_stacks = num_stacks 39 | self.multistack = [Stack(self.stack_size) for _ in range(self.num_stacks)] 40 | 41 | def get_stack(self, stack_num): 42 | if 0 > stack_num >= self.num_stacks: 43 | raise IndexError("stack_num invalid") 44 | return self.multistack[stack_num] 45 | 46 | def push(self, stack_num, val): 47 | return self.get_stack(stack_num).push(val) 48 | 49 | def top(self, stack_num): 50 | return self.get_stack(stack_num).top() 51 | 52 | def pop(self, stack_num): 53 | return self.get_stack(stack_num).pop() 54 | 55 | def __str__(self): 56 | str_result = [ 57 | f"Stack {idx}\n{stack}" for idx, stack in enumerate(self.multistack) 58 | ] 59 | return "\n".join(str_result) 60 | 61 | def __repr__(self): 62 | return str(self) 63 | 64 | 65 | class TowersOfHanoi: 66 | def __init__(self, stack_size, debug=False): 67 | self.stack_size = stack_size 68 | self.debug = debug 69 | self._stacks = MultiStack(stack_size) 70 | self.__init_first_stack() 71 | 72 | def __init_first_stack(self): 73 | for val in range(self.stack_size, 0, -1): 74 | self._stacks.push(0, val) 75 | 76 | def solve(self): 77 | if self.debug: 78 | print(f"Solving Towers of Hanoi - {self.stack_size} size") 79 | return self.__toh_solve(self.stack_size, 0, 1, 2) 80 | 81 | def __toh_solve(self, n, a, b, c): 82 | if n > 0: 83 | self.__toh_solve(n - 1, a, c, b) 84 | if self.debug: 85 | print(f"Plate {self._stacks.top(a)} -> Stack {c}") 86 | self._stacks.push(c, self._stacks.pop(a)) 87 | self.__toh_solve(n - 1, b, a, c) 88 | 89 | def __str__(self): 90 | return str(self._stacks) 91 | 92 | def get_stack(self, stack_num): 93 | return self._stacks.get_stack(stack_num)._stack 94 | 95 | 96 | if __name__ == "__main__": 97 | for test_case in range(1, 10): 98 | toh = TowersOfHanoi(test_case, debug=False) 99 | toh.solve() 100 | assert toh.get_stack(2) == deque(range(test_case, 0, -1)) 101 | assert toh.get_stack(0) == toh.get_stack(1) == deque() 102 | -------------------------------------------------------------------------------- /chapter_08/p07_permutations_without_dups.py: -------------------------------------------------------------------------------- 1 | # Write a method to compute all permutations of a string of unique characters 2 | 3 | # approach 1: building from permutations of first n-1 characters 4 | def get_perms(string): 5 | permutations = [] 6 | if string is None: 7 | return None 8 | if len(string) == 0: 9 | # base case 10 | permutations.append(" ") 11 | return permutations 12 | first = string[0] # get first letter in string 13 | remainder = string[1:] 14 | words = get_perms(remainder) 15 | for word in words: 16 | index = 0 17 | for _ in word: 18 | s = insert_char_at(word, first, index) 19 | permutations.append(s) 20 | index += 1 21 | return permutations 22 | 23 | 24 | def insert_char_at(word, char, i): 25 | start = word[:i] 26 | end = word[i:] 27 | return start + char + end 28 | 29 | 30 | # approach 2: Building from permutations of all n-1 character substrings 31 | def get_perms_2(string): 32 | result = [] 33 | get_perms_inner_2(" ", string, result) 34 | return result 35 | 36 | 37 | def get_perms_inner_2(prefix, remainder, result): 38 | if len(remainder) == 0: 39 | result.append(prefix) 40 | length = len(remainder) 41 | for i in range(length): 42 | before = remainder[:i] 43 | after = remainder[i + 1 :] 44 | c = remainder[i] 45 | get_perms_inner_2(prefix + c, before + after, result) 46 | 47 | 48 | if __name__ == "__main__": 49 | print(get_perms("str")) 50 | print(get_perms_2("str")) 51 | -------------------------------------------------------------------------------- /chapter_08/p08_permutations_with_dups.py: -------------------------------------------------------------------------------- 1 | def print_perms(string): 2 | result = [] 3 | letter_count_map = build_freq_table(string) 4 | print_perms_inner(letter_count_map, "", len(string), result) 5 | return result 6 | 7 | 8 | # returns dictionary 9 | def build_freq_table(string): 10 | letter_count_map = {} 11 | for letter in string: 12 | if letter not in letter_count_map: 13 | letter_count_map[letter] = 0 14 | letter_count_map[letter] += 1 15 | return letter_count_map 16 | 17 | 18 | def print_perms_inner(letter_count_map, prefix, remaining, result): 19 | # base case Permutation has been completed 20 | if remaining == 0: 21 | result.append(prefix) 22 | return 23 | # try remaining letter for next char, and generate remaining permutations 24 | for character in letter_count_map: 25 | count = letter_count_map[character] 26 | if count > 0: 27 | letter_count_map[character] -= 1 28 | print_perms_inner( 29 | letter_count_map, prefix + character, remaining - 1, result 30 | ) 31 | letter_count_map[character] = count 32 | 33 | 34 | if __name__ == "__main__": 35 | print(print_perms("aaf")) 36 | -------------------------------------------------------------------------------- /chapter_08/p09_parens.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def next_permutation(arr): 5 | i = len(arr) - 1 6 | while i > 0 and arr[i - 1] >= arr[i]: 7 | i -= 1 8 | if i <= 0: 9 | return False 10 | 11 | j = len(arr) - 1 12 | while arr[j] <= arr[i - 1]: 13 | j -= 1 14 | arr[i - 1], arr[j] = arr[j], arr[i - 1] 15 | 16 | arr[i:] = arr[len(arr) - 1 : i - 1 : -1] 17 | return True 18 | 19 | 20 | def is_matched_parentheses(ray): 21 | lst = [] 22 | for c in ray: 23 | if c == "(": 24 | lst.append(c) 25 | if c == ")": 26 | if len(lst) < 1 or lst.pop() != "(": 27 | return False 28 | return True 29 | 30 | 31 | def generate_parentheses_permutations_brute_force(number_of_pairs): 32 | starting_list = (["("] * number_of_pairs) + [")"] * number_of_pairs 33 | possibilities = ["".join(starting_list)] 34 | while next_permutation(starting_list): 35 | if is_matched_parentheses(starting_list): 36 | possibilities.append("".join(starting_list)) 37 | return possibilities 38 | 39 | 40 | def generate_parentheses_permutations_recursive_1(n): 41 | def helper( 42 | open_parentheses_remaining, closed_parentheses_remaining, current_string 43 | ): 44 | if len(current_string) == n * 2: 45 | result.append(current_string) 46 | if open_parentheses_remaining > 0: 47 | helper( 48 | open_parentheses_remaining - 1, 49 | closed_parentheses_remaining, 50 | current_string + "(", 51 | ) 52 | if closed_parentheses_remaining > open_parentheses_remaining: 53 | helper( 54 | open_parentheses_remaining, 55 | closed_parentheses_remaining - 1, 56 | current_string + ")", 57 | ) 58 | 59 | result = [] 60 | helper(n, n, "") 61 | return result 62 | 63 | 64 | def add_paren(arr, left_rem, right_rem, string_arr, idx): 65 | if left_rem < 0 or right_rem < left_rem: # invalid 66 | return 67 | if left_rem == 0 and right_rem == 0: # out of left and right parentheses 68 | elem = "".join(string_arr) 69 | arr.append(elem) 70 | 71 | else: 72 | string_arr[idx] = "(" # add left and recurse 73 | add_paren(arr, left_rem - 1, right_rem, string_arr, idx + 1) 74 | 75 | string_arr[idx] = ")" # add right and recurse 76 | add_paren(arr, left_rem, right_rem - 1, string_arr, idx + 1) 77 | 78 | 79 | def generate_parentheses_permutations_recursive_2(n): 80 | results = [] 81 | string_arr = ["*"] * n * 2 82 | add_paren(results, n, n, string_arr, 0) 83 | return results 84 | 85 | 86 | testable_functions = [ 87 | generate_parentheses_permutations_brute_force, 88 | generate_parentheses_permutations_recursive_1, 89 | generate_parentheses_permutations_recursive_2, 90 | ] 91 | 92 | test_cases = [ 93 | (0, [""]), 94 | (1, ["()"]), 95 | (2, sorted(["()()", "(())"])), 96 | (3, sorted(["((()))", "(()())", "(())()", "()(())", "()()()"])), 97 | ] 98 | 99 | 100 | class TestSuite(unittest.TestCase): 101 | def test_generate_parentheses_permutations(self): 102 | for f in testable_functions: 103 | for num, expected in test_cases: 104 | assert sorted(f(num)) == expected, f"{f.__name__} {num} failed" 105 | 106 | 107 | def example(): 108 | print(generate_parentheses_permutations_recursive_1(2)) 109 | print(generate_parentheses_permutations_brute_force(3)) 110 | print(generate_parentheses_permutations_recursive_2(3)) 111 | 112 | 113 | if __name__ == "__main__": 114 | example() 115 | -------------------------------------------------------------------------------- /chapter_08/p10_paint_fill.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import List 3 | 4 | 5 | def flood_fill(screen: List[List[int]], i: int, j: int, color: int, new_color: int): 6 | if ( 7 | i < 0 8 | or i >= len(screen) 9 | or j < 0 10 | or j >= len(screen[0]) 11 | or screen[i][j] != color 12 | ): 13 | return 14 | screen[i][j] = new_color 15 | flood_fill(screen, i + 1, j, color, new_color) 16 | flood_fill(screen, i - 1, j, color, new_color) 17 | flood_fill(screen, i, j + 1, color, new_color) 18 | flood_fill(screen, i, j - 1, color, new_color) 19 | 20 | 21 | def paint_fill( 22 | screen: List[List[int]], r: int, c: int, new_color: int 23 | ) -> List[List[int]]: 24 | if not screen: 25 | return None 26 | 27 | color = screen[r][c] 28 | flood_fill(screen, r, c, color, new_color) 29 | return screen 30 | 31 | 32 | class Test(unittest.TestCase): 33 | test_cases = [ 34 | ( 35 | [[1, 2, 5], [2, 2, 4], [2, 8, 6]], 36 | 1, 37 | 1, 38 | 3, 39 | [[1, 3, 5], [3, 3, 4], [3, 8, 6]], 40 | ) 41 | ] 42 | testable_functions = [paint_fill] 43 | 44 | def test_paint_fill(self): 45 | for f in self.testable_functions: 46 | for [screen, r, c, new_color, expected] in self.test_cases: 47 | assert f(screen, r, c, new_color) == expected 48 | 49 | 50 | if __name__ == "__main__": 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /chapter_08/p11_coins.py: -------------------------------------------------------------------------------- 1 | STANDARD_COIN_SIZES = [1, 5, 10, 25] 2 | 3 | 4 | def coin_combinations(amount, coin_sizes=None): 5 | if amount == 0: 6 | return 1 7 | if amount < 0: 8 | return 0 9 | if coin_sizes is None: 10 | coin_sizes = STANDARD_COIN_SIZES 11 | 12 | if len(coin_sizes) == 0: 13 | return 0 14 | m = len(coin_sizes) 15 | return coin_combinations(amount, coin_sizes[: m - 1]) + coin_combinations( 16 | amount - coin_sizes[m - 1], coin_sizes 17 | ) 18 | 19 | 20 | def coin_combinations_iterative(amount, coin_sizes=None): 21 | table = [0] * (amount + 1) 22 | # first case 0 value 23 | table[0] = 1 24 | 25 | if coin_sizes is None: 26 | coin_sizes = STANDARD_COIN_SIZES 27 | for i in range(0, len(coin_sizes)): 28 | for j in range(coin_sizes[i], amount + 1): 29 | table[j] += table[j - coin_sizes[i]] 30 | return table[amount] 31 | 32 | 33 | if __name__ == "__main__": 34 | print(coin_combinations(100)) 35 | print(coin_combinations_iterative(100)) 36 | -------------------------------------------------------------------------------- /chapter_08/p13_tallest_stack.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from functools import reduce 3 | 4 | 5 | def tallest_stack(boxes): 6 | boxes.sort(reverse=True) 7 | 8 | def tallest_for_bottom(cur_stack, cur_box_idx): 9 | if cur_box_idx == len(boxes): 10 | return reduce(lambda x, y: x + y.height, cur_stack, 0) 11 | 12 | if ( 13 | cur_stack[-1].height > boxes[cur_box_idx].height 14 | and cur_stack[-1].width > boxes[cur_box_idx].width 15 | and cur_stack[-1].depth > boxes[cur_box_idx].depth 16 | ): 17 | return tallest_for_bottom(cur_stack + [boxes[cur_box_idx]], cur_box_idx + 1) 18 | 19 | return tallest_for_bottom(cur_stack, cur_box_idx + 1) 20 | 21 | largest_height = 0 22 | for i, box in enumerate(boxes): 23 | largest_height = max(largest_height, tallest_for_bottom([box], i + 1)) 24 | return largest_height 25 | 26 | 27 | class Box: 28 | def __init__(self, height, width, depth): 29 | self.height = height 30 | self.width = width 31 | self.depth = depth 32 | 33 | def __lt__(self, other): 34 | return self.height < other.height 35 | 36 | def __eq__(self, other): 37 | return self.height == other.height 38 | 39 | 40 | def test_null(): 41 | assert tallest_stack([]) == 0 42 | 43 | 44 | def test_single_box(): 45 | assert tallest_stack([Box(3, 2, 1)]) == 3 46 | 47 | 48 | def test_two_conflicting_boxes(): 49 | assert tallest_stack([Box(3, 2, 1), Box(5, 4, 1)]) == 5 50 | 51 | 52 | def test_two_stackable_boxes(): 53 | assert tallest_stack([Box(3, 2, 1), Box(6, 5, 4)]) == 9 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /chapter_08/p14_boolean_evaluation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def string_to_bool(s: str) -> bool: 5 | return s == "1" 6 | 7 | 8 | def count_ways(exp: str, result: bool, memo) -> int: 9 | if len(exp) == 0: 10 | return 0 11 | if len(exp) == 1: 12 | return 1 if string_to_bool(exp) == result else 0 13 | 14 | if exp + str(result) in memo: 15 | return memo[exp + str(result)] 16 | 17 | ways = 0 18 | for i in range(1, len(exp), 2): 19 | left = exp[:i] 20 | right = exp[i + 1 :] 21 | 22 | left_true = count_ways(left, True, memo) 23 | left_false = count_ways(left, False, memo) 24 | right_true = count_ways(right, True, memo) 25 | right_false = count_ways(right, False, memo) 26 | 27 | total = (left_true + left_false) * (right_true + right_false) 28 | 29 | total_true = 0 30 | if exp[i] == "|": 31 | total_true = ( 32 | left_true * right_true 33 | + left_false * right_true 34 | + left_true * right_false 35 | ) 36 | elif exp[i] == "&": 37 | total_true = left_true * right_true 38 | elif exp[i] == "^": 39 | total_true = left_true * right_false + left_false * right_true 40 | 41 | subways = total_true if result else (total - total_true) 42 | ways += subways 43 | 44 | memo[exp + str(result)] = ways 45 | return ways 46 | 47 | 48 | def evaluate(exp: str, result: bool) -> int: 49 | memo = {} 50 | return count_ways(exp, result, memo) 51 | 52 | 53 | class Test(unittest.TestCase): 54 | test_cases = [("1^0|0|1", False, 2), ("0&0&0&1^1|0", True, 10)] 55 | testable_functions = [evaluate] 56 | 57 | def test_evaluate(self): 58 | for f in self.testable_functions: 59 | for [expression, result, expected] in self.test_cases: 60 | assert f(expression, result) == expected 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /chapter_10/p01_sorted_merge.py: -------------------------------------------------------------------------------- 1 | def sorted_merge(a, b): 2 | index = len(a) - 1 3 | index_b = len(b) - 1 4 | index_a = len(a) - len(b) - 1 5 | 6 | while index_b >= 0: 7 | if index_a >= 0 and a[index_a] > b[index_b]: 8 | a[index] = a[index_a] 9 | index_a -= 1 10 | else: 11 | a[index] = b[index_b] 12 | index_b -= 1 13 | if index > 0: 14 | index -= 1 15 | return a 16 | 17 | 18 | def fill_array_up_to(maxnum): 19 | return [2 * i + 4 for i in range(maxnum)] 20 | 21 | 22 | def fill_array_with_buffer(length, buffer): 23 | return [3 * i + 1 for i in range(length)] + ([0] * buffer) 24 | 25 | 26 | def test_sorted_merge(): 27 | a = [9, 10, 11, 12, 13, 0, 0, 0, 0] 28 | b = [4, 5, 6, 7] 29 | expected = [4, 5, 6, 7, 9, 10, 11, 12, 13] 30 | assert sorted_merge(a, b) == expected 31 | 32 | 33 | def example(): 34 | a = fill_array_with_buffer(5, 10) 35 | b = fill_array_up_to(10) 36 | print(a, b) 37 | print(sorted_merge(a, b)) 38 | 39 | 40 | if __name__ == "__main__": 41 | example() 42 | -------------------------------------------------------------------------------- /chapter_10/p02_group_anagrams.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | def group_anagrams(words): 5 | anagrams = defaultdict(list) 6 | for word in words: 7 | sorted_word = "".join(sorted(word.lower())) 8 | anagrams[sorted_word].append(word) 9 | 10 | sorted_words = [] 11 | for similar_words in anagrams.values(): 12 | sorted_words.extend(similar_words) 13 | return sorted_words 14 | 15 | 16 | def test_group_anagrams(): 17 | words = ["abed", "later", "bead", "alert", "altered", "bade", "alter", "alerted"] 18 | expected_sort = [ 19 | "abed", 20 | "bead", 21 | "bade", 22 | "later", 23 | "alert", 24 | "alter", 25 | "altered", 26 | "alerted", 27 | ] 28 | assert group_anagrams(words) == expected_sort 29 | 30 | 31 | def example(): 32 | words = ["abed", "later", "bead", "alert", "altered", "bade", "alter", "alerted"] 33 | print(group_anagrams(words)) 34 | 35 | 36 | if __name__ == "__main__": 37 | example() 38 | -------------------------------------------------------------------------------- /chapter_10/p03_search_in_rotated_array.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | 3 | 4 | def index(nums: Sequence[int], target: int) -> Optional[int]: 5 | if not nums: 6 | return None 7 | # We cannot guarantee better than O(n) if there are duplicates. 8 | if nums[0] == nums[-1]: 9 | try: 10 | return nums.index(target) 11 | except ValueError: 12 | return None 13 | 14 | is_target_left_of_wraparound = target >= nums[0] 15 | lo, hi = 0, len(nums) - 1 16 | while lo <= hi: 17 | mid = (lo + hi) // 2 18 | is_mid_left_of_wraparound = nums[mid] >= nums[0] 19 | if is_mid_left_of_wraparound and not is_target_left_of_wraparound: 20 | lo = mid + 1 21 | elif not is_mid_left_of_wraparound and is_target_left_of_wraparound: 22 | hi = mid - 1 23 | elif nums[mid] < target: 24 | lo = mid + 1 25 | elif nums[mid] > target: 26 | hi = mid - 1 27 | else: 28 | assert nums[mid] == target 29 | return mid 30 | return None 31 | 32 | 33 | def search_rotated(array: Sequence[int], num: int) -> Optional[int]: 34 | if not array: 35 | return None 36 | return _recursive_search(array, num, 0, len(array) - 1) 37 | 38 | 39 | def _recursive_search(array, num, start, end): 40 | middle = (end - start) // 2 + start 41 | if array[middle] == num: 42 | return middle 43 | if end - start <= 0: 44 | return None 45 | 46 | result = None 47 | if array[start] < array[middle]: # left side is normal 48 | if array[start] <= num < array[middle]: 49 | result = _recursive_search(array, num, start, middle - 1) 50 | else: 51 | result = _recursive_search(array, num, middle + 1, end) 52 | elif array[middle] < array[end]: # right side is normal 53 | if array[middle] < num <= array[end]: 54 | result = _recursive_search(array, num, middle + 1, end) 55 | else: 56 | result = _recursive_search(array, num, start, middle - 1) 57 | elif array[start] == array[middle]: 58 | if array[middle] != array[end]: 59 | result = _recursive_search(array, num, middle + 1, end) 60 | else: 61 | result = _recursive_search(array, num, start, middle - 1) 62 | if result is None: 63 | result = _recursive_search(array, num, middle + 1, end) 64 | return result 65 | 66 | 67 | test_cases = [ 68 | # array, target, valid solutions 69 | ([15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], 5, 8), 70 | ([2, 3, 1, 2, 2, 2, 2, 2, 2, 2], 2, {0, 3, 4, 5, 6, 7, 8, 9}), 71 | ([2, 3, 1, 2, 2, 2, 2, 2, 2, 2], 3, 1), 72 | ([2, 3, 1, 2, 2, 2, 2, 2, 2, 2], 4, None), 73 | ([2, 3, 1, 2, 2, 2, 2, 2, 2, 2], 1, 2), 74 | ([2, 3, 1, 2, 2, 2, 2, 2, 2, 2], 8, None), 75 | ] 76 | 77 | testable_functions = [index, search_rotated] 78 | 79 | 80 | def test_index(): 81 | for array, target, expected in test_cases: 82 | for method in testable_functions: 83 | ind = method(array, target) 84 | if isinstance(expected, set): 85 | assert ind in expected 86 | else: 87 | error_msg = ( 88 | f"arr:{array} target:{target} calculated:{ind} expected:{expected}" 89 | ) 90 | assert ind == expected, error_msg 91 | 92 | 93 | if __name__ == "__main__": 94 | test_index() 95 | -------------------------------------------------------------------------------- /chapter_10/p04_search_sorted_no_size_array.py: -------------------------------------------------------------------------------- 1 | def sorted_nosize_search(listy, num): 2 | # Adapted to work with a Python sorted 3 | # array and handle the index error exception 4 | exp_backoff = index = 0 5 | limit = False 6 | while not limit: 7 | try: 8 | temp = listy[index] 9 | if temp > num: 10 | limit = True 11 | else: 12 | index = 2**exp_backoff 13 | exp_backoff += 1 14 | except IndexError: 15 | limit = True 16 | return bi_search(listy, num, index // 2, index) 17 | 18 | 19 | def bi_search(listy, num, low, high): 20 | while low <= high: 21 | middle = (high + low) // 2 22 | 23 | try: 24 | value_at = listy[middle] 25 | except IndexError: 26 | value_at = -1 27 | 28 | if num < value_at or value_at == -1: 29 | high = middle - 1 30 | elif num > value_at: 31 | low = middle + 1 32 | else: 33 | return middle 34 | return -1 35 | 36 | 37 | test_cases = [ 38 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 0), -1), 39 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 1), 0), 40 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 2), 1), 41 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 3), 2), 42 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 4), 3), 43 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 5), 4), 44 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 6), 5), 45 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 7), 6), 46 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 8), 7), 47 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 9), 8), 48 | (([1, 2, 3, 4, 5, 6, 7, 8, 9], 10), -1), 49 | ] 50 | 51 | testable_functions = [sorted_nosize_search] 52 | 53 | 54 | def test_sorted_search(): 55 | for function in testable_functions: 56 | for (n, m), expected in test_cases: 57 | calculated = function(n, m) 58 | error_msg = f"{function.__name__}: {calculated} != {expected}" 59 | assert function(n, m) == expected, error_msg 60 | 61 | 62 | if __name__ == "__main__": 63 | test_sorted_search() 64 | -------------------------------------------------------------------------------- /chapter_10/p05_sparse_search.py: -------------------------------------------------------------------------------- 1 | def sparse_search(arr, item): 2 | def inner_search(arr, item, low, high): 3 | middle = ((high - low) // 2) + low 4 | 5 | if arr[middle] == "": 6 | left = middle - 1 7 | right = middle + 1 8 | while True: 9 | if left < low and right > high: 10 | return None 11 | elif right <= high and arr[right] != "": 12 | middle = right 13 | break 14 | elif left >= low and arr[left] != "": 15 | middle = left 16 | break 17 | left -= 1 18 | right += 1 19 | 20 | if arr[middle] == item: 21 | return middle 22 | if arr[middle] > item: 23 | return inner_search(arr, item, low, middle - 1) 24 | if arr[middle] < item: 25 | return inner_search(arr, item, middle + 1, high) 26 | 27 | return inner_search(arr, item, 0, len(arr) - 1) 28 | 29 | 30 | test_cases = [ 31 | ((["a", "", "", "b", "", "c", "", "", "d", "", "", "", "", "e", ""], "d"), 8), 32 | ((["a", "", "", "b", "", "c", "", "", "d", "", "", "", "", "e", ""], "f"), None), 33 | ((["a", "", "", "b", "", "c", "", "", "d", "", "", "", "", "e", ""], "a"), 0), 34 | ] 35 | 36 | testable_functions = [sparse_search] 37 | 38 | 39 | def test_sorted_search(): 40 | for function in testable_functions: 41 | for (n, m), expected in test_cases: 42 | calculated = function(n, m) 43 | error_msg = f"{function.__name__}: {calculated} != {expected}" 44 | assert function(n, m) == expected, error_msg 45 | 46 | 47 | if __name__ == "__main__": 48 | test_sorted_search() 49 | -------------------------------------------------------------------------------- /chapter_16/p01_number_swapper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | def swap_numbers_diff(a, b): 5 | a = b - a 6 | b = b - a # = b-(b-a) = b-b+a = a 7 | a = a + b # = (b-a)+a = b-a+a = b 8 | return a, b 9 | 10 | 11 | def swap_numbers_xor(a, b): 12 | a = a ^ b 13 | b = a ^ b # = (a^b)^b = a^(b^b) = a^0 = a 14 | a = a ^ b # = (a^b)^(a) = a^b^a = b^(a^a) = b^0 = b 15 | return a, b 16 | 17 | 18 | testable_functions = [swap_numbers_diff, swap_numbers_xor] 19 | 20 | test_cases = [[1, 2], [10, 3], [4, 4], [1, 0], [7, -4], [-7, -4]] 21 | 22 | 23 | class Test(unittest.TestCase): 24 | def test_small_big(self): 25 | for f in testable_functions: 26 | for a, b in test_cases: 27 | assert (b, a) == f(a, b) 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /chapter_16/p02_word_frequencies.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | 4 | def preprocess(book): 5 | word_counts = {} 6 | for word in book.split(): 7 | word = word.lower() 8 | word = word.translate(str.maketrans("", "", string.punctuation)) 9 | if not word: 10 | continue 11 | 12 | if word not in word_counts: 13 | word_counts[word] = 1 14 | else: 15 | word_counts[word] += 1 16 | return word_counts 17 | 18 | 19 | def get_frequency_repetitive(book, word): 20 | word_counts = preprocess(book) 21 | return word_counts.get(word.lower(), 0) 22 | 23 | 24 | def get_frequency_single_query(book, word): 25 | if book is None or word is None: 26 | return 0 27 | word = word.lower() 28 | count = 0 29 | for book_word in book.split(): 30 | # make lowercase, remove punctuation 31 | book_word = book_word.lower() 32 | book_word = book_word.translate(str.maketrans("", "", string.punctuation)) 33 | if book_word == word: 34 | count += 1 35 | return count 36 | 37 | 38 | def example(): 39 | book = """Once upon a time there was this book. 40 | This is a sentence. This is a much longer sentence. 41 | This book is terribly short. But you get the idea. 42 | You should see the word this 6 times in this example text. 43 | """ 44 | word = "book" 45 | 46 | count = get_frequency_repetitive(book, word) 47 | print(f'The word "{word}" appears {count} times.') 48 | 49 | count = get_frequency_single_query(book, word) 50 | print(f'The word "{word}" appears {count} times.') 51 | 52 | 53 | if __name__ == "__main__": 54 | pass 55 | -------------------------------------------------------------------------------- /chapter_16/p06_smallest_difference.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | # Function to sort the arrays in O(nlog(n)) 5 | def merge_sort(arr): 6 | # function to create the partitions 7 | def make_partition(_arr): 8 | if len(_arr) <= 1: 9 | return _arr 10 | middle = len(_arr) // 2 11 | left = make_partition(_arr[:middle]) 12 | right = make_partition(_arr[middle:]) 13 | 14 | return merge_partition(left, right) 15 | 16 | # function to merge the array partitions 17 | def merge_partition(left, right): 18 | result = [] 19 | i, j = 0, 0 20 | while i < len(left) and j < len(right): 21 | if left[i] <= right[j]: 22 | result.append(left[i]) 23 | i += 1 24 | else: 25 | result.append(right[j]) 26 | j += 1 27 | 28 | result += left[i:] 29 | result += right[j:] 30 | return result 31 | 32 | return make_partition(arr) 33 | 34 | 35 | # Function to find the smallest difference 36 | def find_smallest_difference(array1, array2): 37 | array1 = merge_sort(array1) 38 | array2 = merge_sort(array2) 39 | a, b, difference = 0, 0, sys.maxsize 40 | pair = [] 41 | 42 | while a < len(array1) and b < len(array2): 43 | if abs(array1[a] - array2[b]) < difference: 44 | difference = abs(array1[a] - array2[b]) 45 | pair = [array1[a], array2[b]] 46 | if difference == 0: 47 | break 48 | if array1[a] < array2[b]: 49 | a += 1 50 | else: 51 | b += 1 52 | 53 | return difference, pair 54 | 55 | 56 | def test_find_smallest_diff(): 57 | test_cases = [ 58 | [[1, 3, 15, 11, 2], [23, 127, 235, 19, 8], (3, [11, 8])], 59 | [[2, 11, 15, 1], [12, 4, 235, 19, 127, 23], (1, [11, 12])], 60 | [[32, 1, 5, -31], [98, 7, 32, 43, 72, 86], (0, [32, 32])], 61 | ] 62 | for a, b, expected in test_cases: 63 | assert find_smallest_difference(a, b) == expected 64 | -------------------------------------------------------------------------------- /chapter_16/p08_english_int.py: -------------------------------------------------------------------------------- 1 | ones = [ 2 | "One", 3 | "Two", 4 | "Three", 5 | "Four", 6 | "Five", 7 | "Six", 8 | "Seven", 9 | "Eight", 10 | "Nine", 11 | "Ten", 12 | "Eleven", 13 | "Tweleve", 14 | "Thirteen", 15 | "Fourteen", 16 | "Fifteen", 17 | "Sixteen", 18 | "Seventeen", 19 | "Eigteen", 20 | "Nineteen", 21 | ] 22 | 23 | twos = { 24 | 20: "Twenty", 25 | 30: "Thirty", 26 | 40: "Forty", 27 | 50: "Fifty", 28 | 60: "Sixty", 29 | 70: "Seventy", 30 | 80: "Eighty", 31 | 90: "Ninety", 32 | } 33 | 34 | threes = ["", "Thousand", "Million", "Billion"] 35 | 36 | 37 | def get_chunks(n): 38 | result = [] 39 | 40 | if 1 <= (n % 100) < 20: 41 | result.append(ones[(n % 100) - 1]) 42 | 43 | elif 20 <= (n % 100) < 100: 44 | if (n % 10) != 0: 45 | result.append(ones[n % 10 - 1]) 46 | result.insert(0, twos.get((n % 100 - n % 10), "")) 47 | 48 | if n >= 100: 49 | result = [ones[n // 100 - 1], "Hundred"] + result 50 | 51 | return result 52 | 53 | 54 | def get_in_words(n): 55 | if n == 0: 56 | return "Zero" 57 | 58 | int_in_words = [] 59 | index = 0 60 | 61 | while n > 0: 62 | temp = n % 1000 63 | res = get_chunks(temp) 64 | if res: 65 | int_in_words = res + [threes[index]] + int_in_words 66 | index += 1 67 | n //= 1000 68 | 69 | return " ".join(int_in_words) 70 | 71 | 72 | def example(): 73 | nums = [ 74 | 1, 75 | 10, 76 | 13, 77 | 19, 78 | 20, 79 | 23, 80 | 50, 81 | 73, 82 | 93, 83 | 100, 84 | 101, 85 | 110, 86 | 119, 87 | 195, 88 | 300, 89 | 504, 90 | 950, 91 | 974, 92 | 999, 93 | 1000, 94 | 10000, 95 | 909000, 96 | 1000000, 97 | 9000009, 98 | 19323984, 99 | 908900034, 100 | 100000000781, 101 | ] 102 | 103 | for n in nums: 104 | print(n, get_in_words(n)) 105 | 106 | 107 | if __name__ == "__main__": 108 | example() 109 | -------------------------------------------------------------------------------- /chapter_16/p19_pond_sizes.py: -------------------------------------------------------------------------------- 1 | # helper function to compute a size of a pond recursively 2 | def pond_region(grid, x, y): 3 | if x < 0 or y < 0 or x >= len(grid) or y >= len(grid[0]): 4 | return 0 5 | 6 | if grid[x][y] != 0: 7 | return 0 8 | 9 | grid[x][y] = -1 10 | size = 1 11 | for row in range(x - 1, x + 2): 12 | for col in range(y - 1, y + 2): 13 | if x != row or y != col: 14 | size += pond_region(grid, row, col) 15 | 16 | return size 17 | 18 | 19 | # fuction to find all the pond sizes 20 | def find_ponds(grid): 21 | result = [] 22 | 23 | for i in range(len(grid)): # noqa 24 | for j in range(len(grid[0])): 25 | if grid[i][j] == 0: 26 | result.append(pond_region(grid, i, j)) 27 | 28 | return result 29 | 30 | 31 | def example(): 32 | grid = [[0, 2, 1, 0], [0, 1, 0, 1], [1, 1, 0, 1], [0, 1, 0, 1]] 33 | pond_sizes = find_ponds(grid) 34 | print(", ".join(map(str, pond_sizes))) 35 | 36 | 37 | if __name__ == "__main__": 38 | example() 39 | -------------------------------------------------------------------------------- /chapter_16/p26_calculator.py: -------------------------------------------------------------------------------- 1 | class Stack: 2 | def __init__(self): 3 | self.stack = [] 4 | 5 | def push(self, value): 6 | self.stack.append(value) 7 | 8 | def pop(self): 9 | if not self.stack: 10 | raise ValueError("Stack is empty") 11 | 12 | return self.stack.pop() 13 | 14 | def is_empty(self): 15 | return self.stack == [] 16 | 17 | def size(self): 18 | return len(self.stack) 19 | 20 | def peek(self): 21 | return self.stack[-1] 22 | 23 | 24 | def parse_equation(equation_str): 25 | terms = [] 26 | operations = {"+", "/", "-", "*"} 27 | index = 0 28 | while index < len(equation_str): 29 | if equation_str[index] == " ": 30 | index += 1 31 | 32 | elif equation_str[index] not in operations: 33 | i = index + 1 34 | t = equation_str[index:i] 35 | while i < len(equation_str) and equation_str[i] not in operations: 36 | if equation_str[i] != " ": 37 | t += equation_str[i] 38 | i += 1 39 | terms.append(t) 40 | index = i 41 | else: 42 | terms.append(equation_str[index]) 43 | index += 1 44 | 45 | return terms 46 | 47 | 48 | def collapse(a, b, operation): 49 | return eval(str(b) + operation + str(a)) # noqa 50 | 51 | 52 | def calculate(s): 53 | terms = parse_equation(s) 54 | op_stack = Stack() 55 | num_stack = Stack() 56 | priority = {"+": 1, "-": 1, "*": 2, "/": 2, "%": 2} 57 | for x in terms: 58 | if x in priority.keys(): 59 | if op_stack.is_empty() or priority[x] > priority[op_stack.peek()]: 60 | op_stack.push(x) 61 | else: 62 | while ( 63 | not op_stack.is_empty() and priority[x] <= priority[op_stack.peek()] 64 | ): 65 | res = collapse(num_stack.pop(), num_stack.pop(), op_stack.pop()) 66 | num_stack.push(res) 67 | op_stack.push(x) 68 | 69 | else: 70 | num_stack.push(int(x)) 71 | 72 | while not op_stack.is_empty(): 73 | res = collapse(num_stack.pop(), num_stack.pop(), op_stack.pop()) 74 | num_stack.push(res) 75 | 76 | return num_stack.pop() 77 | 78 | 79 | def example(): 80 | s = "2 -6 - 7 * 8 / 2 + 5" 81 | s1 = "2*3+5/6*3+15" 82 | 83 | print(s, "=>", calculate(s)) 84 | print(s1, "=>", calculate(s1)) 85 | 86 | 87 | if __name__ == "__main__": 88 | example() 89 | -------------------------------------------------------------------------------- /chapter_17/p01_add_without_plus.py: -------------------------------------------------------------------------------- 1 | def add_without_plus(a, b): 2 | while b != 0: 3 | # Sum without carry bit 4 | _sum = a ^ b 5 | 6 | # Sum with only carrying 7 | carry = (a & b) << 1 8 | 9 | a = _sum 10 | b = carry 11 | 12 | return a 13 | 14 | 15 | def add_without_plus_recursive(addend_a, addend_b): 16 | if addend_b == 0: 17 | return addend_a 18 | 19 | _sum = addend_a ^ addend_b 20 | carry = (addend_a & addend_b) << 1 21 | 22 | return add_without_plus_recursive(_sum, carry) 23 | 24 | 25 | testable_functions = [add_without_plus_recursive, add_without_plus] 26 | 27 | test_cases = [ 28 | # a, b, expected 29 | (1, 1, 2), 30 | (1, 2, 3), 31 | (1001, 234, 1235), 32 | (123456789, 123456789, 123456789 * 2), 33 | # does not work with negatives :( 34 | ] 35 | 36 | 37 | def test_add_without_plus(): 38 | for f in testable_functions: 39 | for a, b, expected in test_cases: 40 | assert f(a, b) == expected 41 | -------------------------------------------------------------------------------- /chapter_17/p02_shuffle.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | def random_number_generator(lower, higher): 5 | return lower + int(random.random() * (higher - lower + 1)) 6 | 7 | 8 | def shuffle_list_recursively(cards, current_index): 9 | if current_index == 0: 10 | return cards 11 | 12 | shuffle_list_recursively(cards, current_index - 1) 13 | random_index = random_number_generator(0, current_index) 14 | 15 | temp = cards[random_index] 16 | cards[random_index] = cards[current_index] 17 | cards[current_index] = temp 18 | 19 | return cards 20 | 21 | 22 | def shuffle_list_iteratively(cards): 23 | for i in range(len(cards) - 1): 24 | random_index = random_number_generator(0, i) 25 | temp = cards[random_index] 26 | cards[random_index] = cards[i] 27 | cards[i] = temp 28 | 29 | return cards 30 | 31 | 32 | def test_shuffle_list_recursively(): 33 | cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 34 | result = shuffle_list_recursively(cards, len(cards) - 1) 35 | assert len(result) == len(cards) 36 | assert sum(result) == sum(range(0, len(cards))) 37 | 38 | 39 | def test_shuffle_list_iteratively(): 40 | cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 41 | result = shuffle_list_iteratively(cards) 42 | assert len(result) == len(cards) 43 | assert sum(result) == sum(range(0, len(cards))) 44 | -------------------------------------------------------------------------------- /chapter_17/p07_baby_names.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | 4 | def count_baby_names(name_counts, synonyms): 5 | parent = {} 6 | for name in name_counts.keys(): 7 | parent[name] = name 8 | 9 | def find(x): 10 | if parent[x] != x: 11 | parent[x] = find(parent[x]) 12 | return parent[x] 13 | 14 | def union(x1, x2): 15 | r1 = find(x1) 16 | r2 = find(x2) 17 | if r1 != r2: 18 | parent[r1] = r2 19 | 20 | res = collections.defaultdict(int) 21 | for pair in synonyms: 22 | union(pair[0], pair[1]) 23 | 24 | for key in parent.keys(): 25 | # find root of cluster 26 | root = find(key) 27 | res[root] += name_counts[key] 28 | return dict(res) 29 | 30 | 31 | test_cases = [ 32 | # name_counts, synonyms, expected_counts 33 | [ 34 | { 35 | "john": 10, 36 | "jon": 3, 37 | "davis": 2, 38 | "kari": 3, 39 | "johny": 11, 40 | "carlton": 8, 41 | "carleton": 2, 42 | "jonathan": 9, 43 | "carrie": 5, 44 | }, 45 | [ 46 | ("jonathan", "john"), 47 | ("jon", "johny"), 48 | ("johny", "john"), 49 | ("kari", "carrie"), 50 | ("carleton", "carlton"), 51 | ], 52 | {"john": 33, "davis": 2, "carrie": 8, "carlton": 10}, 53 | ] 54 | ] 55 | 56 | 57 | def test_baby_names(): 58 | for name_counts, synonyms, expected_counts in test_cases: 59 | assert count_baby_names(name_counts, synonyms) == expected_counts 60 | 61 | 62 | if __name__ == "__main__": 63 | test_baby_names() 64 | -------------------------------------------------------------------------------- /chapter_17/p08_circus_tower.py: -------------------------------------------------------------------------------- 1 | def find_max_people(height_weight_pairs): 2 | if not height_weight_pairs: 3 | return 0 4 | # sort on second dimension in decreasing order 5 | height_weight_pairs.sort(key=lambda x: -x[1]) 6 | # then, problem is lis in first dimension 7 | dp = [float("inf") for _ in range(len(height_weight_pairs))] 8 | dp[0] = 1 9 | max_so_far = 1 10 | for i in range(1, len(height_weight_pairs)): 11 | # look at the elements in the range [0,i-1] (does not include i) 12 | choices = [1] 13 | for j in range(0, i): 14 | if height_weight_pairs[i][0] < height_weight_pairs[j][0]: 15 | # then feasible candidate 16 | choices.append(1 + dp[j]) 17 | dp[i] = max(choices) 18 | max_so_far = max(max_so_far, dp[i]) 19 | return max_so_far 20 | 21 | 22 | test_cases = [ 23 | # height_weight_pairs, expected_length 24 | ([], 0), 25 | ([(65, 100), (100, 65)], 1), 26 | ([(65, 100), (65, 100)], 1), 27 | ([(65, 100), (65, 101)], 1), 28 | ([(65, 100), (55, 40), (75, 90), (80, 120)], 3), 29 | ([(65, 100), (70, 150), (56, 90), (75, 190), (60, 95), (68, 110)], 6), 30 | ] 31 | 32 | 33 | def test_circus_tower(): 34 | for height_weight_pairs, expected_length in test_cases: 35 | assert find_max_people(height_weight_pairs) == expected_length 36 | -------------------------------------------------------------------------------- /chapter_17/p09_kth_multiple.py: -------------------------------------------------------------------------------- 1 | # o(k^2) algo 2 | def get_kth_multiple(k): 3 | res = [1, 3, 5, 7] 4 | is_number_seen = {1, 3, 5} 5 | if k <= 3: 6 | return res[k] 7 | 8 | for i in range(k - 3): 9 | choices = [] 10 | for j in range(len(res)): 11 | if 3 * res[j] not in is_number_seen: 12 | choices.append(3 * res[j]) 13 | if 5 * res[j] not in is_number_seen: 14 | choices.append(5 * res[j]) 15 | if 7 * res[j] not in is_number_seen: 16 | choices.append(7 * res[j]) 17 | ans = min(choices) 18 | res.append(ans) 19 | 20 | is_number_seen.add(ans) 21 | 22 | return res[-1] 23 | 24 | 25 | def get_kth_multiple_via_heap(k): 26 | res = [] 27 | is_number_seen = set() 28 | import heapq 29 | 30 | heap = [3, 5, 7] 31 | heapq.heapify(heap) 32 | for i in range(k): 33 | next_el = heapq.heappop(heap) 34 | # is_number_seen.add(next_el) 35 | res.append(next_el) 36 | if (next_el * 3) not in is_number_seen: 37 | is_number_seen.add(next_el * 3) 38 | heapq.heappush(heap, next_el * 3) 39 | if (next_el * 5) not in is_number_seen: 40 | is_number_seen.add(next_el * 5) 41 | heapq.heappush(heap, next_el * 5) 42 | if (next_el * 7) not in is_number_seen: 43 | is_number_seen.add(next_el * 7) 44 | heapq.heappush(heap, next_el * 7) 45 | print(res) 46 | return res[-1] 47 | 48 | 49 | test_cases = [ 50 | # k, expected 51 | (1, 3), 52 | (2, 5), 53 | (3, 7), 54 | (1000, 82046671875), 55 | ] 56 | 57 | testable_functions = [get_kth_multiple_via_heap] 58 | 59 | 60 | def test_kth_multiple(): 61 | for f in testable_functions: 62 | for k, expected in test_cases: 63 | assert f(k) == expected 64 | 65 | 66 | if __name__ == "__main__": 67 | test_kth_multiple() 68 | -------------------------------------------------------------------------------- /chapter_17/p15_longest_word.py: -------------------------------------------------------------------------------- 1 | def longest_composite_word(word_list): 2 | mapping = {} 3 | 4 | for word in word_list: 5 | mapping[word] = True 6 | 7 | for original_word in sorted(word_list, key=len, reverse=True): 8 | if contains_subwords(original_word, True, mapping): 9 | return original_word 10 | 11 | return None 12 | 13 | 14 | def contains_subwords(word, is_original_word, mapping): 15 | if (word in mapping) and (not is_original_word): 16 | return mapping[word] 17 | 18 | for i in range(1, len(word)): 19 | left = word[0:i] 20 | right = word[i:] 21 | 22 | if ( 23 | (left in mapping) 24 | and (mapping[left] is True) 25 | and (contains_subwords(right, False, mapping)) 26 | ): 27 | return True 28 | 29 | mapping[word] = False 30 | return False 31 | 32 | 33 | def test_lcw(): 34 | word_list = ["cat", "banana", "dog", "nana", "walk", "walker", "dogwalker"] 35 | result = longest_composite_word(word_list) 36 | assert result == "dogwalker" 37 | 38 | 39 | def test_lcw_returns_none_for_no_match(): 40 | word_list = ["cat", "banana", "dog"] 41 | result = longest_composite_word(word_list) 42 | assert result is None 43 | 44 | 45 | def test_lcw_checks_alphabetically(): 46 | word_list = [ 47 | "cat", 48 | "banana", 49 | "dog", 50 | "nana", 51 | "catbanana", 52 | "walk", 53 | "walker", 54 | "dogwalker", 55 | ] 56 | result = longest_composite_word(word_list) 57 | assert result == "catbanana" 58 | -------------------------------------------------------------------------------- /chapter_17/p16_the_masseuse.py: -------------------------------------------------------------------------------- 1 | # o(n) time 2 | # o(n) space 3 | 4 | 5 | def find_best_schedule(appointments): 6 | n = len(appointments) 7 | dp = [0] * (n + 1) 8 | dp[-2] = appointments[-1] 9 | 10 | max_so_far = -float("inf") 11 | 12 | for i in reversed(range(n - 1)): 13 | choices = [] 14 | # choice 1, take the ith element, then skip i+1, and take i+2. 15 | choices.append((appointments[i] + dp[i + 2], i + 2)) 16 | # choice 2, don't take ith element, the answer sits at dp[i+1] 17 | choices.append((dp[i + 1], i + 1)) 18 | dp[i] = max(choices)[0] 19 | if dp[i] > max_so_far: 20 | max_so_far = dp[i] 21 | 22 | return max_so_far 23 | 24 | 25 | def test_find_best_schedule(): 26 | appointments = [30, 15, 60, 75, 45, 15, 15, 45] 27 | assert find_best_schedule(appointments) == 180 28 | appointments = [30, 15, 60, 15, 45, 15, 45] 29 | assert find_best_schedule(appointments) == 180 30 | appointments = [30, 15, 15, 60] 31 | assert find_best_schedule(appointments) == 90 32 | -------------------------------------------------------------------------------- /chapter_17/p17_multi_search.py: -------------------------------------------------------------------------------- 1 | # trie solution 2 | # third optimal solution in ctci 3 | # o(kt+bk) 4 | import collections 5 | 6 | 7 | class TrieNode: 8 | def __init__(self): 9 | self.children = collections.defaultdict(TrieNode) 10 | self.is_word = False 11 | 12 | 13 | class Trie: 14 | def __init__(self): 15 | self.root = TrieNode() 16 | 17 | def insert(self, word): 18 | node = self.root 19 | for letter in word: 20 | node = node.children[letter] 21 | node.is_word = True 22 | 23 | def check_existence(self, word): 24 | # checks if the prefixes exist on a path, 25 | # if they do, then return the location in the word 26 | res = [] 27 | node = self.root 28 | for i, letter in enumerate(word): 29 | if letter in node.children.keys(): 30 | node = node.children[letter] 31 | if node.is_word: 32 | res.append(word[0 : i + 1]) 33 | else: 34 | break 35 | return res 36 | 37 | 38 | def multisearch(text, search_terms): 39 | t = Trie() 40 | 41 | # insert t's in trie 42 | for word in search_terms: 43 | t.insert(word) 44 | # a dict with t's element as keys, and all the possible indices 45 | # where they are discovered in string 46 | res = collections.defaultdict(list) 47 | # start matching the sentence now 48 | n = len(text) 49 | for i in range(n): 50 | discovered_words = t.check_existence(text[i:]) 51 | for word in discovered_words: 52 | # the discovered word, starts at i 53 | res[word].append(i) 54 | return dict(res) 55 | 56 | 57 | def test_multisearch(): 58 | small_words = ["i", "is", "pp", "ms"] 59 | expected = {"i": [1, 4, 7, 10], "is": [1, 4], "pp": [8]} 60 | assert multisearch("mississippi", small_words) == expected 61 | -------------------------------------------------------------------------------- /chapter_17/p18_shortest_supersequence.py: -------------------------------------------------------------------------------- 1 | # o(n) time, o(n) space solution via sliding window. 2 | # better than the one provided in ctci. 3 | 4 | import collections 5 | 6 | 7 | def min_window(big_array, small_array): 8 | n = len(small_array) 9 | frequencies = collections.defaultdict(int) 10 | 11 | for i in range(n): 12 | frequencies[small_array[i]] += 1 13 | 14 | # window invariant: 'contains all the chars in t' 15 | min_win_len = float("inf") 16 | left = 0 17 | missing = n 18 | min_win_left = -1 19 | min_win_right = -1 20 | for right, char in enumerate(big_array): 21 | # insertion logic 22 | if frequencies[char] > 0: 23 | missing -= 1 24 | # nevertheless, insert the element 25 | frequencies[char] -= 1 26 | 27 | if missing == 0: 28 | 29 | while left <= right and missing == 0: 30 | if right - left + 1 < min_win_len: 31 | min_win_len = right - left + 1 32 | min_win_left = left 33 | min_win_right = right 34 | 35 | if frequencies[big_array[left]] == 0: 36 | # then you are making a blunder 37 | missing += 1 38 | frequencies[big_array[left]] += 1 39 | left += 1 40 | # break 41 | else: 42 | frequencies[big_array[left]] += 1 43 | left += 1 44 | 45 | if min_win_len == float("inf"): 46 | return 47 | return min_win_left, min_win_right 48 | 49 | 50 | def test_min_window(): 51 | s = "75902135791158897" 52 | t = "159" 53 | assert min_window(s, t) == (7, 10) 54 | -------------------------------------------------------------------------------- /chapter_17/p21_volume_of_histogram.py: -------------------------------------------------------------------------------- 1 | # volume of histogram 2 | # o(n) time 3 | # o(n) space 4 | 5 | 6 | def find_volume(a): 7 | if len(a) <= 2: 8 | return 0 9 | n = len(a) 10 | h_left = [0] * n # max height on left, excluding the curr bar 11 | h_right = [0] * n # max height on right, excluding the curr bar 12 | for i in range(1, n): 13 | h_left[i] = max(h_left[i - 1], a[i - 1]) 14 | for i in reversed(range(n - 1)): 15 | h_right[i] = max(h_right[i + 1], a[i + 1]) 16 | volume = 0 17 | # neglect extreme left and extreme right bar, they can never be filled 18 | for i in range(1, n - 1): 19 | min_height = min(h_left[i], h_right[i]) 20 | if min_height - a[i] > 0: 21 | volume += min_height - a[i] 22 | 23 | return volume 24 | 25 | 26 | def test_find_volume(): 27 | hist = [0, 0, 4, 0, 0, 6, 0, 0, 3, 0, 5, 0, 1, 0, 0, 0] 28 | assert find_volume(hist) == 26 29 | -------------------------------------------------------------------------------- /chapter_17/p22_word_transformer.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | 4 | def word_transformer(source, target, words): 5 | # state function 6 | # takes a word, gives the other words one hop away 7 | def state(word1): 8 | res = [] 9 | for word2 in words: 10 | # check whether word1 and word2 are one hop away 11 | if word2 != word1 and len(word2) == len(word1): 12 | n_unequal_chars = 0 13 | for i in range(len(word2)): 14 | if word1[i] != word2[i]: 15 | n_unequal_chars += 1 16 | if n_unequal_chars == 1: 17 | res.append(word2) 18 | return res 19 | 20 | # function to reconstruct the path from parent pointers after bfs 21 | def reconstruct_path(parents, target): 22 | res = [target] 23 | temp = target 24 | while temp: 25 | temp = parents[temp] 26 | res.append(temp) 27 | # list needs to be reversed since we need the path from source to target. 28 | # parent pointers are in opposite directions by default 29 | return res[::-1] 30 | 31 | # naive bfs loop 32 | 33 | q = collections.deque() 34 | visited = set() 35 | q.append(source) 36 | level = 0 37 | # a dictionary to keep a track of the parent pointers 38 | parents = collections.defaultdict(str) 39 | parents[source] = None 40 | 41 | while q: 42 | for i in range(len(q)): 43 | word = q.popleft() 44 | if word == target: 45 | path = reconstruct_path(parents, target) 46 | return path[1:] 47 | visited.add(word) 48 | # get neighbouring words 49 | neighs = state(word) 50 | for neigh in neighs: 51 | if neigh not in visited: 52 | parents[neigh] = word 53 | q.append(neigh) 54 | level += 1 55 | return -1 56 | 57 | 58 | def test_word_transformer(): 59 | source = "bit" 60 | target = "dog" 61 | dictionary = ["but", "put", "big", "pot", "pog", "dog", "lot"] 62 | expected = ["bit", "but", "put", "pot", "pog", "dog"] 63 | assert word_transformer(source, target, dictionary) == expected 64 | 65 | source = "damp" 66 | target = "like" 67 | dictionary = ["damp", "lime", "limp", "lamp", "like"] 68 | expected = ["damp", "lamp", "limp", "lime", "like"] 69 | assert word_transformer(source, target, dictionary) == expected 70 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = *.py 3 | 4 | [pylava] 5 | format = pylint 6 | linters = pylint 7 | skip = *.venv/*,*.tox/*,.git*,*.direnv/*,*build/* 8 | ignore = C0102,C0111,C0203,C0301,C0302,C0325,C0330,C0412,C0413,C901,E0101,E0202,E203,E0213,E0611,E1003,E1102,E1120,E1123,E1129,E1133,E1135,E1136,E1137,E126,E131,E241,E402,E501,F401,R0201,R0911,R0912,R0914,R0916,R1702,W0105,W0107,W0108,W0125,W0201,W0212,W0223,W0511,W0603,W0622,W0640,W0703,W1203,W1306,W1307,W1401,W1402,W503 9 | 10 | [flake8] 11 | max-line-length = 88 12 | extend-ignore = E203,E800,VNE001,VNE002 13 | pytest-parametrize-names-type = csv --------------------------------------------------------------------------------