├── .github └── workflows │ └── pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── bitwise XOR ├── complement of base 10 numbers.py ├── flip and invert matrix.py ├── intro.py ├── single number.py └── two single numbers.py ├── breadth first search ├── binary tree level order traveral.py ├── connect all level order siblings.py ├── connect level order siblings.py ├── level averages in a binary tree.py ├── level order successor.py ├── minimum depth of a binary tree.py ├── reverse level order traversal.py ├── right view of a binary tree.py └── zigzag traversal.py ├── cyclic sort ├── cyclic sort.py ├── find all duplicate numbers.py ├── find all missing numbers.py ├── find the corrupt pair.py ├── find the duplicate number.py ├── find the first k missing positive numbers.py ├── find the missing number.py └── find the smallest missing positive number.py ├── depth first search ├── all paths for a sum.py ├── binary tree path sum.py ├── count paths for a sum.py ├── path with given sequence.py ├── path with maximum sum.py ├── sum of path numbers.py └── tree diameter.py ├── dynamic programming ├── 01 knapsack.py ├── count of subset sum.py ├── equal subset sum partition.py ├── minimum subset sum difference.py ├── subset sum.py └── target sum.py ├── fast_slow pointers ├── cycle in a circular array.py ├── happy number.py ├── linkedlist cycle.py ├── middle of linkedlist.py ├── palindorme linkedlist.py ├── rearrange a linkedlist.py └── start of linkedlist cycle.py ├── in-place reversal of a linkedlist ├── reverse a linkedlist.py ├── reverse a sub list.py ├── reverse alternating k element sub list.py ├── reverse every k element sub list.py └── rotate a linkedlist.py ├── k way merge ├── k pairs with largest sums.py ├── kth smallest number in a sorted matrix.py ├── kth smallest number in m sorted lists.py ├── merge k sorted lists.py └── smallest number range.py ├── merge intervals ├── conflicting appointment.py ├── employee free time.py ├── insert interval.py ├── interval intersection.py ├── maximum cpu load.py ├── merge intervals.py └── minimum meeting room.py ├── miscellaneous └── kth smallest number.py ├── modified binary search ├── bitonic array maximum.py ├── ceiling of a number.py ├── minimum difference element.py ├── next letter.py ├── number range.py ├── order agnostic binary search.py ├── rotation count.py ├── search bitonic array.py ├── search in a sorted infinite array.py └── search in rotated array.py ├── scripts ├── finddir.sh ├── fixlinks.sh └── generate_md.sh ├── sliding window ├── fruits into baskets.py ├── intro.py ├── longest substring with K distinct characters.py ├── longest_substring_with_ones_after_replacement.py ├── longest_substring_with_same_letters_after_replacement.py ├── maximum sum subarrays of size k.py ├── no-repeat substring.py ├── permutation in a string.py ├── smallest subarray with a given sum.py ├── smallest window containing substring.py ├── string anagrams.py └── word concatenation.py ├── subsets ├── balanced parentheses.py ├── evaluate expression.py ├── permutations.py ├── string permutations by changing case.py ├── structurally unique binary search trees.py ├── subsets with duplicates.py ├── subsets.py └── unique generalized abbreviations.py ├── top k elements ├── connect ropes.py ├── frequency sort.py ├── frequency stack.py ├── k closest numbers.py ├── k closest points to the origin.py ├── kth largest number in a stream.py ├── kth smallest number.py ├── maximum distinct elements.py ├── rearrange string k distance apart.py ├── rearrange string.py ├── scheduling tasks.py ├── sum of elements.py ├── top k frequent numbers.py └── top k numbers.py ├── topological sort ├── alien dictionary.py ├── all tasks scheduling orders.py ├── minimum height trees.py ├── reconstructing a sequence.py ├── task scheduling order.py ├── tasks scheduling.py └── topological sort.py ├── two heaps ├── find the median of a number stream.py ├── maximize capital.py ├── next interval.py └── sliding window median.py └── two pointers ├── comparing strings containing backspaces.py ├── dutch national flag problem.py ├── minimum window sort.py ├── pair with target sum.py ├── quadruple sum to target.py ├── remove duplicates.py ├── squaring a sorted array.py ├── triplet sum close to target.py ├── triplet sum to zero.py └── triplet with smaller sum.py /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v1 32 | - name: Process files 33 | run: | 34 | ./scripts/finddir.sh . 35 | ./scripts/generate_md.sh . 36 | ./scripts/fixlinks.sh . 37 | - name: Build with Jekyll 38 | uses: actions/jekyll-build-pages@v1 39 | with: 40 | source: ./ 41 | destination: ./_site 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v1 44 | 45 | # Deployment job 46 | deploy: 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | runs-on: ubuntu-latest 51 | needs: build 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v1 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 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 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JoannWu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grokking-the-coding-interview 2 | educative.io course: https://www.educative.io/courses/grokking-the-coding-interview in python 3 | 4 | Thanks for humble-barnacle001 for creating the website: https://joanwu5.github.io/Grokking-the-coding-interview/ 5 | -------------------------------------------------------------------------------- /bitwise XOR/complement of base 10 numbers.py: -------------------------------------------------------------------------------- 1 | # Every non-negative integer N has a binary representation, 2 | # for example, 8 can be represented as “1000” in binary and 7 as “0111” in binary. 3 | 4 | # The complement of a binary representation is the number in binary that 5 | # we get when we change every 1 to a 0 and every 0 to a 1. For example, the binary complement of “1010” is “0101”. 6 | # For a given positive number N in base-10, return the complement of its binary representation as a base-10 integer. 7 | 8 | # Example: 9 | # Input: 8 10 | # Output: 7 11 | # Explanation: 8 is 1000 in binary, its complement is 0111 in binary, which is 7 in base-10. 12 | 13 | # time: O(logN) space: O(1) 14 | def complement_base_ten_number(number): 15 | bit_count = 1 16 | while bit_count <= number: 17 | bit_count = bit_count << 1 18 | 19 | return number ^ (bit_count - 1) 20 | 21 | print(complement_base_ten_number(8)) 22 | print(complement_base_ten_number(10)) -------------------------------------------------------------------------------- /bitwise XOR/flip and invert matrix.py: -------------------------------------------------------------------------------- 1 | # Given a binary matrix representing an image, we want to flip the image horizontally, then invert it. 2 | # To flip an image horizontally means that each row of the image is reversed. 3 | # For example, flipping [0, 1, 1] horizontally results in [1, 1, 0]. 4 | 5 | # To invert an image means that each 0 is replaced by 1, and each 1 is replaced by 0. 6 | # For example, inverting [1, 1, 0] results in [0, 0, 1]. 7 | 8 | # Example 1: 9 | 10 | # Input: [ 11 | # [1,0,1], 12 | # [1,1,1], 13 | # [0,1,1] 14 | # ] 15 | # Output: [ 16 | # [0,1,0], 17 | # [0,0,0], 18 | # [0,0,1] 19 | # ] 20 | # Explanation: First reverse each row: [[1,0,1],[1,1,1],[1,1,0]]. Then, invert the image: [[0,1,0],[0,0,0],[0,0,1]] 21 | 22 | def flip_invert_image(matrix): 23 | m = len(matrix) 24 | n = len(matrix[0]) 25 | 26 | for i in range(m): 27 | for j in range((n + 1) // 2): 28 | matrix[i][j], matrix[i][n - j - 1] = matrix[i][n - j - 1] ^ 1, matrix[i][j] ^ 1 29 | 30 | return matrix 31 | 32 | print(flip_invert_image([ 33 | [1,0,1], 34 | [1,1,1], 35 | [0,1,1] 36 | ])) 37 | 38 | print(flip_invert_image([ 39 | [1,1,0,0], 40 | [1,0,0,1], 41 | [0,1,1,1], 42 | [1,0,1,0] 43 | ])) -------------------------------------------------------------------------------- /bitwise XOR/intro.py: -------------------------------------------------------------------------------- 1 | # Important properties of XOR to remember # 2 | # Following are some important properties of XOR to remember: 3 | 4 | # Taking XOR of a number with itself returns 0, e.g., 5 | 6 | # 1 ^ 1 = 0 7 | # 29 ^ 29 = 0 8 | # Taking XOR of a number with 0 returns the same number, e.g., 9 | 10 | # 1 ^ 0 = 1 11 | # 31 ^ 0 = 31 12 | # XOR is Associative & Commutative, which means: 13 | 14 | # (a ^ b) ^ c = a ^ (b ^ c) 15 | # a ^ b = b ^ a 16 | 17 | # Given an array of n-1 integers in the range from 1 to n, find the one number that is missing from the array. 18 | 19 | # Example: 20 | # Input: 1, 5, 2, 6, 4 21 | # Answer: 3 22 | 23 | # O(N) space: O(1) 24 | def find_missing_number(arr): 25 | n = len(arr) + 1 26 | arr_sum = 0 27 | for i in range(1, n + 1): 28 | arr_sum += i 29 | 30 | for i in arr: 31 | arr_sum -= i 32 | 33 | return arr_sum 34 | 35 | print(find_missing_number([1, 5, 2, 6, 4])) 36 | 37 | # problem: While finding the sum of numbers from 1 to n, we can get integer overflow when nn is large. 38 | def find_missing_number_2(arr): 39 | n = len(arr) + 1 40 | arr_xor1 = 1 41 | for i in range(2, n + 1): 42 | arr_xor1 = arr_xor1 ^ i 43 | 44 | arr_xor2 = arr[0] 45 | for i in range(1, n - 1): 46 | arr_xor2 = arr_xor2 ^ arr[i] 47 | 48 | return arr_xor2 ^ arr_xor1 49 | print(find_missing_number_2([1, 5, 2, 6, 4])) 50 | -------------------------------------------------------------------------------- /bitwise XOR/single number.py: -------------------------------------------------------------------------------- 1 | # In a non-empty array of integers, every number appears twice except for one, find that single number. 2 | # Example: 3 | 4 | # Input: 1, 4, 2, 1, 3, 2, 3 5 | # Output: 4 6 | 7 | # O(N) space: O(1) 8 | def find_single_number(arr): 9 | arr_xor = arr[0] 10 | for i in range(1, len(arr)): 11 | arr_xor = arr_xor ^ arr[i] 12 | 13 | return arr_xor 14 | 15 | print(find_single_number([1, 4, 2, 1, 3, 2, 3])) -------------------------------------------------------------------------------- /bitwise XOR/two single numbers.py: -------------------------------------------------------------------------------- 1 | # In a non-empty array of numbers, every number appears exactly twice except two numbers that appear only once. 2 | # Find the two numbers that appear only once. 3 | 4 | # Example 1: 5 | # Input: [1, 4, 2, 1, 3, 5, 6, 2, 3, 5] 6 | # Output: [4, 6] 7 | 8 | # Input: [2, 1, 3, 2] 9 | # Output: [1, 3] 10 | 11 | # O(N) space: O(1) 12 | def find_two_single_numbers(arr): 13 | n1xn2 = 0 14 | for num in arr: 15 | n1xn2 ^= num 16 | 17 | rightmost_set_bit = 1 18 | while rightmost_set_bit & n1xn2 == 0: 19 | rightmost_set_bit <<= 1 20 | 21 | num1, num2 = 0, 0 22 | for num in arr: 23 | if num & rightmost_set_bit == 0: 24 | num1 ^= num 25 | else: 26 | num2 ^= num 27 | 28 | return [num1, num2] 29 | 30 | print(find_two_single_numbers([1, 4, 2, 1, 3, 5, 6, 2, 3, 5])) 31 | print(find_two_single_numbers([2, 1, 3, 2])) -------------------------------------------------------------------------------- /breadth first search/binary tree level order traveral.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, populate an array to represent its level-by-level traversal. 2 | # You should populate the values of all nodes of each level from left to right in separate sub-arrays. 3 | 4 | # O(N) 5 | # space: O(N) 6 | from collections import deque 7 | 8 | class TreeNode: 9 | def __init__(self, value) -> None: 10 | self.value = value 11 | self.left, self.right = None, None 12 | 13 | def traverse(root): 14 | result = [] 15 | if root is None: 16 | return result 17 | 18 | queue = deque() 19 | queue.append(root) 20 | while queue: 21 | level_size = len(queue) 22 | current_level = [] 23 | for _ in range(level_size): 24 | current_node = queue.popleft() 25 | current_level.append(current_node.value) 26 | 27 | if current_node.left: 28 | queue.append(current_node.left) 29 | 30 | if current_node.right: 31 | queue.append(current_node.right) 32 | 33 | result.append(current_level) 34 | return result 35 | 36 | root = TreeNode(12) 37 | root.left = TreeNode(7) 38 | root.right = TreeNode(1) 39 | root.left.left = TreeNode(9) 40 | root.right.left = TreeNode(10) 41 | root.right.right = TreeNode(5) 42 | print(traverse(root)) -------------------------------------------------------------------------------- /breadth first search/connect all level order siblings.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, connect each node with its level order successor. 2 | # The last node of each level should point to the first node of the next level. 3 | 4 | # O(N) space:O(N) 5 | from collections import deque 6 | 7 | class TreeNode: 8 | def __init__(self, value) -> None: 9 | self.value = value 10 | self.left = None 11 | self.right = None 12 | self.next = None 13 | 14 | def print_tree(self): 15 | current = self 16 | while current: 17 | print(str(current.value) + " ", end = "") 18 | current = current.next 19 | print() 20 | 21 | def connect_all_level_order_siblings(root): 22 | if root is None: 23 | return 24 | 25 | queue = deque() 26 | queue.append(root) 27 | 28 | previous_node = None 29 | while queue: 30 | current_node = queue.popleft() 31 | 32 | if previous_node: 33 | previous_node.next = current_node 34 | previous_node = current_node 35 | 36 | if current_node.left: 37 | queue.append(current_node.left) 38 | if current_node.right: 39 | queue.append(current_node.right) 40 | 41 | return 42 | 43 | root = TreeNode(12) 44 | root.left = TreeNode(7) 45 | root.right = TreeNode(1) 46 | root.left.left = TreeNode(9) 47 | root.right.left = TreeNode(10) 48 | root.right.right = TreeNode(5) 49 | connect_all_level_order_siblings(root) 50 | root.print_tree() -------------------------------------------------------------------------------- /breadth first search/connect level order siblings.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, connect each node with its level order successor. 2 | # The last node of each level should point to a null node. 3 | 4 | # O(N) space:O(N) 5 | from collections import deque 6 | 7 | class TreeNode: 8 | def __init__(self, value) -> None: 9 | self.value = value 10 | self.left = None 11 | self.right = None 12 | self.next = None 13 | 14 | def print_level_order(self): 15 | level_node_root = self 16 | while level_node_root: 17 | current_node = level_node_root 18 | level_node_root = None 19 | while current_node: 20 | print(str(current_node.value) + "", end="") 21 | if level_node_root is None: 22 | if current_node.left: 23 | level_node_root = current_node.left 24 | elif current_node.right: 25 | level_node_root = current_node.right 26 | 27 | current_node = current_node.next 28 | print() 29 | 30 | def connect_level_order_siblings(root): 31 | if root is None: 32 | return root 33 | 34 | queue = deque() 35 | queue.append(root) 36 | 37 | while queue: 38 | previous_node = None 39 | level_size = len(queue) 40 | for _ in range(level_size): 41 | current_node = queue.popleft() 42 | 43 | if current_node.left: 44 | queue.append(current_node.left) 45 | if current_node.right: 46 | queue.append(current_node.right) 47 | 48 | if previous_node: 49 | previous_node.next = current_node 50 | else: 51 | previous_node = current_node 52 | 53 | 54 | root = TreeNode(12) 55 | root.left = TreeNode(7) 56 | root.right = TreeNode(1) 57 | root.left.left = TreeNode(9) 58 | root.right.left = TreeNode(10) 59 | root.right.right = TreeNode(5) 60 | connect_level_order_siblings(root) 61 | root.print_level_order() -------------------------------------------------------------------------------- /breadth first search/level averages in a binary tree.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, populate an array to represent the averages of all of its levels. 2 | 3 | # O(N) space:O(N) 4 | from collections import deque 5 | 6 | class TreeNode: 7 | def __init__(self, value) -> None: 8 | self.value = value 9 | self.left = None 10 | self.right = None 11 | 12 | def level_average(root): 13 | result = [] 14 | if root is None: 15 | return result 16 | 17 | queue = deque() 18 | queue.append(root) 19 | 20 | while queue: 21 | sum_level = 0.0 22 | level_size = len(queue) 23 | 24 | for _ in range(level_size): 25 | current_node = queue.popleft() 26 | 27 | sum_level += current_node.value 28 | 29 | if current_node.left: 30 | queue.append(current_node.left) 31 | if current_node.right: 32 | queue.append(current_node.right) 33 | 34 | result.append(sum_level/level_size) 35 | 36 | return result 37 | 38 | root = TreeNode(12) 39 | root.left = TreeNode(7) 40 | root.right = TreeNode(1) 41 | root.left.left = TreeNode(9) 42 | root.right.left = TreeNode(10) 43 | root.right.right = TreeNode(5) 44 | print(level_average(root)) 45 | 46 | # follow up: 47 | # Problem 1: Find the largest value on each level of a binary tree. 48 | 49 | # Solution: We will follow a similar approach, 50 | # but instead of having a running value we will track the maximum value of each level. 51 | # maxValue = max(maxValue, currentNode.val) -------------------------------------------------------------------------------- /breadth first search/level order successor.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree and a node, find the level order successor of the given node in the tree. 2 | # The level order successor is the node that appears right after the given node in the level order traversal. 3 | 4 | from collections import deque 5 | 6 | class TreeNode: 7 | def __init__(self, value) -> None: 8 | self.value = value 9 | self.left = None 10 | self.right = None 11 | 12 | def level_order_successor(root, key): 13 | if root is None: 14 | return None 15 | 16 | queue = deque() 17 | queue.append(root) 18 | while queue: 19 | current_node = queue.popleft() 20 | if current_node.left: 21 | queue.append(current_node.left) 22 | 23 | if current_node.right: 24 | queue.append(current_node.right) 25 | 26 | if current_node.value == key: 27 | break 28 | 29 | return queue[0].value if queue else None 30 | 31 | root = TreeNode(12) 32 | root.left = TreeNode(7) 33 | root.right = TreeNode(1) 34 | root.left.left = TreeNode(9) 35 | root.right.left = TreeNode(10) 36 | root.right.right = TreeNode(5) 37 | print(level_order_successor(root, 12)) 38 | print(level_order_successor(root, 9)) 39 | 40 | -------------------------------------------------------------------------------- /breadth first search/minimum depth of a binary tree.py: -------------------------------------------------------------------------------- 1 | # Find the minimum depth of a binary tree. 2 | # The minimum depth is the number of nodes along the shortest path from the root node to the nearest leaf node. 3 | 4 | from collections import deque 5 | 6 | class TreeNode: 7 | def __init__(self, value) -> None: 8 | self.value = value 9 | self.left = None 10 | self.right = None 11 | 12 | def minimum_depth(root): 13 | if root is None: 14 | return 0 15 | 16 | min_level = 0 17 | queue = deque() 18 | queue.append(root) 19 | 20 | while queue: 21 | min_level += 1 22 | level_size = len(queue) 23 | for _ in range(level_size): 24 | current_node = queue.popleft() 25 | 26 | if current_node.left is None and current_node.right is None: 27 | return min_level 28 | 29 | if current_node.left: 30 | queue.append(current_node.left) 31 | if current_node.right: 32 | queue.append(current_node.right) 33 | 34 | return min_level 35 | 36 | root = TreeNode(12) 37 | root.left = TreeNode(7) 38 | root.right = TreeNode(1) 39 | root.right.left = TreeNode(10) 40 | root.right.right = TreeNode(5) 41 | print(minimum_depth(root)) 42 | 43 | # follow up: 44 | # Problem 1: Given a binary tree, find its maximum depth (or height). 45 | def maximum_depth(root): 46 | if root is None: 47 | return 0 48 | 49 | max_level = 0 50 | queue = deque() 51 | queue.append(root) 52 | 53 | while queue: 54 | max_level += 1 55 | level_size = len(queue) 56 | for _ in range(level_size): 57 | current_node = queue.popleft() 58 | 59 | if current_node.left: 60 | queue.append(current_node.left) 61 | if current_node.right: 62 | queue.append(current_node.right) 63 | 64 | return max_level 65 | 66 | print(maximum_depth(root)) -------------------------------------------------------------------------------- /breadth first search/reverse level order traversal.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, populate an array to represent its level-by-level traversal in reverse order, 2 | # i.e., the lowest level comes first. 3 | # You should populate the values of all nodes in each level from left to right in separate sub-arrays. 4 | 5 | # O(N) space:O(N) 6 | from collections import deque 7 | 8 | class TreeNode: 9 | def __init__(self, value) -> None: 10 | self.value = value 11 | self.left = None 12 | self.right = None 13 | 14 | def reverse_traversal(root): 15 | result = deque() 16 | if root is None: 17 | return result 18 | 19 | queue = deque() 20 | queue.append(root) 21 | while queue: 22 | level_size = len(queue) 23 | current_level = [] 24 | for _ in range(level_size): 25 | current_node = queue.popleft() 26 | current_level.append(current_node.value) 27 | 28 | if current_node.left: 29 | queue.append(current_node.left) 30 | if current_node.right: 31 | queue.append(current_node.right) 32 | 33 | result.appendleft(current_level) 34 | 35 | return result 36 | 37 | root = TreeNode(12) 38 | root.left = TreeNode(7) 39 | root.right = TreeNode(1) 40 | root.left.left = TreeNode(9) 41 | root.right.left = TreeNode(10) 42 | root.right.right = TreeNode(5) 43 | print(reverse_traversal(root)) -------------------------------------------------------------------------------- /breadth first search/right view of a binary tree.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, return an array containing nodes in its right view. 2 | # The right view of a binary tree is the set of nodes visible when the tree is seen from the right side. 3 | 4 | # O(N) space:O(N) 5 | from collections import deque 6 | 7 | class TreeNode: 8 | def __init__(self, value) -> None: 9 | self.value = value 10 | self.left = None 11 | self.right = None 12 | 13 | def right_view_of_a_binary_tree(root): 14 | result = [] 15 | if root is None: 16 | return result 17 | 18 | queue = deque() 19 | queue.append(root) 20 | 21 | while queue: 22 | level_size = len(queue) 23 | for i in range(level_size): 24 | current_node = queue.popleft() 25 | if i == level_size - 1: 26 | result.append(current_node.value) 27 | 28 | if current_node.left: 29 | queue.append(current_node.left) 30 | if current_node.right: 31 | queue.append(current_node.right) 32 | 33 | 34 | return result 35 | 36 | root = TreeNode(12) 37 | root.left = TreeNode(7) 38 | root.right = TreeNode(1) 39 | root.left.left = TreeNode(9) 40 | root.right.left = TreeNode(10) 41 | root.right.right = TreeNode(5) 42 | root.left.left.right = TreeNode(3) 43 | print(right_view_of_a_binary_tree(root)) 44 | 45 | # follow up: 46 | # Problem 1: Given a binary tree, return an array containing nodes in its left view. 47 | # The left view of a binary tree is the set of nodes visible when the tree is seen from the left side. 48 | 49 | # Solution: We will be following a similar approach, 50 | # but instead of appending the last element of each level we will be appending 51 | # the first element of each level to the output array. 52 | # if i == 0: result.append(current_node.value) -------------------------------------------------------------------------------- /breadth first search/zigzag traversal.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, populate an array to represent its zigzag level order traversal. 2 | # You should populate the values of all nodes of the first level from left to right, 3 | # then right to left for the next level and 4 | # keep alternating in the same manner for the following levels. 5 | 6 | # O(N) space:O(N) 7 | from collections import deque 8 | 9 | 10 | class TreeNode: 11 | def __init__(self, value) -> None: 12 | self.value = value 13 | self.left = None 14 | self.right = None 15 | 16 | def zigzag_traversal(root): 17 | result = [] 18 | if root is None: 19 | return result 20 | 21 | queue = deque() 22 | queue.append(root) 23 | flag = True 24 | while queue: 25 | level_size = len(queue) 26 | current_level = [] 27 | for _ in range(level_size): 28 | current_node = queue.popleft() 29 | 30 | if flag: 31 | current_level.append(current_node.value) 32 | else: 33 | current_level.insert(0, current_node.value) 34 | 35 | if current_node.left: 36 | queue.append(current_node.left) 37 | if current_node.right: 38 | queue.append(current_node.right) 39 | 40 | result.append(current_level) 41 | flag = not flag 42 | 43 | return result 44 | 45 | root = TreeNode(12) 46 | root.left = TreeNode(7) 47 | root.right = TreeNode(1) 48 | root.left.left = TreeNode(9) 49 | root.right.left = TreeNode(10) 50 | root.right.right = TreeNode(5) 51 | print(zigzag_traversal(root)) 52 | -------------------------------------------------------------------------------- /cyclic sort/cyclic sort.py: -------------------------------------------------------------------------------- 1 | # We are given an array containing ‘n’ objects. Each object, when created, 2 | # was assigned a unique number from 1 to ‘n’ based on their creation sequence. 3 | # This means that the object with sequence number ‘3’ was created just before the object with sequence number ‘4’. 4 | 5 | # Write a function to sort the objects in-place on their creation sequence number in O(n) and without any extra space. 6 | # For simplicity, let’s assume we are passed an integer array containing only the sequence numbers, 7 | # though each number is actually an object. 8 | 9 | # Example: 10 | # Input: [3, 1, 5, 4, 2] 11 | # Output: [1, 2, 3, 4, 5] 12 | 13 | # O(N) space:O(1) 14 | def cyclic_sort(arr): 15 | start = 0 16 | while start < len(arr): 17 | index = arr[start] - 1 18 | if arr[index] == arr[start]: 19 | start += 1 20 | else: 21 | arr[index], arr[start] = arr[start], arr[index] 22 | return arr 23 | 24 | print(cyclic_sort([3, 1, 5, 4, 2])) 25 | print(cyclic_sort([2, 6, 4, 3, 1, 5])) 26 | print(cyclic_sort([1, 5, 6, 4, 3, 2])) -------------------------------------------------------------------------------- /cyclic sort/find all duplicate numbers.py: -------------------------------------------------------------------------------- 1 | # We are given an unsorted array containing ‘n’ numbers taken from the range 1 to ‘n’. 2 | # The array has some duplicates, find all the duplicate numbers without using any extra space. 3 | # Example: 4 | 5 | # Input: [3, 4, 4, 5, 5] 6 | # Output: [4, 5] 7 | 8 | # O(N) space:O(1) 9 | def find_all_duplicate_numbers(arr): 10 | i = 0 11 | result = [] 12 | while i < len(arr): 13 | j = arr[i] - 1 14 | if arr[i] != arr[j]: 15 | arr[i], arr[j] = arr[j], arr[i] 16 | else: 17 | i += 1 18 | 19 | for i in range(len(arr)): 20 | if i + 1 != arr[i]: 21 | result.append(arr[i]) 22 | return result 23 | 24 | print(find_all_duplicate_numbers([3, 4, 4, 5, 5])) 25 | print(find_all_duplicate_numbers([5, 4, 7, 2, 3, 5, 3])) 26 | -------------------------------------------------------------------------------- /cyclic sort/find all missing numbers.py: -------------------------------------------------------------------------------- 1 | # We are given an unsorted array containing numbers taken from the range 1 to ‘n’. 2 | # The array can have duplicates, which means some numbers will be missing. 3 | # Find all those missing numbers. 4 | 5 | # Example: 6 | # Input: [2, 3, 1, 8, 2, 3, 5, 1] 7 | # Output: 4, 6, 7 8 | # Explanation: The array should have all numbers from 1 to 8, due to duplicates 4, 6, and 7 are missing. 9 | 10 | # O(N) space:O(1) 11 | def find_all_missing_number(arr): 12 | i = 0 13 | while i < len(arr): 14 | temp = arr[i] - 1 15 | if arr[i] != arr[temp]: 16 | arr[i], arr[temp] = arr[temp], arr[i] 17 | else: 18 | i += 1 19 | 20 | print(i, arr) 21 | 22 | result = [] 23 | for i in range(len(arr)): 24 | if arr[i] != i + 1: 25 | result.append(i+1) 26 | 27 | return result 28 | 29 | print(find_all_missing_number([2, 3, 1, 8, 2, 3, 5, 1])) -------------------------------------------------------------------------------- /cyclic sort/find the corrupt pair.py: -------------------------------------------------------------------------------- 1 | # We are given an unsorted array containing ‘n’ numbers taken from the range 1 to ‘n’. 2 | # The array originally contained all the numbers from 1 to ‘n’, 3 | # but due to a data error, 4 | # one of the numbers got duplicated which also resulted in one number going missing. Find both these numbers. 5 | 6 | # Example: 7 | # Input: [3, 1, 2, 5, 2] 8 | # Output: [2, 4] 9 | # Explanation: '2' is duplicated and '4' is missing. 10 | 11 | # O(N) space:O(1) 12 | def find_corrupt_pair(arr): 13 | i = 0 14 | while i < len(arr): 15 | j = arr[i] - 1 16 | if arr[i] != arr[j]: 17 | arr[i], arr[j] = arr[j], arr[i] 18 | else: 19 | i += 1 20 | 21 | for i in range(len(arr)): 22 | if arr[i] != i + 1: 23 | return [i+1, arr[i]] 24 | 25 | return [-1, -1] 26 | 27 | print(find_corrupt_pair([3, 1, 2, 5, 2])) 28 | print(find_corrupt_pair([3, 1, 2, 3, 6, 4])) -------------------------------------------------------------------------------- /cyclic sort/find the duplicate number.py: -------------------------------------------------------------------------------- 1 | # We are given an unsorted array containing ‘n+1’ numbers taken from the range 1 to ‘n’. 2 | # The array has only one duplicate but it can be repeated multiple times. 3 | # Find that duplicate number without using any extra space. You are, however, allowed to modify the input array. 4 | 5 | # Example: 6 | # Input: [1, 4, 4, 3, 2] 7 | # Output: 4 8 | 9 | # O(N) space:O(1) 10 | def find_the_duplicate_number(arr): 11 | i = 0 12 | while i < len(arr) - 1: 13 | j = arr[i] - 1 14 | if i != j: 15 | if arr[i] != arr[j]: 16 | arr[i], arr[j] = arr[j], arr[i] 17 | else: 18 | return arr[i] 19 | else: 20 | i += 1 21 | 22 | return -1 23 | 24 | print(find_the_duplicate_number([1, 4, 4, 3, 2])) 25 | print(find_the_duplicate_number([2, 1, 3, 3, 5, 4])) 26 | print(find_the_duplicate_number([2, 4, 1, 4, 4])) 27 | 28 | # follow up: Can we solve the above problem in O(1) space and without modifying the input array? 29 | def find_start_of_cycle_2(arr, cycle_length): 30 | pointer1, pointer2 = arr[0], arr[0] 31 | for _ in range(cycle_length): 32 | pointer1 = arr[pointer1] 33 | 34 | while pointer1 != pointer2: 35 | pointer1 = arr[pointer1] 36 | pointer2 = arr[pointer2] 37 | 38 | return pointer1 39 | 40 | def find_the_duplicate_number_2(arr): 41 | slow, fast = 0, 0 42 | while True: 43 | slow = arr[slow] 44 | fast = arr[arr[fast]] 45 | 46 | if slow == fast: 47 | break 48 | 49 | current = arr[slow] 50 | 51 | cycle_length = 0 52 | while True: 53 | current = arr[current] 54 | cycle_length += 1 55 | if current == arr[slow]: 56 | break 57 | 58 | return find_start_of_cycle_2(arr, cycle_length) 59 | 60 | print(find_the_duplicate_number_2([1, 4, 4, 3, 2])) 61 | print(find_the_duplicate_number_2([2, 1, 3, 3, 5, 4])) 62 | print(find_the_duplicate_number_2([2, 4, 1, 4, 4])) 63 | -------------------------------------------------------------------------------- /cyclic sort/find the first k missing positive numbers.py: -------------------------------------------------------------------------------- 1 | # Given an unsorted array containing numbers and a number ‘k’, 2 | # find the first ‘k’ missing positive numbers in the array. 3 | 4 | # Example: 5 | # Input: [3, -1, 4, 5, 5], k=3 6 | # Output: [1, 2, 6] 7 | # Explanation: The smallest missing positive numbers are 1, 2 and 6. 8 | 9 | # O(N + K) space:O(1) 10 | def find_first_k_missing_positive_number(arr, k): 11 | i = 0 12 | while i < len(arr): 13 | j = arr[i] - 1 14 | if arr[i] > 0 and arr[i] <= len(arr) and arr[i] != arr[j]: 15 | arr[i], arr[j] = arr[j], arr[i] 16 | else: 17 | i += 1 18 | 19 | missing_number = [] 20 | extra_number = [] 21 | 22 | for i in range(len(arr)): 23 | if k == 0: 24 | break 25 | if arr[i] != i + 1: 26 | missing_number.append(i + 1) 27 | extra_number.append(arr[i]) 28 | k -= 1 29 | 30 | add_length = len(arr) 31 | while k > 0: 32 | add_length += 1 33 | if add_length not in extra_number: 34 | missing_number.append(add_length) 35 | k -= 1 36 | 37 | return missing_number 38 | 39 | print(find_first_k_missing_positive_number([3, -1, 4, 5, 5], 3)) 40 | print(find_first_k_missing_positive_number([2, 3, 4], 3)) 41 | print(find_first_k_missing_positive_number([-2, -3, 4], 2)) 42 | 43 | 44 | -------------------------------------------------------------------------------- /cyclic sort/find the missing number.py: -------------------------------------------------------------------------------- 1 | # We are given an array containing ‘n’ distinct numbers taken from the range 0 to ‘n’. 2 | # Since the array has only ‘n’ numbers out of the total ‘n+1’ numbers, find the missing number. 3 | # Example: 4 | # Input: [4, 0, 3, 1] 5 | # Output: 2 6 | 7 | # O(N) space:O(1) 8 | def find_missing_number(arr): 9 | i = 0 10 | while i < len(arr): 11 | temp = arr[i] 12 | if arr[i] < len(arr) and arr[i] != arr[temp]: 13 | arr[temp], arr[i] = arr[i], arr[temp] 14 | else: 15 | i += 1 16 | 17 | for i in range(len(arr)): 18 | if arr[i] != i: 19 | return i 20 | 21 | return len(arr) 22 | 23 | print(find_missing_number([4, 0, 3, 1])) 24 | print(find_missing_number([7, 3, 5, 2, 4, 6, 0, 1])) -------------------------------------------------------------------------------- /cyclic sort/find the smallest missing positive number.py: -------------------------------------------------------------------------------- 1 | # Given an unsorted array containing numbers, find the smallest missing positive number in it. 2 | 3 | # Example: 4 | # Input: [-3, 1, 5, 4, 2] 5 | # Output: 3 6 | # Explanation: The smallest missing positive number is '3' 7 | 8 | # O(N) space:O(1) 9 | def find_the_smallest_missing_positive_number(arr): 10 | i = 0 11 | while i < len(arr): 12 | j = arr[i] - 1 13 | if arr[i] > 0 and arr[i] <= len(arr) and arr[i] != arr[j]: 14 | arr[i], arr[j] = arr[j], arr[i] 15 | else: 16 | i += 1 17 | 18 | for i in range(len(arr)): 19 | if arr[i] != i + 1: 20 | return i + 1 21 | 22 | return -1 23 | 24 | print(find_the_smallest_missing_positive_number([-3, 1, 5, 4, 2])) 25 | print(find_the_smallest_missing_positive_number([3, -2, 0, 1, 2])) 26 | print(find_the_smallest_missing_positive_number([3, 2, 5, 1])) 27 | -------------------------------------------------------------------------------- /depth first search/all paths for a sum.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree and a number ‘S’, 2 | # find all paths from root-to-leaf such that the sum of all the node values of each path equals ‘S’. 3 | 4 | # O(N^2) best time: O(NlogN) 5 | # space: O(N) best: O(logN) 6 | class TreeNode: 7 | def __init__(self, value) -> None: 8 | self.value = value 9 | self.left, self.right = None, None 10 | 11 | def all_paths_for_a_sum(current_node, sum_value, current_path, all_paths): 12 | if current_node is None: 13 | return 14 | 15 | current_path.append(current_node.value) 16 | if current_node.value == sum_value and current_node.left is None and current_node.right is None: 17 | all_paths.append(current_path.copy()) # must use copy, or the array will be empty in the end 18 | else: 19 | all_paths_for_a_sum(current_node.left, sum_value - current_node.value, current_path, all_paths) 20 | all_paths_for_a_sum(current_node.right, sum_value - current_node.value, current_path, all_paths) 21 | 22 | current_path.pop() 23 | return 24 | 25 | root = TreeNode(12) 26 | root.left = TreeNode(7) 27 | root.right = TreeNode(1) 28 | root.left.left = TreeNode(4) 29 | root.right.left = TreeNode(10) 30 | root.right.right = TreeNode(5) 31 | all_paths = [] 32 | all_paths_for_a_sum(root, 23, [], all_paths) 33 | print(all_paths) 34 | 35 | # follow up : 36 | # Problem 1: Given a binary tree, return all root-to-leaf paths. 37 | # Solution: We can follow a similar approach. We just need to remove the “check for the path sum”. 38 | 39 | # Problem 2: Given a binary tree, find the root-to-leaf path with the maximum sum. 40 | # Solution: We need to find the path with the maximum sum. 41 | # As we traverse all paths, we can keep track of the path with the maximum sum. 42 | 43 | 44 | -------------------------------------------------------------------------------- /depth first search/binary tree path sum.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree and a number ‘S’, 2 | # find if the tree has a path from root-to-leaf such that the sum of all the node values of that path equals ‘S’. 3 | 4 | # O(N) space:O(N) 5 | class TreeNode: 6 | def __init__(self, value) -> None: 7 | self.value = value 8 | self.left, self.right = None, None 9 | 10 | def binary_tree_path_sum(root, sum_value): 11 | if root is None: 12 | return False 13 | 14 | if root.value == sum_value and root.left is None and root.right is None: 15 | return True 16 | 17 | return binary_tree_path_sum(root.left, sum_value - root.value) or binary_tree_path_sum( 18 | root.right, sum_value - root.value) 19 | 20 | 21 | root = TreeNode(12) 22 | root.left = TreeNode(7) 23 | root.right = TreeNode(1) 24 | root.left.left = TreeNode(9) 25 | root.right.left = TreeNode(10) 26 | root.right.right = TreeNode(5) 27 | print(binary_tree_path_sum(root, 23)) 28 | print(binary_tree_path_sum(root, 16)) 29 | -------------------------------------------------------------------------------- /depth first search/count paths for a sum.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree and a number ‘S’, 2 | # find all paths in the tree such that the sum of all the node values of each path equals ‘S’. 3 | # Please note that the paths can start or end at any node 4 | # but all paths must follow direction from parent to child(top to bottom). 5 | 6 | # worst: O(N^2) best:O(NlogN) 7 | # space: O(N) 8 | class TreeNode: 9 | def __init__(self, value) -> None: 10 | self.value = value 11 | self.left, self.right = None, None 12 | 13 | def count_paths(root, sum_value): 14 | return count_paths_recursive(root, sum_value, []) 15 | 16 | def count_paths_recursive(current_node, sum_value, current_path): 17 | if current_node is None: 18 | return 0 19 | 20 | current_path.append(current_node.value) 21 | path_count, path_sum = 0, 0 22 | for i in range(len(current_path)-1, -1, -1): 23 | path_sum += current_path[i] 24 | 25 | if path_sum == sum_value: 26 | path_count += 1 27 | 28 | path_count += count_paths_recursive(current_node.left, sum_value, current_path) 29 | path_count += count_paths_recursive(current_node.right, sum_value, current_path) 30 | 31 | del current_path[-1] 32 | return path_count 33 | 34 | root = TreeNode(12) 35 | root.left = TreeNode(7) 36 | root.right = TreeNode(1) 37 | root.left.left = TreeNode(4) 38 | root.right.left = TreeNode(10) 39 | root.right.right = TreeNode(5) 40 | print(count_paths(root, 11)) 41 | 42 | 43 | -------------------------------------------------------------------------------- /depth first search/path with given sequence.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree and a number sequence, find if the sequence is present as a root-to-leaf path in the given tree. 2 | 3 | # O(N) space:O(N) 4 | class TreeNode: 5 | def __init__(self, value) -> None: 6 | self.value = value 7 | self.left, self.right = None, None 8 | 9 | def find_path(root, sequence): 10 | if root is None: 11 | return len(sequence) == 0 12 | 13 | return find_path_with_sequence(root, sequence, 0) 14 | 15 | def find_path_with_sequence(current_node, sequence, sequence_index): 16 | if current_node is None: 17 | return False 18 | 19 | if sequence_index >= len(sequence) or current_node.value != sequence[sequence_index]: 20 | return False 21 | 22 | if current_node.left is None and current_node.right is None and sequence_index == len(sequence) - 1: 23 | return True 24 | 25 | return find_path_with_sequence(current_node.left, sequence, sequence_index + 1) or \ 26 | find_path_with_sequence(current_node.right, sequence, sequence_index + 1) 27 | 28 | root = TreeNode(12) 29 | root.left = TreeNode(7) 30 | root.right = TreeNode(1) 31 | root.left.left = TreeNode(9) 32 | root.right.left = TreeNode(10) 33 | root.right.right = TreeNode(5) 34 | print(find_path(root, [12, 7, 9])) 35 | print(find_path(root, [12, 10, 6])) 36 | -------------------------------------------------------------------------------- /depth first search/path with maximum sum.py: -------------------------------------------------------------------------------- 1 | # Find the path with the maximum sum in a given binary tree. 2 | # Write a function that returns the maximum sum. 3 | # A path can be defined as a sequence of nodes between any two nodes and doesn’t necessarily pass through the root. 4 | 5 | import math 6 | 7 | class TreeNode: 8 | def __init__(self, value) -> None: 9 | self.value = value 10 | self.left, self.right = None, None 11 | 12 | class MaximumPathSum: 13 | def find_maximum_path_sum(self, root): 14 | self.result = -math.inf 15 | self.caculate_sum(root) 16 | return self.result 17 | 18 | def caculate_sum(self, current_node): 19 | if current_node is None: 20 | return 0 21 | 22 | max_path_sum_left = self.caculate_sum(current_node.left) 23 | max_path_sum_right = self.caculate_sum(current_node.right) 24 | 25 | max_path_sum_left = max(max_path_sum_left, 0) 26 | max_path_sum_right = max(max_path_sum_right, 0) 27 | 28 | local_max_sum = max_path_sum_left + max_path_sum_right + current_node.value 29 | self.result = max(self.result, local_max_sum) 30 | 31 | return max(max_path_sum_left, max_path_sum_right) + current_node.value 32 | 33 | maximumPathSum = MaximumPathSum() 34 | root = TreeNode(1) 35 | root.left = TreeNode(2) 36 | root.right = TreeNode(3) 37 | print(maximumPathSum.find_maximum_path_sum(root)) 38 | 39 | root.left.left = TreeNode(1) 40 | root.left.right = TreeNode(3) 41 | root.right.left = TreeNode(5) 42 | root.right.right = TreeNode(6) 43 | root.right.left.left = TreeNode(7) 44 | root.right.left.right = TreeNode(8) 45 | root.right.right.left = TreeNode(9) 46 | print(maximumPathSum.find_maximum_path_sum(root)) 47 | 48 | root = TreeNode(-1) 49 | root.left = TreeNode(-2) 50 | print(maximumPathSum.find_maximum_path_sum(root)) 51 | -------------------------------------------------------------------------------- /depth first search/sum of path numbers.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree where each node can only have a digit (0-9) value, 2 | # each root-to-leaf path will represent a number. 3 | # Find the total sum of all the numbers represented by all paths. 4 | 5 | # O(N) space:O(N) 6 | class TreeNode: 7 | def __init__(self, value) -> None: 8 | self.value = value 9 | self.left, self.right = None, None 10 | 11 | def sum_of_path_numbers(current_node, path_sum): 12 | if current_node is None: 13 | return 0 14 | 15 | path_sum = path_sum * 10 + current_node.value 16 | if current_node.left is None and current_node.right is None: 17 | return path_sum 18 | 19 | return sum_of_path_numbers(current_node.left, path_sum) + sum_of_path_numbers(current_node.right, path_sum) 20 | 21 | root = TreeNode(1) 22 | root.left = TreeNode(0) 23 | root.right = TreeNode(1) 24 | root.left.left = TreeNode(1) 25 | root.right.left = TreeNode(6) 26 | root.right.right = TreeNode(5) 27 | print(sum_of_path_numbers(root, 0)) -------------------------------------------------------------------------------- /depth first search/tree diameter.py: -------------------------------------------------------------------------------- 1 | # Given a binary tree, find the length of its diameter. 2 | # The diameter of a tree is the number of nodes on the longest path between any two leaf nodes. 3 | # The diameter of a tree may or may not pass through the root. 4 | # Note: You can always assume that there are at least two leaf nodes in the given tree. 5 | 6 | # O(N) space:O(N) 7 | class TreeNode: 8 | def __init__(self, value) -> None: 9 | self.value = value 10 | self.left, self.right = None, None 11 | 12 | def find_diameter(root): 13 | result = [0] 14 | caculate_height(root, result) 15 | return result[0] 16 | 17 | 18 | def caculate_height(current_node, result): 19 | if current_node is None: 20 | return 0 21 | 22 | left_tree_height = caculate_height(current_node.left, result) 23 | right_tree_height = caculate_height(current_node.right, result) 24 | 25 | diameter = left_tree_height + right_tree_height + 1 26 | result[0] = max(result[0], diameter) 27 | 28 | return max(left_tree_height, right_tree_height) + 1 29 | 30 | root = TreeNode(12) 31 | root.left = TreeNode(7) 32 | root.right = TreeNode(1) 33 | root.left.left = TreeNode(4) 34 | root.right.left = TreeNode(10) 35 | root.right.right = TreeNode(5) 36 | print(find_diameter(root)) 37 | 38 | head = TreeNode(12) 39 | head.left = TreeNode(7) 40 | head.right = TreeNode(1) 41 | head.right.left = TreeNode(10) 42 | head.right.right = TreeNode(5) 43 | head.right.left.left = TreeNode(6) 44 | head.right.left.right = TreeNode(8) 45 | head.right.right.right = TreeNode(9) 46 | head.right.left.right.right = TreeNode(11) 47 | head.right.right.right.right = TreeNode(13) 48 | print(find_diameter(head)) -------------------------------------------------------------------------------- /dynamic programming/01 knapsack.py: -------------------------------------------------------------------------------- 1 | # Given two integer arrays to represent weights and profits of ‘N’ items, 2 | # we need to find a subset of these items which will give us maximum profit 3 | # such that their cumulative weight is not more than a given number ‘C’. 4 | # Each item can only be selected once, which means either we put an item in the knapsack or we skip it. 5 | 6 | # brute force 7 | # O(2^N) space: O(N) 8 | def knapsack_1(profits, weights, capacity): 9 | return knapsack_recursive_1(profits, weights, capacity, 0) 10 | 11 | def knapsack_recursive_1(profits, weights, capacity, current_index): 12 | if capacity <= 0 or current_index >= len(profits): 13 | return 0 14 | 15 | profit1, profit2 = 0, 0 16 | if weights[current_index] <= capacity: 17 | profit1 = profits[current_index] + knapsack_recursive_1(profits, weights, capacity - weights[current_index], current_index + 1) 18 | 19 | profit2 = knapsack_recursive_1(profits, weights, capacity, current_index + 1) 20 | return max(profit1, profit2) 21 | 22 | print(knapsack_1([4, 5, 3, 7], [2, 3, 1, 4], 5)) 23 | 24 | # top-down dp 25 | # O(N * C) space: O(N * C) 26 | def knapsack_2(profits, weights, capacity): 27 | dp = [[0 for _ in range(capacity + 1)] for _ in range(len(profits))] 28 | return knapsack_recursive_2(dp, profits, weights, capacity, 0) 29 | 30 | def knapsack_recursive_2(dp, profits, weights, capacity, current_index): 31 | if capacity <= 0 or current_index >= len(profits): 32 | return 0 33 | 34 | if dp[current_index][capacity] != 0: 35 | return dp[current_index][capacity] 36 | 37 | profit1, profit2 = 0, 0 38 | if weights[current_index] <= capacity: 39 | profit1 = profits[current_index] + knapsack_recursive_2(dp, profits, weights, capacity - weights[current_index], current_index + 1) 40 | 41 | profit2 = knapsack_recursive_2(dp, profits, weights, capacity, current_index + 1) 42 | dp[current_index][capacity] = max(profit1, profit2) 43 | return dp[current_index][capacity] 44 | 45 | print(knapsack_2([4, 5, 3, 7], [2, 3, 1, 4], 5)) 46 | 47 | 48 | # bottom-up dp 49 | # O(N * C) space: O(N * C) 50 | def knapsack_3(profits, weights, capacity): 51 | if capacity <= 0 or len(profits) == 0 or len(profits) != len(weights): 52 | return 0 53 | 54 | 55 | n = len(profits) 56 | dp = [[0 for _ in range(capacity + 1)] for _ in range(n)] 57 | 58 | for c in range(capacity + 1): 59 | if weights[0] <= c: 60 | dp[0][c] = profits[0] 61 | 62 | for i in range(1, n): 63 | for j in range(1, capacity + 1): 64 | profit1, profit2 = 0, 0 65 | if weights[i] <= j: 66 | profit1 = dp[i - 1][j - weights[i]] + profits[i] 67 | profit2 = dp[i - 1][j] 68 | dp[i][j] = max(profit1, profit2) 69 | 70 | print(print_selected_elements(dp, profits, weights, capacity)) 71 | return dp[n - 1][capacity] 72 | 73 | def print_selected_elements(dp, profits, weights, capacity): 74 | n = len(profits) 75 | result = [] 76 | total_profit = dp[n - 1][capacity] 77 | for i in range(n - 1, -1, -1): 78 | if total_profit != dp[i - 1][capacity]: 79 | result.append(weights[i]) 80 | capacity -= weights[i] 81 | total_profit -= profits[i] 82 | 83 | if total_profit > 0: 84 | result.append(weights[0]) 85 | 86 | return result 87 | 88 | print(knapsack_3([4, 5, 3, 7], [2, 3, 1, 4], 5)) 89 | 90 | # improve bottom-up solution 91 | def knapsack_3_1(profits, weights, capacity): 92 | if capacity <= 0 or len(profits) == 0 or len(profits) != len(weights): 93 | return 0 94 | 95 | n = len(profits) 96 | dp = [0 for _ in range(capacity + 1)] 97 | 98 | for c in range(capacity + 1): 99 | if weights[0] <= c: 100 | dp[c] = profits[0] 101 | 102 | for i in range(1, n): 103 | for j in range(capacity, -1, -1): 104 | profit1 = 0 105 | if weights[i] <= j: 106 | profit1 = dp[j - weights[i]] + profits[i] 107 | dp[j] = max(profit1, dp[j]) 108 | 109 | return dp[capacity] 110 | 111 | print(knapsack_3_1([4, 5, 3, 7], [2, 3, 1, 4], 5)) -------------------------------------------------------------------------------- /dynamic programming/count of subset sum.py: -------------------------------------------------------------------------------- 1 | # Given a set of positive numbers, find the total number of subsets whose sum is equal to a given number ‘S’. 2 | 3 | # Example: 4 | # Input: {1, 1, 2, 3}, S=4 5 | # Output: 3 6 | # The given set has '3' subsets whose sum is '4': {1, 1, 2}, {1, 3}, {1, 3} 7 | # Note that we have two similar sets {1, 3}, because we have two '1' in our input. 8 | 9 | # top-down dp 10 | # O(N * S) space: O(N * S) 11 | def count_of_subset_sum(nums, s): 12 | dp = [[-1 for _ in range(s + 1)] for _ in range(len(nums))] 13 | return count_of_subset_sum_recursive(dp, nums, 0, s) 14 | 15 | def count_of_subset_sum_recursive(dp, nums, current_index, s): 16 | if s == 0: 17 | return 1 18 | 19 | if current_index >= len(nums): 20 | return 0 21 | 22 | if dp[current_index][s] == -1: 23 | sum1 = 0 24 | if s - nums[current_index] >= 0: 25 | sum1 = count_of_subset_sum_recursive(dp, nums, current_index + 1, s - nums[current_index]) 26 | 27 | sum2 = count_of_subset_sum_recursive(dp, nums, current_index + 1, s) 28 | dp[current_index][s] = sum1 + sum2 29 | 30 | return dp[current_index][s] 31 | 32 | 33 | print(count_of_subset_sum([1, 1, 2, 3], 4)) 34 | print(count_of_subset_sum([1, 2, 7, 1, 5], 9)) 35 | 36 | # bottom- up dp 37 | # O(N * S) space: O(N * S) 38 | def count_of_subset_sum_2(nums, s): 39 | dp = [[-1 for _ in range(s + 1)] for _ in range(len(nums))] 40 | 41 | for i in range(len(nums)): 42 | dp[i][0] = 1 43 | 44 | for j in range(1, s + 1): 45 | if nums[0] == j: 46 | dp[0][j] = 1 47 | else: 48 | dp[0][j] = 0 49 | 50 | for i in range(1, len(nums)): 51 | for j in range(1, s + 1): 52 | dp[i][j] = dp[i - 1][j] 53 | if j >= nums[i]: 54 | dp[i][j] += dp[i - 1][j - nums[i]] 55 | 56 | return dp[len(nums) - 1][s] 57 | 58 | 59 | print(count_of_subset_sum_2([1, 1, 2, 3], 4)) 60 | print(count_of_subset_sum_2([1, 2, 7, 1, 5], 9)) 61 | -------------------------------------------------------------------------------- /dynamic programming/equal subset sum partition.py: -------------------------------------------------------------------------------- 1 | # Given a set of positive numbers, 2 | # find if we can partition it into two subsets such that the sum of elements in both subsets is equal. 3 | 4 | # Example: 5 | # Input: {1, 2, 3, 4} 6 | # Output: True 7 | # Explanation: The given set can be partitioned into two subsets with equal sum: {1, 4} & {2, 3} 8 | 9 | # top-down dp 10 | # O(N * S) where ‘N’ represents total numbers and ‘S’ is the total sum of all the numbers. 11 | def can_partition(num): 12 | sum_num = sum(num) 13 | if sum_num % 2 != 0: 14 | return False 15 | 16 | dp = [[-1 for _ in range(int(sum_num/2) + 1)] for _ in range(len(num))] 17 | return True if can_partition_recursive(dp, num, int(sum_num/2), 0) == 1 else False 18 | 19 | def can_partition_recursive(dp, num, sum_num, current_index): 20 | if sum_num == 0: 21 | return 1 22 | 23 | n = len(num) 24 | if n == 0 or current_index >= n: 25 | return 0 26 | 27 | if dp[current_index][sum_num] == -1: 28 | if num[current_index] <= sum_num: 29 | if can_partition_recursive(dp, num, sum_num - num[current_index], current_index + 1): 30 | dp[current_index][sum_num] = 1 31 | return 1 32 | 33 | dp[current_index][sum_num] = can_partition_recursive(dp, num, sum_num, current_index + 1) 34 | 35 | return dp[current_index][sum_num] 36 | 37 | print(can_partition([1, 2, 3, 4])) 38 | print(can_partition([1, 1, 3, 4, 7])) 39 | print(can_partition([2, 3, 4, 6])) 40 | 41 | # bottom-up 42 | # O(N * S) space: O(N * S) 43 | def can_partition_2(num): 44 | sum_num = sum(num) 45 | if sum_num % 2 != 0: 46 | return False 47 | sum_num = int(sum_num/2) 48 | n = len(num) 49 | dp = [[False for _ in range(sum_num + 1)] for _ in range(n)] 50 | 51 | for i in range(n): 52 | dp[i][0] = True 53 | 54 | for j in range(1, sum_num + 1): 55 | dp[0][j] = num[0] == j 56 | 57 | for i in range(1, n): 58 | for j in range(1, sum_num + 1): 59 | if dp[i - 1][j]: 60 | dp[i][j] = dp[i - 1][j] 61 | elif j >= num[i]: 62 | dp[i][j] = dp[i - 1][j - num[i]] 63 | 64 | return dp[n - 1][sum_num] 65 | 66 | print(can_partition_2([1, 2, 3, 4])) 67 | print(can_partition_2([1, 1, 3, 4, 7])) 68 | print(can_partition_2([2, 3, 4, 6])) -------------------------------------------------------------------------------- /dynamic programming/minimum subset sum difference.py: -------------------------------------------------------------------------------- 1 | # Given a set of positive numbers, partition the set into two subsets with minimum difference between their subset sums. 2 | 3 | # Example: 4 | # Input: {1, 2, 3, 9} 5 | # Output: 3 6 | # Explanation: We can partition the given set into two subsets where minimum absolute difference 7 | # between the sum of numbers is '3'. Following are the two subsets: {1, 2, 3} & {9}. 8 | 9 | # brute force 10 | # O(2^N) space:O(N) 11 | def minimum_subset_sum_difference_1(nums): 12 | return minimum_subset_sum_difference_1_recursive(nums, 0, 0, 0) 13 | 14 | def minimum_subset_sum_difference_1_recursive(nums, current_index, sum1, sum2): 15 | if current_index >= len(nums): 16 | return abs(sum1 - sum2) 17 | 18 | diff1 = minimum_subset_sum_difference_1_recursive(nums, current_index + 1, sum1 + nums[current_index], sum2) 19 | diff2 = minimum_subset_sum_difference_1_recursive(nums, current_index + 1, sum1, sum2 + nums[current_index]) 20 | return min(diff1, diff2) 21 | 22 | print(minimum_subset_sum_difference_1([1, 2, 3, 9])) 23 | print(minimum_subset_sum_difference_1([1, 2, 7, 1, 5])) 24 | print(minimum_subset_sum_difference_1([1, 3, 100, 4])) 25 | 26 | # top-down dp 27 | def minimum_subset_sum_difference_2(nums): 28 | s = sum(nums) 29 | dp = [[-1 for _ in range(s + 1)] for _ in range(len(nums))] 30 | return minimum_subset_sum_difference_2_recursive(dp, nums, 0, 0, 0) 31 | 32 | # O(N * S) where ‘N’ represents total numbers and ‘S’ is the total sum of all the numbers. 33 | # space: O(N * S) 34 | def minimum_subset_sum_difference_2_recursive(dp, nums, current_index, sum1, sum2): 35 | if current_index >= len(nums): 36 | return abs(sum1 - sum2) 37 | 38 | if dp[current_index][sum1] == -1: 39 | diff1 = minimum_subset_sum_difference_2_recursive(dp, nums, current_index + 1, sum1 + nums[current_index], sum2) 40 | diff2 = minimum_subset_sum_difference_2_recursive(dp, nums, current_index + 1, sum1, sum2 + nums[current_index]) 41 | dp[current_index][sum1] = min(diff1, diff2) 42 | 43 | return dp[current_index][sum1] 44 | 45 | print(minimum_subset_sum_difference_2([1, 2, 3, 9])) 46 | print(minimum_subset_sum_difference_2([1, 2, 7, 1, 5])) 47 | print(minimum_subset_sum_difference_2([1, 3, 100, 4])) 48 | 49 | # bottom-up dp 50 | # O(N * S) space: O(N * S) 51 | def minimum_subset_sum_difference_3(nums): 52 | s = sum(nums) 53 | dp = [[False for _ in range(int(s/2) + 1)] for _ in range(len(nums))] 54 | for i in range(len(nums)): 55 | dp[i][0] = True 56 | 57 | for j in range(int(s/2) + 1): 58 | dp[0][j] = nums[0] == j 59 | 60 | for i in range(1, len(nums)): 61 | for j in range(1, int(s/2) + 1): 62 | if dp[i - 1][j]: 63 | dp[i][j] = dp[i - 1][j] 64 | elif j >= nums[i]: 65 | dp[i][j] = dp[i - 1][j - nums[i]] 66 | 67 | sum1 = 0 68 | for i in range(int(s/2), -1, -1): 69 | if dp[len(nums) - 1][i]: 70 | sum1 = i 71 | break 72 | 73 | sum2 = s - sum1 74 | return abs(sum1 - sum2) 75 | 76 | print(minimum_subset_sum_difference_3([1, 2, 3, 9])) 77 | print(minimum_subset_sum_difference_3([1, 2, 7, 1, 5])) 78 | print(minimum_subset_sum_difference_3([1, 3, 100, 4])) -------------------------------------------------------------------------------- /dynamic programming/subset sum.py: -------------------------------------------------------------------------------- 1 | # Given a set of positive numbers, determine if a subset exists whose sum is equal to a given number ‘S’. 2 | 3 | # Example: 4 | # Input: {1, 2, 3, 7}, S=6 5 | # Output: True 6 | # The given set has a subset whose sum is '6': {1, 2, 3} 7 | 8 | # O(N * S) space: O(N * S) 9 | def can_subset_sum(num, s): 10 | if sum(num) < s: 11 | return False 12 | 13 | n = len(num) 14 | dp = [[False for _ in range(s + 1)]for _ in range(n)] 15 | 16 | for i in range(n): 17 | dp[i][0] = True 18 | 19 | for j in range(1, s + 1): 20 | dp[0][j] = j == num[0] 21 | 22 | for i in range(1, n): 23 | for j in range(1, s + 1): 24 | if dp[i - 1][j]: 25 | dp[i][j] = dp[i - 1][j] 26 | elif j >= num[i]: 27 | dp[i][j] = dp[i - 1][j - num[i]] 28 | 29 | return dp[n - 1][s] 30 | 31 | print(can_subset_sum([1, 2, 3, 7], 6)) 32 | print(can_subset_sum([1, 2, 7, 1, 5], 10)) 33 | print(can_subset_sum([1, 3, 4, 8], 6)) 34 | 35 | # O(N * S) space: O(S) 36 | def can_subset_sum_2(num, s): 37 | if sum(num) < s: 38 | return False 39 | 40 | n = len(num) 41 | dp = [False for _ in range(s + 1)] 42 | 43 | dp[0] = True 44 | 45 | for i in range(n): 46 | for j in range(s, -1, -1): 47 | if not dp[j] and j >= num[i]: 48 | dp[j] = dp[j - num[i]] 49 | 50 | return dp[s] 51 | 52 | 53 | print(can_subset_sum_2([1, 2, 3, 7], 6)) 54 | print(can_subset_sum_2([1, 2, 7, 1, 5], 10)) 55 | print(can_subset_sum_2([1, 3, 4, 8], 6)) 56 | -------------------------------------------------------------------------------- /dynamic programming/target sum.py: -------------------------------------------------------------------------------- 1 | # You are given a set of positive numbers and a target sum ‘S’. 2 | # Each number should be assigned either a ‘+’ or ‘-’ sign. 3 | # We need to find the total ways to assign symbols to make the sum of the numbers equal to the target ‘S’. 4 | 5 | # Example: 6 | # Input: {1, 1, 2, 3}, S=1 7 | # Output: 3 8 | # Explanation: The given set has '3' ways to make a sum of '1': {+1-1-2+3} & {-1+1-2+3} & {+1+1+2-3} 9 | 10 | # top-down dp 11 | # O(N * S) space: O(N * S) 12 | def target_sum(nums, s): 13 | total_sum = sum(nums) 14 | if total_sum < s or (s + total_sum) % 2 != 0: 15 | return 0 16 | 17 | target_sum = (s + total_sum) // 2 18 | dp = [[-1 for _ in range(target_sum + 1)] for _ in range(len(nums))] 19 | 20 | return target_sum_recursive(dp, nums, target_sum, 0) 21 | 22 | def target_sum_recursive(dp, nums, s, current_index): 23 | if s == 0: 24 | return 1 25 | 26 | if current_index >= len(nums): 27 | return 0 28 | 29 | if dp[current_index][s] == -1: 30 | sum1 = 0 31 | if s >= nums[current_index]: 32 | sum1 = target_sum_recursive(dp, nums, s - nums[current_index], current_index + 1) 33 | sum2 = target_sum_recursive(dp, nums, s, current_index + 1) 34 | 35 | dp[current_index][s] = sum1 + sum2 36 | 37 | return dp[current_index][s] 38 | 39 | print(target_sum([1, 1, 2, 3], 1)) 40 | print(target_sum([1, 2, 7, 1], 9)) 41 | 42 | # bottom-up 43 | # O(N * S) space: O(N * S) 44 | def target_sum_2(nums, s): 45 | total_sum = sum(nums) 46 | if total_sum < s or (s + total_sum) % 2 != 0: 47 | return 0 48 | 49 | target_sum = (s + total_sum) // 2 50 | 51 | dp = [[0 for _ in range(target_sum + 1)] for _ in range(len(nums))] 52 | 53 | for i in range(len(nums)): 54 | dp[i][0] = 1 55 | 56 | for j in range(1, target_sum + 1): 57 | dp[0][j] = 1 if nums[0] == j else 0 58 | 59 | for i in range(1, len(nums)): 60 | for j in range(1, target_sum + 1): 61 | dp[i][j] = dp[i - 1][j] 62 | if j >= nums[i]: 63 | dp[i][j] += dp[i - 1][j - nums[i]] 64 | 65 | return dp[len(nums) - 1][target_sum] 66 | 67 | print(target_sum_2([1, 1, 2, 3], 1)) 68 | print(target_sum_2([1, 2, 7, 1], 9)) 69 | 70 | # bottom-up optimize space complexity 71 | # O(N * S) space: O(S) 72 | def target_sum_3(nums, s): 73 | total_sum = sum(nums) 74 | if total_sum < s or (s + total_sum) % 2 != 0: 75 | return 0 76 | 77 | target_sum = (s + total_sum) // 2 78 | 79 | dp = [0 for _ in range(target_sum + 1)] 80 | 81 | dp[0] = 1 82 | 83 | for j in range(1, target_sum + 1): 84 | dp[j] = 1 if nums[0] == j else 0 85 | 86 | for i in range(1, len(nums)): 87 | for j in range(target_sum, -1, -1): 88 | if j >= nums[i]: 89 | dp[j] += dp[j - nums[i]] 90 | 91 | return dp[target_sum] 92 | 93 | print(target_sum_3([1, 1, 2, 3], 1)) 94 | print(target_sum_3([1, 2, 7, 1], 9)) 95 | -------------------------------------------------------------------------------- /fast_slow pointers/cycle in a circular array.py: -------------------------------------------------------------------------------- 1 | # We are given an array containing positive and negative numbers. 2 | # Suppose the array contains a number ‘M’ at a particular index. 3 | # Now, if ‘M’ is positive we will move forward ‘M’ indices and if ‘M’ is negative move backwards ‘M’ indices. 4 | # You should assume that the array is circular which means two things: 5 | 6 | # 1. If, while moving forward, we reach the end of the array, 7 | # we will jump to the first element to continue the movement. 8 | 9 | # 2. If, while moving backward, we reach the beginning of the array, 10 | # we will jump to the last element to continue the movement. 11 | # Write a method to determine if the array has a cycle. 12 | # The cycle should have more than one element and should follow one direction 13 | # which means the cycle should not contain both forward and backward movements. 14 | 15 | # Example: 16 | # Input: [1, 2, -1, 2, 2] 17 | # Output: true 18 | # Explanation: The array has a cycle among indices: 0 -> 1 -> 3 -> 0 19 | 20 | # Input: [2, 1, -1, -2] 21 | # Output: false 22 | # Explanation: The array does not have any cycle. 23 | 24 | # O(N^2) space:O(1) 25 | def cycle_in_circular_array(arr): 26 | for i in range(len(arr)): 27 | is_forward = arr[i] >= 0 28 | slow, fast = i, i 29 | while True: 30 | slow = find_next_index(arr, is_forward, slow) 31 | fast = find_next_index(arr, is_forward, fast) 32 | if fast != -1: 33 | fast = find_next_index(arr, is_forward, fast) 34 | if slow == fast: 35 | break 36 | 37 | if slow != -1 and slow == fast: 38 | return True 39 | return False 40 | 41 | def find_next_index(arr, is_forward, current_index): 42 | direction = arr[current_index] >= 0 43 | if is_forward != direction: 44 | return -1 45 | next_index = (current_index + arr[current_index]) % len(arr) 46 | if next_index == current_index: 47 | return -1 48 | return next_index 49 | 50 | print(cycle_in_circular_array([1, 2, -1, 2, 2])) 51 | print(cycle_in_circular_array([2, 2, -1, 2])) 52 | print(cycle_in_circular_array([2, 1, -1, -2])) 53 | print(cycle_in_circular_array([0, 1])) 54 | 55 | # O(N^2) space:O(1) 56 | def cycle_in_circular_array_2(arr): 57 | for i in range(len(arr)): 58 | is_forward = arr[i] >= 0 59 | next_index = i 60 | while next_index != -1: 61 | temp = next_index 62 | next_index = find_next_index(arr, is_forward, temp) 63 | if next_index == i: 64 | return True 65 | 66 | return False 67 | 68 | print(cycle_in_circular_array_2([1, 2, -1, 2, 2])) 69 | print(cycle_in_circular_array_2([2, 2, -1, 2])) 70 | print(cycle_in_circular_array_2([2, 1, -1, -2])) 71 | print(cycle_in_circular_array_2([0, 1])) 72 | 73 | 74 | -------------------------------------------------------------------------------- /fast_slow pointers/happy number.py: -------------------------------------------------------------------------------- 1 | # Any number will be called a happy number if, 2 | # after repeatedly replacing it with a number equal to the sum of the square of all of its digits, 3 | # leads us to number ‘1’. All other (not-happy) numbers will never reach ‘1’. Instead, they will be stuck in a cycle of numbers which does not include ‘1’. 4 | 5 | # Example: 6 | # Input: 23 7 | # Output: true (23 is a happy number) 8 | # Explanations: Here are the steps to find out that 23 is a happy number: 9 | 10 | # O(logN) ??? don't understand 11 | # space:O(1) 12 | def happy_number(num): 13 | slow, fast = num, num 14 | while True: 15 | slow = find_next_number(slow) 16 | fast = find_next_number(find_next_number(fast)) 17 | if slow == fast: 18 | break 19 | 20 | return slow == 1 21 | 22 | def find_next_number(num): 23 | next_number = 0 24 | while num > 0: 25 | digit = num % 10 26 | next_number += digit * digit 27 | num = num // 10 28 | return next_number 29 | 30 | print(happy_number(23)) 31 | print(happy_number(12)) -------------------------------------------------------------------------------- /fast_slow pointers/linkedlist cycle.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList, write a function to determine if the LinkedList has a cycle in it or not. 2 | 3 | # O(N) space:O(1) 4 | class Node: 5 | def __init__(self, value, next = None) -> None: 6 | self.value = value 7 | self.next = None 8 | 9 | 10 | def linkedlist_cycle(head): 11 | slow, fast = head, head 12 | while fast is not None and fast.next is not None: 13 | slow = slow.next 14 | fast = fast.next.next 15 | if slow == fast: 16 | return True 17 | return False 18 | 19 | # follow up:Given the head of a LinkedList with a cycle, find the length of the cycle. 20 | # O(N) space:O(1) 21 | def linkedlist_cycle_length(head): 22 | slow, fast = head, head 23 | while fast is not None and fast.next is not None: 24 | slow = slow.next 25 | fast = fast.next.next 26 | if slow == fast: 27 | return caculate_cycle_length(slow) 28 | 29 | return 0 30 | 31 | def caculate_cycle_length(pointer): 32 | current = pointer 33 | cycle_length = 0 34 | while True: 35 | current = current.next 36 | cycle_length += 1 37 | if current == pointer: 38 | break 39 | return cycle_length 40 | 41 | head = Node(1) 42 | head.next = Node(2) 43 | head.next.next = Node(3) 44 | head.next.next.next = Node(4) 45 | head.next.next.next.next = Node(5) 46 | head.next.next.next.next.next = Node(6) 47 | print(linkedlist_cycle(head)) 48 | print(linkedlist_cycle_length(head)) 49 | 50 | head.next.next.next.next.next.next = head.next.next 51 | print(linkedlist_cycle(head)) 52 | print(linkedlist_cycle_length(head)) 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /fast_slow pointers/middle of linkedlist.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList, write a method to return the middle node of the LinkedList. 2 | # If the total number of nodes in the LinkedList is even, return the second middle node. 3 | 4 | # Example: 5 | # Input: 1 -> 2 -> 3 -> 4 -> 5 -> null 6 | # Output: 3 7 | 8 | # O(N) space:O(1) 9 | class Node: 10 | def __init__(self, value, next = None) -> None: 11 | self.value = value 12 | self.next = None 13 | 14 | def middle_of_linkedlist(head): 15 | slow, fast = head, head 16 | while fast is not None and fast.next is not None: 17 | slow = slow.next 18 | fast = fast.next.next 19 | return slow.value 20 | 21 | 22 | head = Node(1) 23 | head.next = Node(2) 24 | head.next.next = Node(3) 25 | head.next.next.next = Node(4) 26 | head.next.next.next.next = Node(5) 27 | print(middle_of_linkedlist(head)) 28 | 29 | head.next.next.next.next.next = Node(6) 30 | print(middle_of_linkedlist(head)) 31 | -------------------------------------------------------------------------------- /fast_slow pointers/palindorme linkedlist.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList, write a method to check if the LinkedList is a palindrome or not. 2 | 3 | # Your algorithm should use constant space and 4 | # the input LinkedList should be in the original form once the algorithm is finished. 5 | # The algorithm should have O(N)O(N) time complexity where ‘N’ is the number of nodes in the LinkedList. 6 | 7 | # Example: 8 | # Input: 2 -> 4 -> 6 -> 4 -> 2 -> null 9 | # Output: true 10 | 11 | class Node: 12 | def __init__(self, value, next = None) -> None: 13 | self.value = value 14 | self.next = None 15 | 16 | def palindorme_linkedlist(head): 17 | if head is None or head.next is None: 18 | return True 19 | 20 | middle = find_middle_of_linkedlist(head) 21 | reversed_second_half = reverse_linkedlist(middle) 22 | pointer1, pointer2 = head, reversed_second_half 23 | while pointer1 is not None and pointer2 is not None: 24 | if pointer1.value != pointer2.value: 25 | break 26 | pointer1 = pointer1.next 27 | pointer2 = pointer2.next 28 | 29 | reverse_linkedlist(reversed_second_half) 30 | 31 | if pointer1 is None or pointer2 is None: 32 | return True 33 | return False 34 | 35 | def reverse_linkedlist(head): 36 | new_header = None 37 | while head is not None: 38 | pointer = head.next 39 | head.next = new_header 40 | new_header = head 41 | head = pointer 42 | return new_header 43 | 44 | def find_middle_of_linkedlist(head): 45 | slow, fast = head, head 46 | while fast is not None and fast.next is not None: 47 | slow = slow.next 48 | fast = fast.next.next 49 | return slow 50 | 51 | head = Node(1) 52 | head.next = Node(2) 53 | head.next.next = Node(3) 54 | head.next.next.next = Node(2) 55 | head.next.next.next.next = Node(1) 56 | print(palindorme_linkedlist(head)) 57 | 58 | head.next.next.next.next.next = Node(1) 59 | print(palindorme_linkedlist(head)) 60 | 61 | head2 = Node(1) 62 | head2.next = Node(2) 63 | head2.next.next = Node(2) 64 | head2.next.next.next = Node(1) 65 | print(palindorme_linkedlist(head2)) -------------------------------------------------------------------------------- /fast_slow pointers/rearrange a linkedlist.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList, 2 | # write a method to modify the LinkedList such that 3 | # the nodes from the second half of the LinkedList are inserted alternately to 4 | # the nodes from the first half in reverse order. 5 | # So if the LinkedList has nodes 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null, 6 | # your method should return 1 -> 6 -> 2 -> 5 -> 3 -> 4 -> null. 7 | 8 | # Your algorithm should not use any extra space and the input LinkedList should be modified in-place. 9 | 10 | # Example: 11 | # Input: 2 -> 4 -> 6 -> 8 -> 10 -> 12 -> null 12 | # Output: 2 -> 12 -> 4 -> 10 -> 6 -> 8 -> null 13 | 14 | # Input: 2 -> 4 -> 6 -> 8 -> 10 -> null 15 | # Output: 2 -> 10 -> 4 -> 8 -> 6 -> null 16 | 17 | # O(N) space:O(1) 18 | 19 | class Node: 20 | def __init__(self, value, next = None) -> None: 21 | self.value = value 22 | self.next = None 23 | 24 | def print_linkedlist(self): 25 | head = self 26 | while head is not None: 27 | print(str(head.value) + " ", end = '') 28 | head = head.next 29 | print() 30 | 31 | def rearrange_linkedlist(head): 32 | if head is None or head.next is None: 33 | return head 34 | 35 | middle = find_middle_of_linkedlist(head) 36 | reversed_seconf_half = reverse_linkedlist(middle) 37 | pointer1, pointer2 = head, reversed_seconf_half 38 | while pointer1 is not None and pointer2 is not None: 39 | pointer1_next = pointer1.next 40 | poineter2_next = pointer2.next 41 | pointer1.next = pointer2 42 | pointer2.next = pointer1_next 43 | pointer2 = poineter2_next 44 | pointer1 = pointer1_next 45 | 46 | if pointer1 is not None: 47 | pointer1.next = None 48 | return head 49 | 50 | def find_middle_of_linkedlist(head): 51 | slow, fast = head, head 52 | while fast is not None and fast.next is not None: 53 | slow = slow.next 54 | fast = fast.next.next 55 | return slow 56 | 57 | def reverse_linkedlist(head): 58 | new_header = None 59 | while head is not None: 60 | pointer = head.next 61 | head.next = new_header 62 | new_header = head 63 | head = pointer 64 | return new_header 65 | 66 | 67 | head = Node(2) 68 | head.next = Node(4) 69 | head.next.next = Node(6) 70 | head.next.next.next = Node(8) 71 | head.next.next.next.next = Node(10) 72 | rearrange_linkedlist(head) 73 | head.print_linkedlist() 74 | 75 | head.next.next.next.next.next = Node(12) 76 | rearrange_linkedlist(head) 77 | head.print_linkedlist() 78 | 79 | -------------------------------------------------------------------------------- /fast_slow pointers/start of linkedlist cycle.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList that contains a cycle, write a function to find the starting node of the cycle. 2 | 3 | # O(N) space:O(1) 4 | class Node: 5 | def __init__(self, value, next = None) -> None: 6 | self.value = value 7 | self.next = None 8 | 9 | def start_of_linkedlist_cycle(head): 10 | cycle_length = linkedlist_cycle(head) 11 | if cycle_length == 0: 12 | return None 13 | 14 | pointer1, pointer2 = head, head 15 | for i in range(cycle_length): 16 | pointer2 = pointer2.next 17 | 18 | while pointer1 != pointer2: 19 | pointer1 = pointer1.next 20 | pointer2 = pointer2.next 21 | 22 | return pointer1.value 23 | 24 | def linkedlist_cycle(head): 25 | slow, fast = head, head 26 | while fast is not None and fast.next is not None: 27 | slow = slow.next 28 | fast = fast.next.next 29 | if slow == fast: 30 | return caculate_linkedlist_cycle(slow) 31 | 32 | return 0 33 | 34 | def caculate_linkedlist_cycle(pointer): 35 | current = pointer 36 | cycle_length = 0 37 | while True: 38 | current = current.next 39 | cycle_length += 1 40 | if current == pointer: 41 | break 42 | 43 | return cycle_length 44 | 45 | 46 | head = Node(1) 47 | head.next = Node(2) 48 | head.next.next = Node(3) 49 | head.next.next.next = Node(4) 50 | head.next.next.next.next = Node(5) 51 | head.next.next.next.next.next = Node(6) 52 | print(start_of_linkedlist_cycle(head)) 53 | 54 | head.next.next.next.next.next.next = head.next.next 55 | print(start_of_linkedlist_cycle(head)) -------------------------------------------------------------------------------- /in-place reversal of a linkedlist/reverse a linkedlist.py: -------------------------------------------------------------------------------- 1 | # reverse a linkedlist 2 | # O(N) space:O(1) 3 | 4 | class Node: 5 | def __init__(self, value, next = None) -> None: 6 | self.value = value 7 | self.next = None 8 | 9 | def print_linkedlist(head): 10 | while head is not None: 11 | print(str(head.value) + "", end = "") 12 | head = head.next 13 | print() 14 | 15 | def reverse_linkedlist(head): 16 | if head is None or head.next is None: 17 | return head 18 | 19 | new_header = None 20 | while head is not None: 21 | pointer = head.next 22 | head.next = new_header 23 | new_header = head 24 | head = pointer 25 | 26 | return new_header 27 | 28 | head = Node(1) 29 | head.next = Node(2) 30 | head.next.next = Node(3) 31 | head.next.next.next = Node(4) 32 | head.next.next.next.next = Node(1) 33 | new_header = reverse_linkedlist(head) 34 | new_header.print_linkedlist() 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /in-place reversal of a linkedlist/reverse a sub list.py: -------------------------------------------------------------------------------- 1 | # Given the head of a LinkedList and two positions ‘p’ and ‘q’, reverse the LinkedList from position ‘p’ to ‘q’. 2 | 3 | # Example: 4 | # p = 2, q = 4, 1->2->3->4->5->null 5 | # output: 1->4->3->2->5->null 6 | 7 | # O(N) space:O(1) 8 | class Node: 9 | def __init__(self, value, next = None) -> None: 10 | self.value = value 11 | self.next = None 12 | 13 | def print_linkedlist(head): 14 | while head is not None: 15 | print(str(head.value) + "", end = "") 16 | head = head.next 17 | print() 18 | 19 | def reverse_sub_list(head, p, q): 20 | if p == q: 21 | return head 22 | 23 | current, pre_pointer = head, None 24 | count = 0 25 | while current is not None: 26 | if count == p - 1: 27 | break 28 | count += 1 29 | pre_pointer = current 30 | current = current.next 31 | 32 | count = 0 33 | new_header = None 34 | last_node_of_sub_list = current 35 | 36 | while current is not None: 37 | if count == q - p + 1: 38 | break 39 | next_pointer = current.next 40 | current.next = new_header 41 | new_header = current 42 | current = next_pointer 43 | count += 1 44 | 45 | if pre_pointer is not None: 46 | pre_pointer.next = new_header 47 | else: 48 | head = new_header 49 | 50 | last_node_of_sub_list.next = current 51 | return head 52 | 53 | 54 | head = Node(1) 55 | head.next = Node(2) 56 | head.next.next = Node(3) 57 | head.next.next.next = Node(4) 58 | head.next.next.next.next = Node(5) 59 | head = reverse_sub_list(head,2,4) 60 | head.print_linkedlist() 61 | 62 | head = Node(1) 63 | head.next = Node(2) 64 | head.next.next = Node(3) 65 | head.next.next.next = Node(4) 66 | head.next.next.next.next = Node(5) 67 | head = reverse_sub_list(head,1,5) 68 | head.print_linkedlist() 69 | 70 | # follow up: 71 | # Problem 1: Reverse the first ‘k’ elements of a given LinkedList. 72 | # Solution: This problem can be easily converted to our parent problem; 73 | # to reverse the first ‘k’ nodes of the list, we need to pass p=1 and q=k. 74 | 75 | # Problem 2: Given a LinkedList with ‘n’ nodes, reverse it based on its size in the following way: 76 | # If ‘n’ is even, reverse the list in a group of n/2 nodes. 77 | # If n is odd, keep the middle node as it is, reverse the first ‘n/2’ nodes and reverse the last ‘n/2’ nodes. 78 | # Solution: When ‘n’ is even we can perform the following steps: 79 | 80 | # Reverse first ‘n/2’ nodes: head = reverse(head, 1, n/2) 81 | # Reverse last ‘n/2’ nodes: head = reverse(head, n/2 + 1, n) 82 | # When ‘n’ is odd, our algorithm will look like: 83 | 84 | # head = reverse(head, 1, n/2) 85 | # head = reverse(head, n/2 + 2, n) 86 | # Please note the function call in the second step. We’re skipping two elements as we will be skipping the middle element. -------------------------------------------------------------------------------- /in-place reversal of a linkedlist/reverse alternating k element sub list.py: -------------------------------------------------------------------------------- 1 | # Given the head of a LinkedList and a number ‘k’, reverse every alternating ‘k’ sized sub-list starting from the head. 2 | 3 | # If, in the end, you are left with a sub-list with less than ‘k’ elements, reverse it too. 4 | 5 | # Example: 6 | # k = 3, 1->2->3->4->5->6->7->null 7 | # output: 3->2->1->4->5->6->7->null 8 | 9 | # O(N) space:O(1) 10 | class Node: 11 | def __init__(self, value, next = None) -> None: 12 | self.value = value 13 | self.next = None 14 | 15 | def print_linkedlist(self): 16 | temp = self 17 | while temp is not None: 18 | print(str(temp.value) + "", end = "") 19 | temp = temp.next 20 | print() 21 | 22 | def reverse_alternating_k_element_sub_list(head, k): 23 | if head is None or head.next is None or k <= 1: 24 | return head 25 | 26 | previous, current = None, head 27 | while True: 28 | last_node_of_previous_part = previous 29 | last_node_of_sub_list = current 30 | 31 | next_pointer = None 32 | i = 0 33 | while current is not None and i < k: 34 | next_pointer = current.next 35 | current.next = previous 36 | previous = current 37 | current = next_pointer 38 | i += 1 39 | 40 | if last_node_of_previous_part is not None: 41 | last_node_of_previous_part.next = previous 42 | else: 43 | head = previous 44 | 45 | last_node_of_sub_list.next = current 46 | 47 | i = 0 48 | while current is not None and i < k: 49 | previous = current 50 | current = current.next 51 | i += 1 52 | 53 | if current is None: 54 | break 55 | 56 | return head 57 | 58 | head = Node(1) 59 | head.next = Node(2) 60 | head.next.next = Node(3) 61 | head.next.next.next = Node(4) 62 | head.next.next.next.next = Node(5) 63 | head.next.next.next.next.next = Node(6) 64 | head.next.next.next.next.next.next = Node(7) 65 | head = reverse_alternating_k_element_sub_list(head,3) 66 | head.print_linkedlist() 67 | 68 | head = Node(1) 69 | head.next = Node(2) 70 | head.next.next = Node(3) 71 | head.next.next.next = Node(4) 72 | head.next.next.next.next = Node(5) 73 | head.next.next.next.next.next = Node(6) 74 | head.next.next.next.next.next.next = Node(7) 75 | head.next.next.next.next.next.next.next = Node(8) 76 | head = reverse_alternating_k_element_sub_list(head,2) 77 | head.print_linkedlist() -------------------------------------------------------------------------------- /in-place reversal of a linkedlist/reverse every k element sub list.py: -------------------------------------------------------------------------------- 1 | # Given the head of a LinkedList and a number ‘k’, 2 | # reverse every ‘k’ sized sub-list starting from the head. 3 | 4 | # If, in the end, you are left with a sub-list with less than ‘k’ elements, reverse it too. 5 | # Example: 6 | # k = 3, 1->2->3->4->5->6->7->null 7 | # output: 3->2->1->6->5->4->7->null 8 | 9 | # O(N) space:O(1) 10 | class Node: 11 | def __init__(self, value, next = None) -> None: 12 | self.value = value 13 | self.next = None 14 | 15 | def print_linkedlist(self): 16 | temp = self 17 | while temp is not None: 18 | print(str(temp.value) + "", end = "") 19 | temp = temp.next 20 | print() 21 | 22 | def reverse_every_k_element_sub_list(head, k): 23 | if head is None or head.next is None or k <= 1: 24 | return head 25 | 26 | current, previous = head, None 27 | while True: 28 | last_node_of_previous_part = previous 29 | last_node_of_sub_list = current 30 | next_pointer = None 31 | i = 0 32 | while current is not None and i < k: 33 | next_pointer = current.next 34 | current.next = previous 35 | previous = current 36 | current = next_pointer 37 | i += 1 38 | 39 | if last_node_of_previous_part is not None: 40 | last_node_of_previous_part.next = previous 41 | else: 42 | head = previous 43 | 44 | last_node_of_sub_list.next = current 45 | if current is None: 46 | break 47 | 48 | previous = last_node_of_sub_list 49 | 50 | return head 51 | 52 | 53 | head = Node(1) 54 | head.next = Node(2) 55 | head.next.next = Node(3) 56 | head.next.next.next = Node(4) 57 | head.next.next.next.next = Node(5) 58 | head.next.next.next.next.next = Node(6) 59 | head.next.next.next.next.next.next = Node(7) 60 | head = reverse_every_k_element_sub_list(head,3) 61 | head.print_linkedlist() 62 | 63 | -------------------------------------------------------------------------------- /in-place reversal of a linkedlist/rotate a linkedlist.py: -------------------------------------------------------------------------------- 1 | # Given the head of a Singly LinkedList and a number ‘k’, rotate the LinkedList to the right by ‘k’ nodes. 2 | 3 | # Example: 4 | # k = 3, 1->2->3->4->5->6->null 5 | # output: 4->5->6->1->2->3->null 6 | 7 | # k = 8, 1->2->3->4->5->null 8 | # output: 3->4->5->1->2->null 9 | 10 | # O(N) space:O(1) 11 | class Node: 12 | def __init__(self, value, next = None) -> None: 13 | self.value = value 14 | self.next = None 15 | 16 | def print_linkedlist(self): 17 | temp = self 18 | while temp is not None: 19 | print(str(temp.value) + "", end = "") 20 | temp = temp.next 21 | print() 22 | 23 | def rotate_linkedlist(head, k): 24 | if head is None or head.next is None or k <= 0: 25 | return head 26 | 27 | linkedlist_length = 1 28 | pointer = head 29 | sub_list_last_node = None 30 | while pointer is not None: 31 | linkedlist_length += 1 32 | sub_list_last_node = pointer 33 | pointer = pointer.next 34 | 35 | k = k % linkedlist_length 36 | if k <= 0: 37 | return head 38 | 39 | first_node = head 40 | previous = None 41 | for _ in range(k): 42 | previous = first_node 43 | first_node = first_node.next 44 | 45 | previous.next = None 46 | sub_list_last_node.next = head 47 | 48 | return first_node 49 | 50 | head = Node(1) 51 | head.next = Node(2) 52 | head.next.next = Node(3) 53 | head.next.next.next = Node(4) 54 | head.next.next.next.next = Node(5) 55 | head = rotate_linkedlist(head,8) 56 | head.print_linkedlist() 57 | 58 | head = Node(1) 59 | head.next = Node(2) 60 | head.next.next = Node(3) 61 | head.next.next.next = Node(4) 62 | head.next.next.next.next = Node(5) 63 | head.next.next.next.next.next = Node(6) 64 | head = rotate_linkedlist(head,3) 65 | head.print_linkedlist() -------------------------------------------------------------------------------- /k way merge/k pairs with largest sums.py: -------------------------------------------------------------------------------- 1 | # Given two sorted arrays in descending order, 2 | # find ‘K’ pairs with the largest sum where each pair consists of numbers from both the arrays. 3 | 4 | # Example: 5 | # Input: L1=[9, 8, 2], L2=[6, 3, 1], K=3 6 | # Output: [9, 3], [9, 6], [8, 6] 7 | # Explanation: These 3 pairs have the largest sum. No other pair has a sum larger than any of these. 8 | 9 | # O(N * M * logK) space: O(K) 10 | from heapq import * 11 | 12 | def find_k_pairs_with_largest_sum(nums1, nums2, k): 13 | minheap = [] 14 | result = [] 15 | 16 | for i in range(0, min(k, len(nums1))): 17 | for j in range(0, min(k, len(nums2))): 18 | if len(minheap) < k: 19 | heappush(minheap, (nums1[i] + nums2[j], i, j)) 20 | else: 21 | if nums1[i] + nums2[j] < minheap[0][0]: 22 | break 23 | else: 24 | heappop(minheap) 25 | heappush(minheap, (nums1[i] + nums2[j], i, j)) 26 | 27 | while minheap: 28 | _, i, j = heappop(minheap) 29 | result.append([nums1[i], nums2[j]]) 30 | 31 | return result 32 | 33 | print(find_k_pairs_with_largest_sum([9, 8, 2], [6, 3, 1], 3)) 34 | print(find_k_pairs_with_largest_sum([5, 2, 1], [2, -1], 3)) 35 | -------------------------------------------------------------------------------- /k way merge/kth smallest number in a sorted matrix.py: -------------------------------------------------------------------------------- 1 | # Given an N * N matrix where each row and column is sorted in ascending order, find the Kth smallest element in the matrix. 2 | 3 | # Example: 4 | # Input: Matrix=[ 5 | # [2, 6, 8], 6 | # [3, 7, 10], 7 | # [5, 8, 11] 8 | # ], 9 | # K=5 10 | # Output: 7 11 | # Explanation: The 5th smallest number in the matrix is 7. 12 | 13 | # O(K * logN + min(N, K)) 14 | # space: O(N) 15 | from heapq import * 16 | 17 | 18 | def kth_smallest_number_in_sorted_matrix(matrix, k): 19 | n = len(matrix) 20 | minheap = [] 21 | 22 | for i in range(min(len(matrix), k)): 23 | heappush(minheap, (matrix[i][0], 0, i)) 24 | 25 | number = -1 26 | while minheap and k > 0: 27 | number, i, row = heappop(minheap) 28 | k -= 1 29 | if i + 1 < n: 30 | heappush(minheap, (matrix[row][i + 1], i + 1, row)) 31 | 32 | return number 33 | 34 | print(kth_smallest_number_in_sorted_matrix([ 35 | [2, 6, 8], 36 | [3, 7, 10], 37 | [5, 8, 11] 38 | ], 5)) 39 | 40 | 41 | # alternative approach: binary search 42 | 43 | # O(N * log(max - min)) where ‘max’ is the largest and ‘min’ is the smallest element in the matrix 44 | # space: O(1) 45 | def kth_smallest_number_in_sorted_matrix_2(matrix, k): 46 | n = len(matrix) 47 | start, end = matrix[0][0], matrix[n - 1][n - 1] 48 | while start < end: 49 | mid = start + (end - start) // 2 50 | smaller, larger = matrix[0][0], matrix[n - 1][n - 1] 51 | count, smaller, larger = count_less_equal(matrix, mid, smaller, larger) 52 | 53 | if count == k: 54 | return smaller 55 | elif count < k: 56 | start = larger 57 | else: 58 | end = smaller 59 | 60 | return start 61 | 62 | def count_less_equal(matrix, mid, smaller, larger): 63 | count, n = 0, len(matrix) 64 | row, col = n - 1, 0 65 | while row >= 0 and col < n: 66 | if matrix[row][col] > mid: 67 | larger = min(larger, matrix[row][col]) 68 | row -= 1 69 | else: 70 | smaller = max(smaller, matrix[row][col]) 71 | count += row + 1 72 | col += 1 73 | 74 | return count, smaller, larger 75 | 76 | print(kth_smallest_number_in_sorted_matrix_2([ 77 | [2, 6, 8], 78 | [3, 7, 10], 79 | [5, 8, 11] 80 | ], 5)) -------------------------------------------------------------------------------- /k way merge/kth smallest number in m sorted lists.py: -------------------------------------------------------------------------------- 1 | # Given ‘M’ sorted arrays, find the K’th smallest number among all the arrays. 2 | 3 | # Example: 4 | # Input: L1=[2, 6, 8], L2=[3, 6, 7], L3=[1, 3, 4], K=5 5 | # Output: 4 6 | # Explanation: The 5th smallest number among all the arrays is 4, this can be verified from the merged 7 | # list of all the arrays: [1, 2, 3, 3, 4, 6, 6, 7, 8] 8 | 9 | # O(K * logM) space: O(M) 10 | from heapq import * 11 | 12 | def kth_smallest_number_in_sorted_lists(arrs, k): 13 | minheap = [] 14 | for i in range(len(arrs)): 15 | heappush(minheap, (arrs[i][0], 0, i)) 16 | 17 | number = 0 18 | while minheap and k > 0: 19 | number, i, list_index = heappop(minheap) 20 | k -= 1 21 | if i + 1 < len(arrs[list_index]): 22 | heappush(minheap, (arrs[list_index][i + 1], i + 1, list_index)) 23 | 24 | return number 25 | 26 | print(kth_smallest_number_in_sorted_lists([[2, 6, 8], [3, 6, 7], [1, 3, 4]], 5)) 27 | 28 | -------------------------------------------------------------------------------- /k way merge/merge k sorted lists.py: -------------------------------------------------------------------------------- 1 | # Given an array of ‘K’ sorted LinkedLists, merge them into one sorted list. 2 | 3 | # Example: 4 | # Input: L1=[2, 6, 8], L2=[3, 6, 7], L3=[1, 3, 4] 5 | # Output: [1, 2, 3, 3, 4, 6, 6, 7, 8] 6 | 7 | # O(N * logK) where ‘N’ is the total number of elements in all the ‘K’ input arrays. 8 | # space: O(K) 9 | from heapq import * 10 | 11 | class ListNode: 12 | def __init__(self, value) -> None: 13 | self.value = value 14 | self.next = None 15 | 16 | def __lt__(self, other): 17 | return self.value < other.value 18 | 19 | def merge_k_sorted_lists(lists): 20 | minheap = [] 21 | for root in lists: 22 | if root: 23 | heappush(minheap, root) 24 | 25 | head, tail = None, None 26 | while minheap: 27 | node = heappop(minheap) 28 | if head is None: 29 | head = tail = node 30 | else: 31 | tail.next = node 32 | tail = tail.next 33 | 34 | if node.next: 35 | heappush(minheap, node.next) 36 | 37 | return head 38 | 39 | l1 = ListNode(2) 40 | l1.next = ListNode(6) 41 | l1.next.next = ListNode(8) 42 | 43 | l2 = ListNode(3) 44 | l2.next = ListNode(6) 45 | l2.next.next = ListNode(7) 46 | 47 | l3 = ListNode(1) 48 | l3.next = ListNode(3) 49 | l3.next.next = ListNode(4) 50 | 51 | 52 | result = merge_k_sorted_lists([l1, l2, l3]) 53 | while result: 54 | print(str(result.value) + ",", end = '') 55 | result = result.next 56 | print() 57 | -------------------------------------------------------------------------------- /k way merge/smallest number range.py: -------------------------------------------------------------------------------- 1 | # Given ‘M’ sorted arrays, 2 | # find the smallest range that includes at least one number from each of the ‘M’ lists. 3 | 4 | # Example: 5 | # Input: L1=[1, 5, 8], L2=[4, 12], L3=[7, 8, 10] 6 | # Output: [4, 7] 7 | # Explanation: The range [4, 7] includes 5 from L1, 4 from L2 and 7 from L3. 8 | 9 | from heapq import * 10 | import math 11 | 12 | def find_smallest_number_range(lists): 13 | range_start, range_end = 0, math.inf 14 | current_max = -math.inf 15 | minheap = [] 16 | 17 | for i in range(len(lists)): 18 | heappush(minheap, (lists[i][0], 0, i)) 19 | current_max = max(current_max, lists[i][0]) 20 | 21 | while len(minheap) == len(lists): 22 | num, i, list_index = heappop(minheap) 23 | if range_end - range_start > current_max - num: 24 | range_start = num 25 | range_end = current_max 26 | 27 | if i < len(lists[list_index]) - 1: 28 | heappush(minheap, (lists[list_index][i + 1], i + 1, list_index)) 29 | current_max = max(current_max, lists[list_index][i + 1]) 30 | 31 | return [range_start, range_end] 32 | 33 | print(find_smallest_number_range([[1, 5, 8], [4, 12], [7, 8, 10]])) 34 | print(find_smallest_number_range([[1, 9], [4, 12], [7, 10, 16]])) -------------------------------------------------------------------------------- /merge intervals/conflicting appointment.py: -------------------------------------------------------------------------------- 1 | # Given an array of intervals representing ‘N’ appointments, 2 | # find out if a person can attend all the appointments. 3 | 4 | # Example: 5 | # Appointments: [[1,4], [2,5], [7,9]] 6 | # Output: false 7 | # Explanation: Since [1,4] and [2,5] overlap, a person cannot attend both of these appointments. 8 | 9 | # O(NlogN) 10 | # space:O(N) (sort uses Timsort, which needs O(N) space) 11 | def is_conflicting_appointment(arr): 12 | start, end = 0, 1 13 | arr.sort(key = lambda x: x[0]) 14 | 15 | for i in range(1, len(arr)): 16 | if arr[i-1][end] > arr[i][start]: 17 | return False 18 | 19 | return True 20 | 21 | print(is_conflicting_appointment([[1,4], [2,5], [7,9]])) 22 | print(is_conflicting_appointment([[6,7], [2,4], [8,12]])) 23 | print(is_conflicting_appointment([[4,5], [2,3], [3,6]])) 24 | 25 | # follow up: Given a list of appointments, find all the conflicting appointments. 26 | # Example: 27 | # Appointments: [[4,5], [2,3], [3,6], [5,7], [7,8]] 28 | # Output: 29 | # [4,5] and [3,6] conflict. 30 | # [3,6] and [5,7] conflict. 31 | 32 | # O(N^2) space:O(N) 33 | def conflicting_appointment(arr): 34 | start, end = 0, 1 35 | arr.sort(key = lambda x: x[0]) 36 | result = [] 37 | 38 | for i in range(len(arr)): 39 | for j in range(i+1, len(arr)): 40 | if arr[i][end] > arr[j][start]: 41 | result.append([arr[i], arr[j]]) 42 | 43 | return result 44 | 45 | 46 | print(conflicting_appointment([[1,4], [2,5], [7,9]])) 47 | print(conflicting_appointment([[6,7], [2,4], [8,12]])) 48 | print(conflicting_appointment([[4,5], [2,3], [3,6]])) 49 | print(conflicting_appointment([[4,5], [2,3], [3,6], [5,7], [7,8]])) -------------------------------------------------------------------------------- /merge intervals/employee free time.py: -------------------------------------------------------------------------------- 1 | # For ‘K’ employees, we are given a list of intervals representing the working hours of each employee. 2 | # Our goal is to find out if there is a free interval that is common to all employees. 3 | # You can assume that each list of employee working hours is sorted on the start time. 4 | 5 | # Example: 6 | # Input: Employee Working Hours=[[[1,3], [5,6]], [[2,3], [6,8]]] 7 | # Output: [3,5] 8 | # Explanation: Both the employess are free between [3,5]. 9 | 10 | # O(NlogK) (N is the number of intervals, k is the number of employees) 11 | # space: O(K) 12 | from heapq import * 13 | 14 | class Interval: 15 | def __init__(self, start, end) -> None: 16 | self.start = start 17 | self.end = end 18 | 19 | def print_interval(self): 20 | print("[" + str(self.start) + "," + str(self.end) + "]", end= '') 21 | 22 | 23 | class EmployeeInterval: 24 | def __init__(self, interval, employee_index, interval_index) -> None: 25 | self.interval = interval 26 | self.employee_index = employee_index 27 | self.interval_index = interval_index 28 | 29 | def __lt__(self, other): 30 | return self.interval.start < other.interval.start 31 | 32 | 33 | def employee_free_time(arr): 34 | min_heap = [] 35 | result = [] 36 | employee_number = len(arr) 37 | for i in range(employee_number): 38 | heappush(min_heap, EmployeeInterval(Interval(arr[i][0][0], arr[i][0][1]), i, 0)) 39 | 40 | previous_interval = min_heap[0].interval 41 | 42 | while min_heap: 43 | queue_top = heappop(min_heap) 44 | if previous_interval.end < queue_top.interval.start: 45 | result.append(Interval(previous_interval.end, queue_top.interval.start)) 46 | previous_interval = queue_top.interval 47 | else: 48 | if previous_interval.end < queue_top.interval.end: 49 | previous_interval = queue_top.interval 50 | 51 | employee_schedule = arr[queue_top.employee_index] 52 | if len(employee_schedule) > queue_top.interval_index + 1: 53 | heappush(min_heap, EmployeeInterval(Interval(employee_schedule[queue_top.interval_index + 1][0], 54 | employee_schedule[queue_top.interval_index + 1][1]), queue_top.employee_index, queue_top.interval_index + 1)) 55 | 56 | return result 57 | 58 | result = employee_free_time([[[1,3], [5,6]], [[2,3], [6,8]]]) 59 | for item in result: 60 | item.print_interval() 61 | print() 62 | 63 | result = employee_free_time([[[1,3], [9,12]], [[2,4]], [[6,8]]]) 64 | for item in result: 65 | item.print_interval() 66 | print() 67 | 68 | result = employee_free_time([[[1,3]], [[2,4]], [[3,5], [7,9]]]) 69 | for item in result: 70 | item.print_interval() 71 | print() -------------------------------------------------------------------------------- /merge intervals/insert interval.py: -------------------------------------------------------------------------------- 1 | # Given a list of non-overlapping intervals sorted by their start time, 2 | # insert a given interval at the correct position and 3 | # merge all necessary intervals to produce a list that has only mutually exclusive intervals. 4 | 5 | # Example: 6 | # Input: Intervals=[[1,3], [5,7], [8,12]], New Interval=[4,6] 7 | # Output: [[1,3], [4,7], [8,12]] 8 | # Explanation: After insertion, since [4,6] overlaps with [5,7], we merged them into one [4,7]. 9 | 10 | # O(N) 11 | # space: O(N) 12 | 13 | def insert_interval(intervals, new_interval): 14 | merged_intervals = [] 15 | i, start, end = 0, 0, 1 16 | 17 | while i < len(intervals) and intervals[i][end] < new_interval[start]: 18 | merged_intervals.append(intervals[i]) 19 | i += 1 20 | 21 | while i < len(intervals) and intervals[i][start] <= new_interval[end]: 22 | new_interval[start] = min(intervals[i][start], new_interval[start]) 23 | new_interval[end] = max(intervals[i][end], new_interval[end]) 24 | i += 1 25 | 26 | merged_intervals.append(new_interval) 27 | 28 | while i < len(intervals): 29 | merged_intervals.append(intervals[i]) 30 | i += 1 31 | 32 | return merged_intervals 33 | 34 | print(insert_interval([[1,3], [5,7], [8,12]], [4,6])) 35 | print(insert_interval([[1,3], [5,7], [8,12]], [4,10])) 36 | print(insert_interval([[2,3],[5,7]], [1,4])) -------------------------------------------------------------------------------- /merge intervals/interval intersection.py: -------------------------------------------------------------------------------- 1 | # Given two lists of intervals, find the intersection of these two lists. 2 | # Each list consists of disjoint intervals sorted on their start time. 3 | 4 | # Example: 5 | # Input: arr1=[[1, 3], [5, 6], [7, 9]], arr2=[[2, 3], [5, 7]] 6 | # Output: [2, 3], [5, 6], [7, 7] 7 | # Explanation: The output list contains the common intervals between the two lists. 8 | 9 | def interval_intersection(arr1, arr2): 10 | i, j = 0, 0 11 | start, end = 0, 1 12 | result = [] 13 | while i < len(arr1) and j < len(arr2): 14 | if ((arr1[i][start] >= arr2[j][start] and arr1[i][start] <= arr2[j][end]) 15 | or (arr2[j][start] >= arr1[i][start] and arr2[j][start] <= arr1[i][end])): 16 | result.append([max(arr1[i][start], arr2[j][start]), min(arr1[i][end], arr2[j][end])]) 17 | 18 | if arr1[i][end] < arr2[j][end]: 19 | i += 1 20 | else: 21 | j += 1 22 | 23 | return result 24 | 25 | print(interval_intersection([[1, 3], [5, 6], [7, 9]], [[2, 3], [5, 7]])) 26 | print(interval_intersection([[1, 3], [5, 7], [9, 12]], [[5, 10]])) 27 | 28 | -------------------------------------------------------------------------------- /merge intervals/maximum cpu load.py: -------------------------------------------------------------------------------- 1 | # We are given a list of Jobs. Each job has a Start time, an End time, and a CPU load when it is running. Our goal is to find the maximum CPU load at any time if all the jobs are running on the same machine. 2 | 3 | # Example: 4 | # Jobs: [[1,4,3], [2,5,4], [7,9,6]] 5 | # Output: 7 6 | # Explanation: Since [1,4,3] and [2,5,4] overlap, their maximum CPU load (3+4=7) will be when both the 7 | # jobs are running at the same time i.e., during the time interval (2,4). 8 | 9 | from heapq import heappop, heappush 10 | 11 | 12 | class Job: 13 | def __init__(self, start, end, cpu_load) -> None: 14 | self.start = start 15 | self.end = end 16 | self.cpu_load = cpu_load 17 | 18 | def __lt__(self, other): 19 | return self.end < other.end 20 | 21 | def max_cpu_load(arr): 22 | jobs = [] 23 | for item in arr: 24 | jobs.append(Job(item[0], item[1], item[2])) 25 | 26 | max_cpu_load = 0 27 | current_cpu_load = 0 28 | min_heap = [] 29 | jobs.sort(key = lambda x: x.start) 30 | 31 | for job in jobs: 32 | while len(min_heap) > 0 and job.start >= min_heap[0].end: 33 | current_cpu_load -= min_heap[0].cpu_load 34 | heappop(min_heap) 35 | 36 | heappush(min_heap, job) 37 | current_cpu_load += job.cpu_load 38 | max_cpu_load = max(max_cpu_load, current_cpu_load) 39 | 40 | return max_cpu_load 41 | 42 | print(max_cpu_load([[1,4,3], [2,5,4], [7,9,6]])) 43 | print(max_cpu_load([[6,7,10], [2,4,11], [8,12,15]])) 44 | print(max_cpu_load([[1,4,2], [2,4,1], [3,6,5]])) 45 | 46 | print(max_cpu_load([[1,4,3], [4,5,2], [2,3,0]])) 47 | 48 | -------------------------------------------------------------------------------- /merge intervals/merge intervals.py: -------------------------------------------------------------------------------- 1 | # Given a list of intervals, 2 | # merge all the overlapping intervals to produce a list that has only mutually exclusive intervals. 3 | 4 | # Example: 5 | # Intervals: [[1,4], [2,5], [7,9]] 6 | # Output: [[1,5], [7,9]] 7 | # Explanation: Since the first two intervals [1,4] and [2,5] overlap, we merged them into one [1,5]. 8 | 9 | 10 | # O(N) for merge O(NlogN) for sorting -> O(NlogN) 11 | # space:O(N) 12 | class Interval: 13 | def __init__(self, start, end) -> None: 14 | self.start = start 15 | self.end = end 16 | 17 | def print_interval(self): 18 | print("[" + str(self.start) + "," + str(self.end) + "]", end='') 19 | 20 | 21 | def merge_intervals(arr): 22 | intervals = [] 23 | for i in arr: 24 | intervals.append(Interval(i[0], i[1])) 25 | 26 | if len(intervals) < 2: 27 | return intervals 28 | 29 | intervals.sort(key = lambda x: x.start) 30 | 31 | merged_intervals = [] 32 | start = intervals[0].start 33 | end = intervals[0].end 34 | 35 | for i in range(1, len(intervals)): 36 | interval = intervals[i] 37 | if interval.start <= end: 38 | end = max(interval.end, end) 39 | else: 40 | merged_intervals.append(Interval(start, end)) 41 | start = interval.start 42 | end = interval.end 43 | 44 | merged_intervals.append(Interval(start, end)) 45 | return merged_intervals 46 | 47 | for i in merge_intervals([[1,4], [2,5], [7,9]]): 48 | i.print_interval() 49 | print() 50 | 51 | for i in merge_intervals([[6,7], [2,4], [5,9]]): 52 | i.print_interval() 53 | print() 54 | 55 | for i in merge_intervals([[1,4], [2,6], [3,5]]): 56 | i.print_interval() 57 | print() -------------------------------------------------------------------------------- /merge intervals/minimum meeting room.py: -------------------------------------------------------------------------------- 1 | # Given a list of intervals representing the start and end time of ‘N’ meetings, 2 | # find the minimum number of rooms required to hold all the meetings. 3 | 4 | # Example: 5 | # Meetings: [[4,5], [2,3], [2,4], [3,5]] 6 | # Output: 2 7 | # Explanation: We will need one room for [2,3] and [3,5], and another room for [2,4] and [4,5]. 8 | 9 | # O(NlogN) (sort :O(NlogN) and push/pop O(logN) for each iteration, overall iterate for N times) 10 | # space: O(N) 11 | 12 | from heapq import * 13 | 14 | class Meeting: 15 | def __init__(self, start, end) -> None: 16 | self.start = start 17 | self.end = end 18 | 19 | def __lt__(self, other): 20 | return self.end < other.end 21 | 22 | def min_meeting_rooms(arr): 23 | meetings = [] 24 | for i in arr: 25 | meetings.append(Meeting(i[0], i[1])) 26 | 27 | meetings.sort(key = lambda x: x.start) 28 | 29 | min_rooms = 0 30 | min_heap = [] 31 | 32 | for meeting in meetings: 33 | while len(min_heap) > 0 and meeting.start >= min_heap[0].end: 34 | heappop(min_heap) 35 | 36 | heappush(min_heap, meeting) 37 | 38 | min_rooms = max(min_rooms, len(min_heap)) 39 | 40 | return min_rooms 41 | 42 | print(min_meeting_rooms([[1,4], [2,5], [7,9]])) 43 | print(min_meeting_rooms([[6,7], [2,4], [8,12]])) 44 | print(min_meeting_rooms([[1,4], [2,3], [3,6]])) 45 | print(min_meeting_rooms([[4,5], [2,3], [2,4], [3,5]])) 46 | 47 | # follow up : 48 | # Problem 1: Given a list of intervals, find the point where the maximum number of intervals overlap. 49 | # ??? don't know how to solve 50 | 51 | # Problem 2: Given a list of intervals representing the arrival and departure times of trains to a train station, 52 | # our goal is to find the minimum number of platforms required for the train station so that no train has to wait. 53 | # Problem 2 is equal to the minimum meeting room -------------------------------------------------------------------------------- /modified binary search/bitonic array maximum.py: -------------------------------------------------------------------------------- 1 | # Find the maximum value in a given Bitonic array. 2 | # An array is considered bitonic if it is monotonically increasing and then monotonically decreasing. 3 | # Monotonically increasing or decreasing means that for any index i in the array arr[i] != arr[i+1]. 4 | 5 | # Example: 6 | # Input: [1, 3, 8, 12, 4, 2] 7 | # Output: 12 8 | # Explanation: The maximum number in the input bitonic array is '12'. 9 | 10 | # O(logN) space:O(1) 11 | def bitonic_array_maximum(arr): 12 | if len(arr) == 0: 13 | return None 14 | 15 | start, end = 0, len(arr) - 1 16 | while start < end: 17 | mid = start + (end - start) // 2 18 | if arr[mid] > arr[mid + 1]: 19 | end = mid 20 | else: 21 | start = mid + 1 22 | 23 | return arr[start] 24 | 25 | print(bitonic_array_maximum([1, 3, 8, 12, 4, 2])) 26 | print(bitonic_array_maximum([3, 8, 3, 1])) 27 | print(bitonic_array_maximum([8, 3, 1])) 28 | print(bitonic_array_maximum([1, 2, 3, 10])) -------------------------------------------------------------------------------- /modified binary search/ceiling of a number.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers sorted in an ascending order, find the ceiling of a given number ‘key’. 2 | # The ceiling of the ‘key’ will be the smallest element in the given array greater than or equal to the ‘key’. 3 | # Write a function to return the index of the ceiling of the ‘key’. If there isn’t any ceiling return -1. 4 | 5 | # Example: 6 | # Input: [4, 6, 10], key = 6 7 | # Output: 1 8 | # Explanation: The smallest number greater than or equal to '6' is '6' having index '1'. 9 | 10 | # O(logN) space: O(1) 11 | def ceiling_number(arr, key): 12 | if len(arr) == 0 or arr[-1] < key: 13 | return -1 14 | 15 | start, end = 0, len(arr) - 1 16 | while start <= end: 17 | mid = start + (end - start) // 2 18 | if key == arr[mid]: 19 | return mid 20 | elif key < arr[mid]: 21 | end = mid - 1 22 | else: 23 | start = mid + 1 24 | 25 | return start 26 | 27 | print(ceiling_number([4, 6, 10], 6)) 28 | print(ceiling_number([1, 3, 8, 10, 15], 12)) 29 | print(ceiling_number([4, 6, 10], 17)) 30 | print(ceiling_number([4, 6, 10], -1)) 31 | 32 | # follow up: 33 | # Given an array of numbers sorted in ascending order, find the floor of a given number ‘key’. 34 | # The floor of the ‘key’ will be the biggest element in the given array smaller than or equal to the ‘key’ 35 | # Write a function to return the index of the floor of the ‘key’. If there isn’t a floor, return -1. 36 | 37 | # Example: 38 | # Input: [1, 3, 8, 10, 15], key = 12 39 | # Output: 3 40 | # Explanation: The biggest number smaller than or equal to '12' is '10' having index '3'. 41 | 42 | # O(logN) space: O(1) 43 | def floor_number(arr, key): 44 | if len(arr) == 0 or arr[0] > key: 45 | return -1 46 | 47 | start, end = 0, len(arr) - 1 48 | while start <= end: 49 | mid = start + (end - start) // 2 50 | if key == arr[mid]: 51 | return mid 52 | elif key < arr[mid]: 53 | end = mid - 1 54 | else: 55 | start = mid + 1 56 | 57 | return end 58 | 59 | print(floor_number([4, 6, 10], 6)) 60 | print(floor_number([1, 3, 8, 10, 15], 12)) 61 | print(floor_number([4, 6, 10], 17)) 62 | print(floor_number([4, 6, 10], -1)) 63 | -------------------------------------------------------------------------------- /modified binary search/minimum difference element.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers sorted in ascending order, 2 | # find the element in the array that has the minimum difference with the given ‘key’. 3 | 4 | # Example: 5 | # Input: [4, 6, 10], key = 7 6 | # Output: 6 7 | # Explanation: The difference between the key '7' and '6' is minimum than any other number in the array 8 | 9 | # O(logN) space: O(1) 10 | def minimum_difference_element(arr, key): 11 | if len(arr) == 0: 12 | return None 13 | 14 | if arr[0] > key: 15 | return arr[0] 16 | 17 | if arr[-1] < key: 18 | return arr[-1] 19 | 20 | start, end = 0, len(arr) - 1 21 | 22 | while start <= end: 23 | mid = start + (end - start) // 2 24 | if arr[mid] == key: 25 | return arr[mid] 26 | elif arr[mid] < key: 27 | start = mid + 1 28 | else: 29 | end = mid - 1 30 | 31 | if arr[start] - key < key - arr[end]: 32 | return arr[start] 33 | else: 34 | return arr[end] 35 | 36 | print(minimum_difference_element([4, 6, 10], 7)) 37 | print(minimum_difference_element([4, 6, 10], 4)) 38 | print(minimum_difference_element([4, 6, 10], 17)) 39 | print(minimum_difference_element([1, 3, 8, 10, 15], 12)) -------------------------------------------------------------------------------- /modified binary search/next letter.py: -------------------------------------------------------------------------------- 1 | # Given an array of lowercase letters sorted in ascending order, 2 | # find the smallest letter in the given array greater than a given ‘key’. 3 | # Assume the given array is a circular list, 4 | # which means that the last letter is assumed to be connected with the first letter. 5 | # This also means that the smallest letter in the given array is greater than the last letter of the array 6 | # and is also the first letter of the array. 7 | 8 | # Write a function to return the next letter of the given ‘key’. 9 | 10 | # Example 1: 11 | 12 | # Input: ['a', 'c', 'f', 'h'], key = 'f' 13 | # Output: 'h' 14 | # Explanation: The smallest letter greater than 'f' is 'h' in the given array. 15 | 16 | # Input: ['a', 'c', 'f', 'h'], key = 'm' 17 | # Output: 'a' 18 | # Explanation: As the array is assumed to be circular, the smallest letter greater than 'm' is 'a'. 19 | 20 | # O(logN) space:O(1) 21 | def next_letter(arr, key): 22 | if len(arr) == 0: 23 | return None 24 | 25 | if key < arr[0] or key > arr[-1]: 26 | return arr[0] 27 | 28 | start, end = 0, len(arr) - 1 29 | while start <= end: 30 | mid = start + (end - start) // 2 31 | if key == mid: 32 | return arr[mid] 33 | elif key < arr[mid]: 34 | end = mid - 1 35 | else: 36 | start = mid + 1 37 | 38 | return arr[start] 39 | 40 | print(next_letter(['a', 'c', 'f', 'h'], 'f')) 41 | print(next_letter(['a', 'c', 'f', 'h'], 'b')) 42 | print(next_letter(['a', 'c', 'f', 'h'], 'm')) -------------------------------------------------------------------------------- /modified binary search/number range.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers sorted in ascending order, find the range of a given number ‘key’. 2 | # The range of the ‘key’ will be the first and last position of the ‘key’ in the array. 3 | # Write a function to return the range of the ‘key’. If the ‘key’ is not present return [-1, -1]. 4 | 5 | # Example: 6 | # Input: [4, 6, 6, 6, 9], key = 6 7 | # Output: [1, 3] 8 | 9 | def number_range(arr, key): 10 | result = [-1, -1] 11 | if len(arr) == 0 or arr[0] > key or arr[-1] < key: 12 | return result 13 | 14 | start, end = 0, len(arr) - 1 15 | while start <= end: 16 | mid = start + (end - start) // 2 17 | 18 | if arr[mid] == key: 19 | temp = mid 20 | while temp >= 0 and arr[temp] == key: 21 | temp -= 1 22 | result[0] = temp + 1 23 | 24 | temp = mid 25 | while temp < len(arr) and arr[temp] == key: 26 | temp += 1 27 | result[1] = temp - 1 28 | 29 | return result 30 | 31 | elif arr[mid] < key: 32 | start = mid + 1 33 | else: 34 | end = mid - 1 35 | 36 | return result 37 | 38 | print(number_range([4, 6, 6, 6, 9], 6)) 39 | print(number_range([1, 3, 8, 10, 15], 10)) 40 | print(number_range([1, 3, 8, 10, 15], 12)) 41 | 42 | -------------------------------------------------------------------------------- /modified binary search/order agnostic binary search.py: -------------------------------------------------------------------------------- 1 | # Given a sorted array of numbers, find if a given number ‘key’ is present in the array. 2 | # Though we know that the array is sorted, we don’t know if it’s sorted in ascending or descending order. 3 | # You should assume that the array can have duplicates. 4 | # Write a function to return the index of the ‘key’ if it is present in the array, otherwise return -1. 5 | 6 | # Example: 7 | # Input: [4, 6, 10], key = 10 8 | # Output: 2 9 | 10 | # O(logN) space: O(1) 11 | def order_agnostic_binary_search(arr, key): 12 | if len(arr) == 0: 13 | return -1 14 | 15 | start, end = 0, len(arr) - 1 16 | is_ascend = arr[start] < arr[end] 17 | while start <= end: 18 | mid = start + (end - start) // 2 19 | if key == arr[mid]: 20 | return mid 21 | 22 | if is_ascend: 23 | if key < arr[mid]: 24 | end = mid - 1 25 | else: 26 | start = mid + 1 27 | else: 28 | if key < arr[mid]: 29 | start = mid + 1 30 | else: 31 | end = mid - 1 32 | 33 | return -1 34 | 35 | print(order_agnostic_binary_search([4, 6, 10], 10)) 36 | print(order_agnostic_binary_search([1, 2, 3, 4, 5, 6, 7], 5)) 37 | print(order_agnostic_binary_search([10, 6, 4], 10)) 38 | print(order_agnostic_binary_search([10, 6, 4], 5)) -------------------------------------------------------------------------------- /modified binary search/rotation count.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers which is sorted in ascending order and is rotated ‘k’ times around a pivot, find ‘k’. 2 | # You can assume that the array does not have any duplicates. 3 | 4 | # Example: 5 | # Input: [10, 15, 1, 3, 8] 6 | # Output: 2 7 | # Explanation: The array has been rotated 2 times. 8 | 9 | # Input: [1, 3, 8, 10] 10 | # Output: 0 11 | # Explanation: The array has been not been rotated. 12 | 13 | # O(logN) space: O(1) 14 | def count_rotation(arr): 15 | if len(arr) == 0: 16 | return 0 17 | 18 | start, end = 0, len(arr) - 1 19 | while start < end: 20 | mid = start + (end - start) // 2 21 | 22 | if mid < end and arr[mid] > arr[mid + 1]: 23 | return mid + 1 24 | 25 | if mid > start and arr[mid - 1] > arr[mid]: 26 | return mid 27 | 28 | if arr[start] < arr[mid]: 29 | start = mid + 1 30 | else: 31 | end = mid - 1 32 | 33 | return 0 34 | 35 | print(count_rotation([10, 15, 1, 3, 8])) 36 | print(count_rotation([4, 5, 7, 9, 10, -1, 2])) 37 | print(count_rotation([1, 3, 8, 10])) 38 | 39 | # follow up: 40 | # How do we find the rotation count of a sorted and rotated array that has duplicates too? 41 | # The above code will fail on the following example! 42 | 43 | # Example: 44 | # Input: [3, 3, 7, 3] 45 | # Output: 3 46 | # Explanation: The array has been rotated 3 times 47 | 48 | print(count_rotation([3, 3, 7, 3])) 49 | 50 | # O(logN) space: O(1) 51 | def count_rotation_duplicate(arr): 52 | if len(arr) == 0: 53 | return 0 54 | 55 | start, end = 0, len(arr) - 1 56 | while start < end: 57 | mid = start + (end - start) // 2 58 | 59 | if mid < end and arr[mid] > arr[mid + 1]: 60 | return mid + 1 61 | 62 | if mid > start and arr[mid - 1] > arr[mid]: 63 | return mid 64 | if arr[start] == arr[mid] and arr[mid] == arr[end]: 65 | if arr[start] > arr[start + 1]: 66 | return start + 1 67 | start += 1 68 | if arr[end - 1] > arr[end]: 69 | return end 70 | end -= 1 71 | elif arr[start] < arr[mid] or (arr[start] == arr[mid] and arr[mid] > arr[end]): 72 | start = mid + 1 73 | else: 74 | end = mid - 1 75 | 76 | return 0 77 | 78 | print(count_rotation_duplicate([3, 3, 7, 3])) 79 | -------------------------------------------------------------------------------- /modified binary search/search bitonic array.py: -------------------------------------------------------------------------------- 1 | # Given a Bitonic array, find if a given ‘key’ is present in it. 2 | # An array is considered bitonic if it is monotonically increasing and then monotonically decreasing. 3 | # Monotonically increasing or decreasing means that for any index i in the array arr[i] != arr[i+1]. 4 | 5 | # Write a function to return the index of the ‘key’. If the ‘key’ is not present, return -1. 6 | 7 | # Example: 8 | 9 | # Input: [1, 3, 8, 4, 3], key=4 10 | # Output: 3 11 | 12 | # O(logN) space:O(1) 13 | def search_bitonic_array(arr, key): 14 | if len(arr) == 0: 15 | return -1 16 | 17 | max_index = find_max(arr) 18 | key_index = binary_search(arr, key, 0, max_index) 19 | if key_index != -1: 20 | return key_index 21 | return binary_search(arr, key, max_index + 1, len(arr) - 1) 22 | 23 | 24 | def find_max(arr): 25 | start, end = 0, len(arr) - 1 26 | while start < end: 27 | mid = start + (end - start) // 2 28 | if arr[mid] > arr[mid + 1]: 29 | end = mid 30 | else: 31 | start = mid + 1 32 | 33 | return start 34 | 35 | def binary_search(arr, key, start, end): 36 | is_ascend = arr[start] < arr[end] 37 | while start <= end: 38 | mid = start + (end - start) // 2 39 | if arr[mid] == key: 40 | return mid 41 | if is_ascend: 42 | if arr[mid] < key: 43 | start = mid + 1 44 | else: 45 | end = mid - 1 46 | else: 47 | if arr[mid] > key: 48 | start = mid + 1 49 | else: 50 | end = mid - 1 51 | 52 | return -1 53 | 54 | print(search_bitonic_array([1, 3, 8, 4, 3], 4)) 55 | print(search_bitonic_array([3, 8, 3, 1], 8)) 56 | print(search_bitonic_array([1, 2, 3, 10], 10)) 57 | print(search_bitonic_array([10, 9, 3, 1], 8)) -------------------------------------------------------------------------------- /modified binary search/search in a sorted infinite array.py: -------------------------------------------------------------------------------- 1 | # Given an infinite sorted array (or an array with unknown size), 2 | # find if a given number ‘key’ is present in the array. 3 | # Write a function to return the index of the ‘key’ if it is present in the array, otherwise return -1. 4 | 5 | # Since it is not possible to define an array with infinite (unknown) size, 6 | # you will be provided with an interface ArrayReader to read elements of the array. 7 | # ArrayReader.get(index) will return the number at index; 8 | # if the array’s size is smaller than the index, it will return Integer.MAX_VALUE. 9 | # Example 1: 10 | 11 | # Input: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30], key = 16 12 | # Output: 6 13 | # Explanation: The key is present at index '6' in the array. 14 | 15 | 16 | # O(logN) space: O(1) 17 | import math 18 | 19 | class ArrayReader: 20 | def __init__(self, arr) -> None: 21 | self.arr = arr 22 | 23 | def get(self, index): 24 | if index >= len(self.arr): 25 | return math.inf 26 | return self.arr[index] 27 | 28 | 29 | def search_in_infinite_array(reader, key): 30 | start, end = 0, 1 31 | while reader.get(end) < key: 32 | new_start = end + 1 33 | end += (end - start + 1) * 2 34 | start = new_start 35 | 36 | return binary_search(reader, start, end, key) 37 | 38 | def binary_search(reader, start, end, key): 39 | while start <= end: 40 | mid = start + (end - start) // 2 41 | if reader.get(mid) == key: 42 | return mid 43 | elif reader.get(mid) < key: 44 | start = mid + 1 45 | else: 46 | end = mid - 1 47 | 48 | return -1 49 | 50 | reader = ArrayReader([4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]) 51 | print(search_in_infinite_array(reader, 16)) 52 | print(search_in_infinite_array(reader, 11)) 53 | reader = ArrayReader([1, 3, 8, 10, 15]) 54 | print(search_in_infinite_array(reader, 15)) 55 | print(search_in_infinite_array(reader, 200)) 56 | -------------------------------------------------------------------------------- /modified binary search/search in rotated array.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers which is sorted in ascending order and also rotated by some arbitrary number, 2 | # find if a given ‘key’ is present in it. 3 | 4 | # Write a function to return the index of the ‘key’ in the rotated array. 5 | # If the ‘key’ is not present, return -1. You can assume that the given array does not have any duplicates. 6 | 7 | # Example: 8 | # Input: [10, 15, 1, 3, 8], key = 15 9 | # Output: 1 10 | # Explanation: '15' is present in the array at index '1'. 11 | 12 | # O(logN) space:O(1) 13 | def search_in_rotated_array(arr, key): 14 | if len(arr) == 0: 15 | return -1 16 | 17 | start, end = 0, len(arr) - 1 18 | while start <= end: 19 | mid = start + (end - start) // 2 20 | if arr[mid] == key: 21 | return mid 22 | 23 | if arr[start] <= arr[mid]: 24 | if key >= arr[start] and key < arr[mid]: 25 | end = mid - 1 26 | else: 27 | start = mid + 1 28 | else: 29 | if key > arr[mid] and key <= arr[end]: 30 | start = mid + 1 31 | else: 32 | end = mid - 1 33 | 34 | return -1 35 | 36 | print(search_in_rotated_array([10, 15, 1, 3, 8], 15)) 37 | print(search_in_rotated_array([4, 5, 7, 9, 10, -1, 2], 10)) 38 | 39 | # follow up: How do we search in a sorted and rotated array that also has duplicates? 40 | print(search_in_rotated_array([3, 7, 3, 3, 3], 7)) 41 | 42 | 43 | # best: O(logN) worst:O(N) space:O(1) 44 | def search_in_rotated_duplicate_array(arr, key): 45 | if len(arr) == 0: 46 | return -1 47 | 48 | start, end = 0, len(arr) - 1 49 | while start <= end: 50 | mid = start + (end - start) // 2 51 | if arr[mid] == key: 52 | return mid 53 | 54 | if arr[start] == arr[mid] and arr[end] == arr[mid]: 55 | start += 1 56 | end -= 1 57 | 58 | if arr[start] <= arr[mid]: 59 | if key >= arr[start] and key < arr[mid]: 60 | end = mid - 1 61 | else: 62 | start = mid + 1 63 | else: 64 | if key > arr[mid] and key <= arr[end]: 65 | start = mid + 1 66 | else: 67 | end = mid - 1 68 | 69 | return -1 70 | 71 | print(search_in_rotated_duplicate_array([3, 7, 3, 3, 3], 7)) -------------------------------------------------------------------------------- /scripts/finddir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | find "$1" -type d -not -path "$1/.git*" -not -path "$1/scripts*" | xargs -I{} sh -c 'find "$1" -not -path "$1/.git*" -not -path "$1/scripts*" -not -name "*.md" -not -name "LICENSE" -not -path "$1" -maxdepth 1 -printf "[%f](<%f>)\n\n" > "$1/index.md"' -- {} -------------------------------------------------------------------------------- /scripts/fixlinks.sh: -------------------------------------------------------------------------------- 1 | #1/bin/bash 2 | 3 | find "$1" -type f -name "index.md" -exec sed -i 's/.py>/.py.html>/g' {} \; -------------------------------------------------------------------------------- /scripts/generate_md.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find "$1" -type f -name "*.py" -exec sh -c 'cp "$1" "$1".md' -- {} \; 4 | find "$1" -type f -name "*.py.md" | while read file; do sed -i '1s/^/```python\n/' "$file"; done 5 | find "$1" -type f -name "*.py.md" | while read file; do sed -i -e '$a```\n' "$file"; done 6 | -------------------------------------------------------------------------------- /sliding window/fruits into baskets.py: -------------------------------------------------------------------------------- 1 | # Given an array of characters where each character represents a fruit tree, 2 | # you are given two baskets and your goal is to put maximum number of fruits in each basket. 3 | # The only restriction is that each basket can have only one type of fruit. 4 | # You can start with any tree, but once you have started you can’t skip a tree. 5 | # You will pick one fruit from each tree until you cannot, 6 | # i.e., you will stop when you have to pick from a third fruit type. 7 | 8 | # Example: 9 | # Input: Fruit=['A', 'B', 'C', 'A', 'C'] 10 | # Output: 3 11 | # Explanation: We can put 2 'C' in one basket and one 'A' in the other from the subarray ['C', 'A', 'C'] 12 | 13 | # sliding window: O(N) space: O(1) 14 | def fruits_into_two_baskets(fruits): 15 | result = 0 16 | fruit_hashmap = dict() 17 | window_start = 0 18 | for window_end in range(len(fruits)): 19 | right_fruit = fruits[window_end] 20 | if right_fruit not in fruit_hashmap: 21 | fruit_hashmap[right_fruit] = 0 22 | fruit_hashmap[right_fruit] += 1 23 | while(len(fruit_hashmap)) > 2: 24 | left_fruit = fruits[window_start] 25 | fruit_hashmap[left_fruit] -= 1 26 | if fruit_hashmap[left_fruit] == 0: 27 | del fruit_hashmap[left_fruit] 28 | window_start += 1 29 | result = max(result, window_end - window_start +1) 30 | return result 31 | 32 | print(fruits_into_two_baskets(['A', 'B', 'C', 'A', 'C'])) 33 | print(fruits_into_two_baskets(['A', 'B', 'C', 'B', 'B', 'C'])) -------------------------------------------------------------------------------- /sliding window/intro.py: -------------------------------------------------------------------------------- 1 | # Given an array, find the average of all contiguous subarrays of size ‘K’ in it. 2 | # Example: 3 | # Array: [1, 3, 2, 6, -1, 4, 1, 8, 2], K=5 4 | # Output: [2.2, 2.8, 2.4, 3.6, 2.8] 5 | 6 | # brute force 7 | # time complexity : O(NK) 8 | 9 | def find_average_of_subarrays(subarray, size): 10 | output = [] 11 | for i in range(len(subarray)-size+1): 12 | average = sum(subarray[i: i+size])/size 13 | output.append(average) 14 | return output 15 | 16 | subarray = [1, 3, 2, 6, -1, 4, 1, 8, 2] 17 | size = 5 18 | print(find_average_of_subarrays(subarray, size)) 19 | subarray = [1, 3, 2, 6, -1] 20 | print(find_average_of_subarrays(subarray, size)) 21 | subarray = [1, 3, 2, 6] 22 | print(find_average_of_subarrays(subarray, size)) 23 | 24 | # sliding window 25 | # time complexity: O(N) 26 | 27 | def find_average_of_subarrays_2(subarray, size): 28 | output = [] 29 | result = sum(subarray[0:size]) 30 | output.append(result/size) 31 | for i in range(len(subarray)-size): 32 | result -= subarray[i] 33 | result += subarray[i+size] 34 | output.append(result/size) 35 | return output 36 | 37 | subarray = [1, 3, 2, 6, -1, 4, 1, 8, 2] 38 | size = 5 39 | print(find_average_of_subarrays_2(subarray, size)) 40 | subarray = [1, 3, 2, 6, -1] 41 | print(find_average_of_subarrays_2(subarray, size)) 42 | 43 | # mark answer 44 | def find_average_of_subarrays_3(subarray, size): 45 | output = [] 46 | windowSum, windowStart = 0, 0 47 | for windowEnd in range(len(subarray)): 48 | windowSum += subarray[windowEnd] 49 | if windowEnd >= size-1: # greater or equal to 50 | output.append(windowSum/size) 51 | windowSum -= subarray[windowStart] 52 | windowStart += 1 53 | return output 54 | 55 | subarray = [1, 3, 2, 6, -1, 4, 1, 8, 2] 56 | size = 5 57 | print(find_average_of_subarrays_3(subarray, size)) 58 | subarray = [1, 3, 2, 6, -1] 59 | print(find_average_of_subarrays_3(subarray, size)) -------------------------------------------------------------------------------- /sliding window/longest substring with K distinct characters.py: -------------------------------------------------------------------------------- 1 | # Given a string, find the length of the longest substring in it with no more than K distinct characters. 2 | # Example: 3 | # Input: String="araaci", K=2 4 | # Output: 4 5 | # Explanation: The longest substring with no more than '2' distinct characters is "araa". 6 | 7 | # Input: String="cbbebi", K=3 8 | # Output: 5 9 | # Explanation: The longest substrings with no more than '3' distinct characters are "cbbeb" & "bbebi". 10 | 11 | # sliding window: O(N) space: O(K) 12 | def longest_substring_with_k_distinct_characters(str, k): 13 | window_start = 0 14 | result = 0 15 | char_frequency = dict() 16 | for window_end in range(len(str)): 17 | right_char = str[window_end] 18 | if right_char not in char_frequency: 19 | char_frequency[right_char] = 0 20 | char_frequency[right_char] += 1 21 | while(len(char_frequency)) > k: 22 | left_char = str[window_start] 23 | char_frequency[left_char] -= 1 24 | if char_frequency[left_char] == 0: 25 | del char_frequency[left_char] 26 | window_start += 1 27 | result = max(result, window_end - window_start + 1) 28 | return result 29 | 30 | print(longest_substring_with_k_distinct_characters("araaci", 2)) 31 | print(longest_substring_with_k_distinct_characters("araaci", 1)) 32 | print(longest_substring_with_k_distinct_characters("cbbebi", 3)) -------------------------------------------------------------------------------- /sliding window/longest_substring_with_ones_after_replacement.py: -------------------------------------------------------------------------------- 1 | # Given an array containing 0s and 1s, if you are allowed to replace no more than ‘k’ 0s with 1s, 2 | # find the length of the longest contiguous subarray having all 1s. 3 | 4 | # Example: 5 | # Input: Array=[0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1], k=2 6 | # Output: 6 7 | # Explanation: Replace the '0' at index 5 and 8 to have the longest contiguous subarray of 1s having length 6. 8 | 9 | # sliding window:O(N) space:O(1) 10 | def longest_substring_with_ones_after_k_replacement(arr, k): 11 | max_one_repeat_times = 0 12 | result = 0 13 | window_start = 0 14 | for window_end in range(len(arr)): 15 | right_char = arr[window_end] 16 | if right_char == 1: 17 | max_one_repeat_times += 1 18 | if window_end - window_start + 1 - max_one_repeat_times > k: 19 | left_char = arr[window_start] 20 | if left_char == 1: 21 | max_one_repeat_times -= 1 22 | window_start += 1 23 | result = max(result, window_end - window_start + 1) 24 | return result 25 | 26 | print(longest_substring_with_ones_after_k_replacement([0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1], 2)) 27 | print(longest_substring_with_ones_after_k_replacement([0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1], 3)) 28 | -------------------------------------------------------------------------------- /sliding window/longest_substring_with_same_letters_after_replacement.py: -------------------------------------------------------------------------------- 1 | # Given a string with lowercase letters only, if you are allowed to replace no more than ‘k’ letters with any letter, 2 | # find the length of the longest substring having the same letters after replacement. 3 | 4 | # Input: String="abbcb", k=1 5 | # Output: 4 6 | # Explanation: Replace the 'c' with 'b' to have a longest repeating substring "bbbb". 7 | 8 | # sliding window:O(N) space:O(1) 9 | 10 | def longest_substring_with_same_letters_after_k_replacement(str, k): 11 | char_frequency = dict() 12 | window_start = 0 13 | max_repeat_letter_count = 0 14 | result = 0 15 | for window_end in range(len(str)): 16 | right_char = str[window_end] 17 | if right_char not in char_frequency: 18 | char_frequency[right_char] = 0 19 | char_frequency[right_char] += 1 20 | max_repeat_letter_count = max(max_repeat_letter_count, char_frequency[right_char]) 21 | if window_end - window_start + 1 - max_repeat_letter_count > k: 22 | left_char = str[window_start] 23 | char_frequency[left_char] -= 1 24 | window_start += 1 25 | result = max(result, window_end - window_start + 1) 26 | return result 27 | 28 | print(longest_substring_with_same_letters_after_k_replacement("abbcb", 1)) -------------------------------------------------------------------------------- /sliding window/maximum sum subarrays of size k.py: -------------------------------------------------------------------------------- 1 | # Given an array of positive numbers and a positive number ‘k’, 2 | # find the maximum sum of any contiguous subarray of size ‘k’. 3 | # Example: 4 | # Input: [2, 1, 5, 1, 3, 2], k=3 5 | # Output: 9 6 | # Explanation: Subarray with maximum sum is [5, 1, 3]. 7 | 8 | # brute force: O(NK) 9 | # sliding window: O(N) space complexity : O(1) 10 | 11 | def max_sum_subarray_of_k(array, size): 12 | maxSum = 0 13 | windowSum, windowStart = 0, 0 14 | for windowEnd in range(len(array)): 15 | windowSum += array[windowEnd] 16 | if windowEnd >= size-1: 17 | if maxSum < windowSum: 18 | maxSum = windowSum 19 | windowSum -= array[windowStart] 20 | windowStart +=1 21 | return maxSum 22 | 23 | array = [2, 1, 5, 1, 3, 2] 24 | k = 3 25 | print(max_sum_subarray_of_k(array, k)) 26 | -------------------------------------------------------------------------------- /sliding window/no-repeat substring.py: -------------------------------------------------------------------------------- 1 | # Given a string, find the length of the longest substring which has no repeating characters. 2 | # Example: 3 | # Input: String="aabccbb" 4 | # Output: 3 5 | # Explanation: The longest substring without any repeating characters is "abc". 6 | 7 | # sliding window: O(N) space: O(K) (k is the number of distinct characters in the string)-> O(1) 8 | def no_repeat_substring(str): 9 | result = 0 10 | window_start = 0 11 | char_index_map = dict() 12 | for window_end in range(len(str)): 13 | right_char = str[window_end] 14 | if right_char in char_index_map: 15 | window_start = max(window_start, char_index_map[right_char] + 1) 16 | char_index_map[right_char] = window_end 17 | result = max(result, window_end-window_start+1) 18 | return result 19 | 20 | print(no_repeat_substring("aabccbb")) -------------------------------------------------------------------------------- /sliding window/permutation in a string.py: -------------------------------------------------------------------------------- 1 | # Permutation is defined as the re-arranging of the characters of the string 2 | # Given a string and a pattern, find out if the string contains any permutation of the pattern. 3 | 4 | # Example: 5 | # Input: String="oidbcaf", Pattern="abc" 6 | # Output: true 7 | # Explanation: The string contains "bca" which is a permutation of the given pattern. 8 | 9 | # Input: String="odicf", Pattern="dc" 10 | # Output: false 11 | # Explanation: No permutation of the pattern is present in the given string as a substring. 12 | 13 | # sliding window:O(N + M) (M is the number of characters in pattern string) 14 | # space:O(K)-> O(M)(M is the worst case) (k is the number of distinct letters in string pattern) 15 | 16 | def permutation_in_a_string(str, pattern): 17 | char_pattern = dict() 18 | for char in pattern: 19 | if char not in char_pattern: 20 | char_pattern[char] = 0 21 | char_pattern[char] += 1 22 | window_start = 0 23 | char_frequency = dict() 24 | for window_end in range(len(str)): 25 | right_char = str[window_end] 26 | if right_char not in char_frequency: 27 | char_frequency[right_char] = 0 28 | char_frequency[right_char] += 1 29 | if window_end - window_start + 1 > len(pattern): 30 | left_char = str[window_start] 31 | char_frequency[left_char] -= 1 32 | if char_frequency[left_char] == 0: 33 | del char_frequency[left_char] 34 | window_start += 1 35 | if char_frequency == char_pattern: 36 | return True 37 | return False 38 | 39 | print(permutation_in_a_string("oidbcaf", "abc")) 40 | print(permutation_in_a_string("odicf", "dc")) 41 | print(permutation_in_a_string("bcdxabcdy", "bcdyabcdx")) 42 | print(permutation_in_a_string("aaacb", "abc")) 43 | 44 | def permutation_in_a_string_2(str, pattern): 45 | window_start, matched = 0, 0 46 | char_frequency = dict() 47 | for char in pattern: 48 | if char not in char_frequency: 49 | char_frequency[char] = 0 50 | char_frequency[char] += 1 51 | 52 | for window_end in range(len(str)): 53 | right_char = str[window_end] 54 | if right_char in char_frequency: 55 | char_frequency[right_char] -= 1 56 | if char_frequency[right_char] == 0: 57 | matched += 1 58 | 59 | if matched == len(char_frequency): 60 | return True 61 | 62 | if window_end >= len(pattern) - 1: 63 | left_char = str[window_start] 64 | window_start += 1 65 | if left_char in char_frequency: 66 | if char_frequency[left_char] == 0: 67 | matched -= 1 68 | char_frequency[left_char] += 1 69 | 70 | return False 71 | 72 | print(permutation_in_a_string_2("oidbcaf", "abc")) 73 | print(permutation_in_a_string_2("odicf", "dc")) 74 | print(permutation_in_a_string_2("bcdxabcdy", "bcdyabcdx")) 75 | print(permutation_in_a_string_2("aaacb", "abc")) -------------------------------------------------------------------------------- /sliding window/smallest subarray with a given sum.py: -------------------------------------------------------------------------------- 1 | # Given an array of positive numbers and a positive number ‘S’, 2 | # find the length of the smallest contiguous subarray whose sum is greater than or equal to ‘S’. 3 | # Return 0, if no such subarray exists. 4 | 5 | # Example: 6 | # Input: [2, 1, 5, 2, 3, 2], S=7 7 | # Output: 2 8 | # Explanation: The smallest subarray with a sum great than or equal to '7' is [5, 2]. 9 | 10 | # Input: [2, 1, 5, 2, 8], S=7 11 | # Output: 1 12 | # Explanation: The smallest subarray with a sum greater than or equal to '7' is [8]. 13 | 14 | # brute force: O(N^2) 15 | # sliding window: O(N) space complexity : O(1) 16 | 17 | import math 18 | def smallest_subarray_with_given_sum(array, sum): 19 | result = math.inf 20 | windowSum, windowStart = 0, 0 21 | for windowEnd in range(len(array)): 22 | windowSum += array[windowEnd] 23 | while windowSum >= sum: 24 | if result >= windowEnd - windowStart + 1: 25 | result = windowEnd - windowStart + 1 26 | windowSum -= array[windowStart] 27 | windowStart += 1 28 | if result == math.inf: 29 | return 0 30 | return result 31 | 32 | print(smallest_subarray_with_given_sum([2, 1, 5, 2, 8], 7)) 33 | print(smallest_subarray_with_given_sum([2, 1, 5, 2, 3, 2], 7)) 34 | print(smallest_subarray_with_given_sum([3, 4, 1, 1, 6], 8)) 35 | -------------------------------------------------------------------------------- /sliding window/smallest window containing substring.py: -------------------------------------------------------------------------------- 1 | # Given a string and a pattern, 2 | # find the smallest substring in the given string which has all the characters of the given pattern. 3 | 4 | # Example: 5 | # Input: String="aabdec", Pattern="abc" 6 | # Output: "abdec" 7 | # Explanation: The smallest substring having all characters of the pattern is "abdec" 8 | 9 | # sliding window:O(N + M) (M is the number of characters in pattern string) 10 | # space:O(K)-> O(M)(M is the worst case) (k is the number of distinct letters in string pattern) 11 | 12 | def smallest_window_containing_substring(str, pattern): 13 | window_start, matched, substr_start = 0, 0, 0 14 | min_length = len(str) + 1 15 | char_pattern = dict() 16 | 17 | for char in pattern: 18 | if char not in char_pattern: 19 | char_pattern[char] = 0 20 | char_pattern[char] += 1 21 | 22 | for window_end in range(len(str)): 23 | right_char = str[window_end] 24 | if right_char in char_pattern: 25 | char_pattern[right_char] -= 1 26 | if char_pattern[right_char] >= 0: 27 | matched += 1 28 | 29 | while matched == len(pattern): 30 | if min_length > window_end - window_start + 1: 31 | min_length = window_end - window_start + 1 32 | substr_start = window_start 33 | 34 | left_char = str[window_start] 35 | window_start += 1 36 | if left_char in char_pattern: 37 | if char_pattern[left_char] == 0: 38 | matched -= 1 39 | char_pattern[left_char] += 1 40 | 41 | if min_length > len(str): 42 | return "" 43 | return str[substr_start:substr_start+min_length] 44 | 45 | print(smallest_window_containing_substring("aabdec","abc")) 46 | print(smallest_window_containing_substring("abdabca","abc")) 47 | print(smallest_window_containing_substring("adcad","abc")) 48 | -------------------------------------------------------------------------------- /sliding window/string anagrams.py: -------------------------------------------------------------------------------- 1 | # Given a string and a pattern, find all anagrams of the pattern in the given string. 2 | # Anagram is actually a Permutation of a string. 3 | 4 | # Example: 5 | # Input: String="ppqp", Pattern="pq" 6 | # Output: [1, 2] 7 | # Explanation: The two anagrams of the pattern in the given string are "pq" and "qp". 8 | 9 | # Input: String="abbcabc", Pattern="abc" 10 | # Output: [2, 3, 4] 11 | # Explanation: The three anagrams of the pattern in the given string are "bca", "cab", and "abc". 12 | 13 | # sliding window:O(N + M) (M is the number of characters in pattern string) 14 | # space:O(K)-> O(M)(M is the worst case) (k is the number of distinct letters in string pattern) 15 | 16 | def string_anagram(str, pattern): 17 | window_start, matched = 0, 0 18 | result = [] 19 | char_pattern = dict() 20 | 21 | for char in pattern: 22 | if char not in char_pattern: 23 | char_pattern[char] = 0 24 | char_pattern[char] += 1 25 | 26 | for window_end in range(len(str)): 27 | right_char = str[window_end] 28 | if right_char in char_pattern: 29 | char_pattern[right_char] -= 1 30 | if char_pattern[right_char] == 0: 31 | matched += 1 32 | 33 | if matched == len(char_pattern): 34 | result.append(window_start) 35 | 36 | if window_end >= len(pattern) -1: 37 | left_char = str[window_start] 38 | window_start += 1 39 | if left_char in char_pattern: 40 | if char_pattern[left_char] == 0: 41 | matched -= 1 42 | char_pattern[left_char] += 1 43 | return result 44 | 45 | print(string_anagram("ppqp","pq")) 46 | print(string_anagram("abbcabc","abc")) -------------------------------------------------------------------------------- /sliding window/word concatenation.py: -------------------------------------------------------------------------------- 1 | # Given a string and a list of words, find all the starting indices of substrings in the given string 2 | # that are a concatenation of all the given words exactly once without any overlapping of words. 3 | # It is given that all words are of the same length. 4 | 5 | # Example: 6 | # Input: String="catfoxcat", Words=["cat", "fox"] 7 | # Output: [0, 3] 8 | # Explanation: The two substring containing both the words are "catfox" & "foxcat". 9 | 10 | # sliding window: O(N*M*len) (where ‘N’ is the number of characters in the given string, 11 | # ‘M’ is the total number of words, and ‘Len’ is the length of a word.) 12 | # space: O(M + N) (hashMap + result array) 13 | 14 | def word_concatenation(str, words): 15 | if len(words) == 0 or len(words[0]) == 0: 16 | return [] 17 | 18 | result = [] 19 | word_pattern = dict() 20 | word_length = len(words[0]) 21 | word_count = len(words) 22 | 23 | for word in words: 24 | if word not in word_pattern: 25 | word_pattern[word] = 0 26 | word_pattern[word] += 1 27 | 28 | for window_start in range(len(str)-word_count*word_length+1): 29 | word_seen = dict() 30 | for j in range(word_count): 31 | next_word_index = window_start + j * word_length 32 | word = str[next_word_index:next_word_index+word_length] 33 | if word not in word_pattern: 34 | break 35 | 36 | if word not in word_seen: 37 | word_seen[word] = 0 38 | word_seen[word] += 1 39 | 40 | if word_seen[word] > word_pattern[word]: 41 | break 42 | 43 | if j + 1 == word_count: 44 | result.append(window_start) 45 | 46 | return result 47 | 48 | print(word_concatenation("catfoxcat", ["cat", "fox"])) 49 | print(word_concatenation("catcatfoxfox", ["cat", "fox"])) -------------------------------------------------------------------------------- /subsets/balanced parentheses.py: -------------------------------------------------------------------------------- 1 | # For a given number ‘N’, write a function to generate all combination of ‘N’ pairs of balanced parentheses. 2 | 3 | # Example: 4 | # Input: N=2 5 | # Output: (()), ()() 6 | # Input: N=3 7 | # Output: ((())), (()()), (())(), ()(()), ()()() 8 | 9 | # O(N*2^N) 10 | # While processing each element, we do need to concatenate the current string with ( or ). 11 | # This operation will take O(N) 12 | # in the worst case, it is equivalent to a binary tree, where each node will have two children. 13 | # This means that we will have 2^N leaf nodes 14 | # space : O(N * 2^N) 15 | from collections import deque 16 | 17 | class Parentheses: 18 | def __init__(self, str, open_count, close_count) -> None: 19 | self.str = str 20 | self.open_count = open_count 21 | self.close_count = close_count 22 | 23 | def generate_valid_parentheses(num): 24 | result = [] 25 | queue = deque() 26 | queue.append(Parentheses("", 0, 0)) 27 | while queue: 28 | ps = queue.popleft() 29 | if ps.open_count == num and ps.close_count == num: 30 | result.append(ps.str) 31 | else: 32 | if ps.open_count < num: 33 | queue.append(Parentheses(ps.str + "(", ps.open_count + 1, ps.close_count)) 34 | 35 | if ps.open_count > ps.close_count: 36 | queue.append(Parentheses(ps.str + ")", ps.open_count, ps.close_count + 1)) 37 | 38 | return result 39 | 40 | # recursive 41 | def generate_valid_parentheses_2(num): 42 | result = [] 43 | parenthese_str = [0 for _ in range(2 * num)] 44 | generate_valid_parenthese_recursive(num, 0, 0, 0, parenthese_str, result) 45 | return result 46 | 47 | def generate_valid_parenthese_recursive(num, open_count, close_count, index, parenthese_str, result): 48 | if open_count == num and close_count == num: 49 | result.append(''.join(parenthese_str)) 50 | else: 51 | if open_count < num: 52 | parenthese_str[index] = '(' 53 | generate_valid_parenthese_recursive(num, open_count + 1, close_count, index + 1, parenthese_str, result) 54 | 55 | if open_count > close_count: 56 | parenthese_str[index] = ')' 57 | generate_valid_parenthese_recursive(num, open_count, close_count + 1, index + 1, parenthese_str, result) 58 | 59 | 60 | print(generate_valid_parentheses(3)) 61 | print(generate_valid_parentheses_2(3)) 62 | -------------------------------------------------------------------------------- /subsets/evaluate expression.py: -------------------------------------------------------------------------------- 1 | # Given an expression containing digits and operations (+, -, *), 2 | # find all possible ways in which the expression can be evaluated by grouping the numbers and operators using parentheses. 3 | 4 | # Example: 5 | # Input: "1+2*3" 6 | # Output: 7, 9 7 | # Explanation: 1+(2*3) => 7 and (1+2)*3 => 9 8 | 9 | def evaluate_expression_result(input_str): 10 | return evaluate_expression({}, input_str) 11 | 12 | # O(N * 2^N) space: O(N * 2^N) 13 | def evaluate_expression(map, input_str): 14 | if input_str in map: 15 | return map[input_str] 16 | 17 | result = [] 18 | if '+' not in input_str and '-' not in input_str and '*' not in input_str: 19 | result.append(int(input_str)) 20 | else: 21 | for i in range(len(input_str)): 22 | char = input_str[i] 23 | if not char.isdigit(): 24 | left_part = evaluate_expression(map, input_str[0:i]) 25 | right_part = evaluate_expression(map, input_str[i+1:]) 26 | for part1 in left_part: 27 | for part2 in right_part: 28 | if char == '+': 29 | result.append(part1 + part2) 30 | elif char == '-': 31 | result.append(part1 - part2) 32 | elif char == '*': 33 | result.append(part1 * part2) 34 | 35 | map[input_str] = result 36 | return result 37 | 38 | print(evaluate_expression_result("1+2*3")) 39 | print(evaluate_expression_result("2*3-4-5")) -------------------------------------------------------------------------------- /subsets/permutations.py: -------------------------------------------------------------------------------- 1 | # Given a set of distinct numbers, find all of its permutations. 2 | 3 | # Permutation is defined as the re-arranging of the elements of the set. 4 | # For example, {1, 2, 3} has the following six permutations: 5 | # {1, 2, 3} 6 | # {1, 3, 2} 7 | # {2, 1, 3} 8 | # {2, 3, 1} 9 | # {3, 1, 2} 10 | # {3, 2, 1} 11 | # If a set has ‘n’ distinct elements it will have n!n! permutations. 12 | 13 | # Example: 14 | # Input: [1,3,5] 15 | # Output: [1,3,5], [1,5,3], [3,1,5], [3,5,1], [5,1,3], [5,3,1] 16 | 17 | # O(N * N!) space: O(N * N!) 18 | from collections import deque 19 | 20 | def permutations(arr): 21 | num_length = len(arr) 22 | result = [] 23 | permutations = deque() 24 | permutations.append([]) 25 | for current_num in arr: 26 | n = len(permutations) 27 | for _ in range(n): 28 | old_permutation = permutations.popleft() 29 | for j in range(len(old_permutation) + 1): 30 | new_permutataion = list(old_permutation) 31 | new_permutataion.insert(j, current_num) 32 | if len(new_permutataion) == num_length: 33 | result.append(new_permutataion) 34 | else: 35 | permutations.append(new_permutataion) 36 | 37 | return result 38 | 39 | print(permutations([1, 3, 5])) 40 | 41 | def recursive_permutation(arr): 42 | result = [] 43 | generate_permutations_recursive(arr, 0, [], result) 44 | return result 45 | 46 | def generate_permutations_recursive(arr, index, current_permutation, result): 47 | if index == len(arr): 48 | result.append(current_permutation) 49 | else: 50 | for i in range(len(current_permutation) + 1): 51 | new_permutation = list(current_permutation) 52 | new_permutation.insert(i, arr[index]) 53 | generate_permutations_recursive(arr, index + 1, new_permutation, result) 54 | 55 | 56 | print(recursive_permutation([1, 3, 5])) 57 | -------------------------------------------------------------------------------- /subsets/string permutations by changing case.py: -------------------------------------------------------------------------------- 1 | # Given a string, find all of its permutations preserving the character sequence but changing case. 2 | 3 | # Example: 4 | # Input: "ad52" 5 | # Output: "ad52", "Ad52", "aD52", "AD52" 6 | 7 | # O(N * 2^N) space: O(N * 2^N) 8 | def string_permutation(str): 9 | permutations = [] 10 | permutations.append(str) 11 | for i in range(len(str)): 12 | if str[i].isalpha(): 13 | n = len(permutations) 14 | for j in range(n): 15 | character = list(permutations[j]) 16 | character[i] = character[i].swapcase() 17 | permutations.append(''.join(character)) 18 | 19 | return permutations 20 | 21 | print(string_permutation("ad52")) 22 | 23 | def find_letter_case_string_permutations(str): 24 | result = [] 25 | generate_permutations_recursive(str, 0, [], result) 26 | return result 27 | 28 | def generate_permutations_recursive(str, index, current_permutation, result): 29 | if index == len(str): 30 | result.append(''.join(current_permutation)) 31 | else: 32 | if str[index].isalpha(): 33 | for i in range(2): 34 | new_permutation = list(current_permutation) 35 | if i == 0: 36 | new_permutation.append(str[index].lower()) 37 | else: 38 | new_permutation.append(str[index].upper()) 39 | generate_permutations_recursive(str, index + 1, new_permutation, result) 40 | else: 41 | new_permutation = list(current_permutation) 42 | new_permutation.append(str[index]) 43 | generate_permutations_recursive(str, index + 1, new_permutation, result) 44 | 45 | print(find_letter_case_string_permutations("ad52")) 46 | -------------------------------------------------------------------------------- /subsets/structurally unique binary search trees.py: -------------------------------------------------------------------------------- 1 | # Given a number ‘n’, 2 | # write a function to return all structurally unique Binary Search Trees (BST) that can store values 1 to ‘n’? 3 | 4 | # Example 1: 5 | # Input: 3 6 | # Output: 5 7 | # Explanation: Here are all the structurally unique BSTs storing all numbers from 1 to 3 8 | 9 | class TreeNode: 10 | def __init__(self, value) -> None: 11 | self.value = value 12 | self.left, self.right = None, None 13 | 14 | # time: O(N^2) space: O(N) 15 | def find_unique_trees(n): 16 | return find_unique_trees_recursive({}, n) 17 | 18 | 19 | def find_unique_trees_recursive(map, n): 20 | if n in map: 21 | return map[n] 22 | 23 | if n == 0 or n == 1: 24 | return 1 25 | 26 | result = 0 27 | for i in range(n): 28 | result += find_unique_trees(i) * find_unique_trees(n - i - 1) 29 | 30 | return result 31 | 32 | print(find_unique_trees(2)) 33 | print(find_unique_trees(3)) 34 | 35 | # O(N * 2^N) space: O(N * 2^N) 36 | 37 | def draw_unique_binary_search_trees(n): 38 | if n <= 0: 39 | return [] 40 | return draw_unique_binary_search_trees_recursive(1, n) 41 | 42 | def draw_unique_binary_search_trees_recursive(start, end): 43 | result = [] 44 | if start > end: 45 | result.append(None) 46 | return result 47 | 48 | for i in range(start, end + 1): 49 | left_part = draw_unique_binary_search_trees_recursive(start, i - 1) 50 | right_part = draw_unique_binary_search_trees_recursive(i + 1, end) 51 | 52 | for left in left_part: 53 | for right in right_part: 54 | root = TreeNode(i) 55 | root.left = left 56 | root.right = right 57 | result.append(root) 58 | 59 | return result 60 | 61 | print(len(draw_unique_binary_search_trees(2))) 62 | print(len(draw_unique_binary_search_trees(3))) -------------------------------------------------------------------------------- /subsets/subsets with duplicates.py: -------------------------------------------------------------------------------- 1 | # Given a set of numbers that might contain duplicates, find all of its distinct subsets. 2 | # Example: 3 | # Input: [1, 3, 3] 4 | # Output: [], [1], [3], [1,3], [3,3], [1,3,3] 5 | 6 | # O(2^N) space: O(2^N) 7 | def find_subsets_with_duplicates(arr): 8 | arr.sort() 9 | subsets = [] 10 | subsets.append([]) 11 | start_index, end_index = 0, 0 12 | for i in range(len(arr)): 13 | start_index = 0 14 | if i > 0 and arr[i] == arr[i - 1]: 15 | start_index = end_index + 1 16 | 17 | end_index = len(subsets) - 1 18 | for j in range(start_index, end_index + 1): 19 | subset = list(subsets[j]) 20 | subset.append(arr[i]) 21 | subsets.append(subset) 22 | 23 | return subsets 24 | 25 | print(find_subsets_with_duplicates([1, 3, 3])) 26 | print(find_subsets_with_duplicates([1, 5, 5, 3, 3])) -------------------------------------------------------------------------------- /subsets/subsets.py: -------------------------------------------------------------------------------- 1 | # Given a set with distinct elements, find all of its distinct subsets. 2 | 3 | # Example: 4 | # Input: [1, 3] 5 | # Output: [], [1], [3], [1,3] 6 | 7 | # O(2^N) in each step, the number of subsets doubles as we add each element to all the existing subsets 8 | # space: O(2^N) 9 | def find_subsets(arr): 10 | subsets = [] 11 | subsets.append([]) 12 | for current_num in arr: 13 | n = len(subsets) 14 | for i in range(n): 15 | subset = list(subsets[i]) # create a new subset 16 | subset.append(current_num) 17 | subsets.append(subset) 18 | 19 | return subsets 20 | 21 | print(find_subsets([1, 3])) 22 | print(find_subsets([1, 3, 5])) -------------------------------------------------------------------------------- /subsets/unique generalized abbreviations.py: -------------------------------------------------------------------------------- 1 | # Given a word, write a function to generate all of its unique generalized abbreviations. 2 | # Generalized abbreviation of a word can be generated by 3 | # replacing each substring of the word by the count of characters in the substring. 4 | # Take the example of “ab” which has four substrings: “”, “a”, “b”, and “ab”. 5 | # After replacing these substrings in the actual word by the count of characters 6 | # we get all the generalized abbreviations: “ab”, “1b”, “a1”, and “2”. 7 | 8 | # Example 1: 9 | # Input: "BAT" 10 | # Output: "BAT", "BA1", "B1T", "B2", "1AT", "1A1", "2T", "3" 11 | 12 | # O(N * 2^N) space: O(N * 2^N) 13 | from collections import deque 14 | 15 | class Abbrevations: 16 | def __init__(self, str, start, count) -> None: 17 | self.str = str 18 | self.start = start 19 | self.count = count 20 | 21 | def unique_generalized_abbrevations(word): 22 | result = [] 23 | queue = deque() 24 | queue.append(Abbrevations(list(), 0, 0)) 25 | while queue: 26 | ab_word = queue.popleft() 27 | if ab_word.start == len(word): 28 | if ab_word.count != 0: 29 | ab_word.str.append(str(ab_word.count)) 30 | result.append(''.join(ab_word.str)) 31 | else: 32 | queue.append(Abbrevations(list(ab_word.str), ab_word.start + 1, ab_word.count + 1)) 33 | 34 | if ab_word.count != 0: 35 | ab_word.str.append(str(ab_word.count)) 36 | 37 | new_word = list(ab_word.str) 38 | 39 | new_word.append(word[ab_word.start]) 40 | queue.append(Abbrevations(new_word, ab_word.start + 1, 0)) 41 | 42 | return result 43 | 44 | def unique_generalized_abbrevations_2(word): 45 | result = [] 46 | unique_generalized_abbrevations_recursive(word, 0, 0, [], result) 47 | return result 48 | 49 | def unique_generalized_abbrevations_recursive(word, start, count, current_str, result): 50 | if start == len(word): 51 | if count != 0: 52 | current_str.append(str(count)) 53 | result.append(''.join(current_str)) 54 | else: 55 | unique_generalized_abbrevations_recursive(word, start + 1, count + 1, list(current_str), result) 56 | 57 | if count != 0: 58 | current_str.append(str(count)) 59 | new_word = list(current_str) 60 | new_word.append(word[start]) 61 | unique_generalized_abbrevations_recursive(word, start + 1, 0, new_word, result) 62 | 63 | print(unique_generalized_abbrevations("BAT")) 64 | print(unique_generalized_abbrevations_2("BAT")) -------------------------------------------------------------------------------- /top k elements/connect ropes.py: -------------------------------------------------------------------------------- 1 | # Given ‘N’ ropes with different lengths, we need to connect these ropes into one big rope with minimum cost. 2 | # The cost of connecting two ropes is equal to the sum of their lengths. 3 | 4 | # Example: 5 | # Input: [1, 3, 11, 5] 6 | # Output: 33 7 | # Explanation: First connect 1+3(=4), then 4+5(=9), and then 9+11(=20). So the total cost is 33 (4+9+20) 8 | 9 | # O(NlogN) space: O(N) 10 | from heapq import * 11 | 12 | def connect_ropes(ropes): 13 | minheap = [] 14 | result = 0 15 | 16 | for i in range(len(ropes)): 17 | heappush(minheap, ropes[i]) 18 | 19 | while len(minheap) > 1: 20 | min_length = heappop(minheap) + heappop(minheap) 21 | result += min_length 22 | heappush(minheap, min_length) 23 | 24 | return result 25 | 26 | print(connect_ropes([1, 3, 11, 5])) 27 | print(connect_ropes([3, 4, 5, 6])) 28 | print(connect_ropes([1, 3, 11, 5, 2])) 29 | -------------------------------------------------------------------------------- /top k elements/frequency sort.py: -------------------------------------------------------------------------------- 1 | # Given a string, sort it based on the decreasing frequency of its characters. 2 | 3 | # Example: 4 | # Input: "Programming" 5 | # Output: "rrggmmPiano" 6 | # Explanation: 'r', 'g', and 'm' appeared twice, so they need to appear before any other character. 7 | 8 | # O(N* logN) space: O(N) 9 | 10 | from heapq import * 11 | 12 | class WordFrequency: 13 | def __init__(self, word, frequency) -> None: 14 | self.word = word 15 | self.frequency = frequency 16 | 17 | def __lt__(self, other): 18 | return self.frequency < other.frequency 19 | 20 | 21 | def frequency_sort(input_str): 22 | hashmap = dict() 23 | for word in input_str: 24 | hashmap[word] = hashmap.get(word, 0) + 1 25 | 26 | minheap = [] 27 | for key, val in hashmap.items(): 28 | heappush(minheap, WordFrequency(key, val)) 29 | 30 | result = '' 31 | while minheap: 32 | word_frequency = heappop(minheap) 33 | for _ in range(word_frequency.frequency): 34 | result = word_frequency.word + result 35 | 36 | return result 37 | 38 | print(frequency_sort("Programming")) 39 | print(frequency_sort("abcbab")) -------------------------------------------------------------------------------- /top k elements/frequency stack.py: -------------------------------------------------------------------------------- 1 | # Design a class that simulates a Stack data structure, implementing the following two operations: 2 | 3 | # push(int num): Pushes the number ‘num’ on the stack. 4 | # pop(): Returns the most frequent number in the stack. If there is a tie, return the number which was pushed later. 5 | 6 | # Example: 7 | # After following push operations: push(1), push(2), push(3), push(2), push(1), push(2), push(5) 8 | # 1. pop() should return 2, as it is the most frequent number 9 | # 2. Next pop() should return 1 10 | # 3. Next pop() should return 2 11 | 12 | from heapq import * 13 | 14 | 15 | class Element: 16 | def __init__(self, num, frequency, sequence) -> None: 17 | self.num = num 18 | self.frequency = frequency 19 | self.sequence = sequence 20 | 21 | def __lt__(self, other): 22 | if self.frequency != other.frequency: 23 | return self.frequency > other.frequency 24 | return self.sequence > other.sequence 25 | 26 | 27 | class FrequenceStack: 28 | maxheap = [] 29 | frequencymap = dict() 30 | sequence = 0 31 | 32 | def push(self, num): 33 | self.frequencymap[num] = self.frequencymap.get(num, 0) + 1 34 | heappush(self.maxheap, Element(num, self.frequencymap[num], self.sequence)) 35 | self.sequence += 1 36 | 37 | 38 | def pop(self): 39 | element = heappop(self.maxheap) 40 | if element.frequency > 1: 41 | self.frequencymap[element.num] -= 1 42 | else: 43 | del self.frequencymap[element.num] 44 | 45 | return element.num 46 | 47 | frequenceStack = FrequenceStack() 48 | frequenceStack.push(1) 49 | frequenceStack.push(2) 50 | frequenceStack.push(3) 51 | frequenceStack.push(2) 52 | frequenceStack.push(1) 53 | frequenceStack.push(2) 54 | frequenceStack.push(5) 55 | print(frequenceStack.pop()) 56 | print(frequenceStack.pop()) 57 | print(frequenceStack.pop()) -------------------------------------------------------------------------------- /top k elements/k closest numbers.py: -------------------------------------------------------------------------------- 1 | # Given a sorted number array and two integers ‘K’ and ‘X’, 2 | # find ‘K’ closest numbers to ‘X’ in the array. 3 | # Return the numbers in the sorted order. ‘X’ is not necessarily present in the array. 4 | 5 | # Example: 6 | # Input: [5, 6, 7, 8, 9], K = 3, X = 7 7 | # Output: [6, 7, 8] 8 | 9 | # O(K logK) space: O(K) 10 | from heapq import * 11 | 12 | class Entry: 13 | def __init__(self, key, value) -> None: 14 | self.key = key 15 | self.value = value 16 | 17 | def __lt__(self, other): 18 | return self.value > other.value 19 | 20 | class ClostestNumber: 21 | minheap = [] 22 | 23 | def __init__(self, nums, k, x) -> None: 24 | self.k = k 25 | self.x = x 26 | self.nums = nums 27 | 28 | def find_k_closest_numbers(self): 29 | for i in range(self.k): 30 | heappush(self.minheap, Entry(self.nums[i], self.distance_from_x(self.nums[i]))) 31 | 32 | for i in range(self.k, len(self.nums)): 33 | if self.distance_from_x(self.nums[i]) < self.minheap[0].value: 34 | heappop(self.minheap) 35 | heappush(self.minheap, Entry(self.nums[i], self.distance_from_x(self.nums[i]))) 36 | 37 | result = [] 38 | while self.minheap: 39 | result.append(heappop(self.minheap).key) 40 | 41 | return result 42 | 43 | def distance_from_x(self, num): 44 | return abs(self.x - num) 45 | 46 | 47 | clostestNumber = ClostestNumber([5, 6, 7, 8, 9], 3, 7) 48 | print(clostestNumber.find_k_closest_numbers()) 49 | clostestNumber = ClostestNumber([2, 4, 5, 6, 9], 3, 6) 50 | print(clostestNumber.find_k_closest_numbers()) 51 | clostestNumber = ClostestNumber([2, 4, 5, 6, 9], 3, 10) 52 | print(clostestNumber.find_k_closest_numbers()) 53 | 54 | # two pointers also can solve this problem 55 | def binary_search(nums, target): 56 | left, right = 0, len(nums) - 1 57 | while left <= right: 58 | mid = left + (right - left) // 2 59 | if nums[mid] == target: 60 | return mid 61 | elif nums[mid] < target: 62 | left = mid + 1 63 | else: 64 | right = mid - 1 65 | 66 | return max(left - 1, 0) 67 | 68 | # O(logN) space: O(1) 69 | def find_k_closest_numbers_2(nums, k, x): 70 | index = binary_search(nums, x) 71 | left, right = index, index + 1 72 | result = [] 73 | for _ in range(k): 74 | if left >= 0 and right < len(nums): 75 | diff1 = abs(nums[left] - x) 76 | diff2 = abs(nums[right] - x) 77 | if diff1 <= diff2: 78 | result.append(nums[left]) 79 | left -= 1 80 | else: 81 | result.append(nums[right]) 82 | right += 1 83 | elif left >= 0: 84 | result.append(nums[left]) 85 | left -= 1 86 | else: 87 | result.append(nums[right]) 88 | right += 1 89 | 90 | return result 91 | 92 | 93 | print(find_k_closest_numbers_2([5, 6, 7, 8, 9], 3, 7)) 94 | print(find_k_closest_numbers_2([2, 4, 5, 6, 9], 3, 6)) 95 | print(find_k_closest_numbers_2([2, 4, 5, 6, 9], 3, 10)) -------------------------------------------------------------------------------- /top k elements/k closest points to the origin.py: -------------------------------------------------------------------------------- 1 | # Given an array of points in the a 2D2D plane, find ‘K’ closest points to the origin. 2 | 3 | # Example: 4 | # Input: points = [[1,2],[1,3]], K = 1 5 | # Output: [[1,2]] 6 | # Explanation: The Euclidean distance between (1, 2) and the origin is sqrt(5). 7 | # The Euclidean distance between (1, 3) and the origin is sqrt(10). 8 | # Since sqrt(5) < sqrt(10), therefore (1, 2) is closer to the origin. 9 | 10 | # Input: point = [[1, 3], [3, 4], [2, -1]], K = 2 11 | # Output: [[1, 3], [2, -1]] 12 | 13 | # O(N * logK) space: O(K) 14 | from heapq import * 15 | 16 | class Point: 17 | def __init__(self, x, y) -> None: 18 | self.x = x 19 | self.y = y 20 | 21 | def __lt__(self, other): 22 | return self.distance_from_origin() > other.distance_from_origin() 23 | 24 | def distance_from_origin(self): 25 | return self.x * self.x + self.y * self.y 26 | 27 | def print_point(self): 28 | print("[" + str(self.x) + "," + str(self.y) + "]", end = '') 29 | 30 | def find_k_closest_to_origin(nums, k): 31 | maxheap = [] 32 | for i in range(k): 33 | heappush(maxheap, Point(nums[i][0], nums[i][1])) 34 | 35 | for i in range(k, len(nums)): 36 | if Point(nums[i][0], nums[i][1]).distance_from_origin() < maxheap[0].distance_from_origin(): 37 | heappop(maxheap) 38 | heappush(maxheap, Point(nums[i][0], nums[i][1])) 39 | 40 | return list(maxheap) 41 | 42 | result = find_k_closest_to_origin([[1, 3], [3, 4], [2, -1]], 2) 43 | for point in result: 44 | point.print_point() 45 | print() 46 | 47 | 48 | result = find_k_closest_to_origin([[1,2],[1,3]], 1) 49 | for point in result: 50 | point.print_point() 51 | print() -------------------------------------------------------------------------------- /top k elements/kth largest number in a stream.py: -------------------------------------------------------------------------------- 1 | # Design a class to efficiently find the Kth largest element in a stream of numbers. 2 | # The class should have the following two things: 3 | # The constructor of the class should accept an integer array containing initial numbers from the stream and an integer ‘K’. 4 | # The class should expose a function add(int num) which will store the given number and return the Kth largest number. 5 | 6 | # Example: 7 | # Input: [3, 1, 5, 12, 2, 11], K = 4 8 | # 1. Calling add(6) should return '5'. 9 | # 2. Calling add(13) should return '6'. 10 | # 2. Calling add(4) should still return '6'. 11 | 12 | # O(logK) space: O(K) 13 | from heapq import * 14 | 15 | class KthlargestStream: 16 | minheap = [] 17 | 18 | def __init__(self, nums, k) -> None: 19 | self.k = k 20 | for num in nums: 21 | self.add(num) 22 | 23 | def add(self, num): 24 | heappush(self.minheap, num) 25 | 26 | if len(self.minheap) > self.k: 27 | heappop(self.minheap) 28 | 29 | return self.minheap[0] 30 | 31 | kthlargestStream = KthlargestStream([3, 1, 5, 12, 2, 11], 4) 32 | print(kthlargestStream.add(6)) 33 | print(kthlargestStream.add(13)) 34 | print(kthlargestStream.add(4)) 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /top k elements/kth smallest number.py: -------------------------------------------------------------------------------- 1 | # Given an unsorted array of numbers, find Kth smallest number in it. 2 | # Please note that it is the Kth smallest number in the sorted order, not the Kth distinct element. 3 | 4 | # Example: 5 | 6 | # Input: [1, 5, 12, 2, 11, 5], K = 3 7 | # Output: 5 8 | # Explanation: The 3rd smallest number is '5', as the first two smaller numbers are [1, 2]. 9 | 10 | # O(N * logK) space: O(K) 11 | from heapq import * 12 | 13 | def find_kth_smallest_number(nums, k): 14 | maxheap = [] 15 | for i in range(k): 16 | heappush(maxheap, -nums[i]) 17 | 18 | for i in range(k, len(nums)): 19 | if nums[i] < -maxheap[0]: 20 | heappop(maxheap) 21 | heappush(maxheap, -nums[i]) 22 | 23 | return -maxheap[0] 24 | 25 | print(find_kth_smallest_number([1, 5, 12, 2, 11, 5], 3)) 26 | print(find_kth_smallest_number([1, 5, 12, 2, 11, 5], 4)) 27 | print(find_kth_smallest_number([5, 12, -1, 11, 12], 3)) -------------------------------------------------------------------------------- /top k elements/maximum distinct elements.py: -------------------------------------------------------------------------------- 1 | # Given an array of numbers and a number ‘K’, 2 | # we need to remove ‘K’ numbers from the array such that we are left with maximum distinct numbers. 3 | 4 | # Example: 5 | # Input: [7, 3, 5, 8, 5, 3, 3], and K=2 6 | # Output: 3 7 | # Explanation: We can remove two occurrences of 3 to be left with 3 distinct numbers [7, 3, 8], 8 | # we have to skip 5 because it is not distinct and occurred twice. 9 | # Another solution could be to remove one instance of '5' and '3' each to be left with three 10 | # distinct numbers [7, 5, 8], in this case, we have to skip 3 because it occurred twice. 11 | 12 | # O(N * logN + K * logN) space: O(N) 13 | from heapq import * 14 | 15 | def find_maximum_distinct_elements(nums, k): 16 | distinct_elements = 0 17 | if len(nums) <= k : 18 | return distinct_elements 19 | 20 | hashmap = dict() 21 | for num in nums: 22 | hashmap[num] = hashmap.get(num, 0) + 1 23 | 24 | minheap = [] 25 | for num, frequency in hashmap.items(): 26 | if frequency == 1: 27 | distinct_elements += 1 28 | else: 29 | heappush(minheap, (frequency, num)) 30 | 31 | while k > 0 and minheap: 32 | frequency, num = heappop(minheap) 33 | k -= frequency - 1 34 | if k >= 0: 35 | distinct_elements += 1 36 | 37 | if k > 0: 38 | distinct_elements -= k 39 | 40 | return distinct_elements 41 | 42 | print(find_maximum_distinct_elements([7, 3, 5, 8, 5, 3, 3], 2)) 43 | print(find_maximum_distinct_elements([3, 5, 12, 11, 12], 3)) 44 | print(find_maximum_distinct_elements([1, 2, 3, 3, 3, 3, 4, 4, 5, 5, 5], 2)) -------------------------------------------------------------------------------- /top k elements/rearrange string k distance apart.py: -------------------------------------------------------------------------------- 1 | # Given a string and a number ‘K’, 2 | # find if the string can be rearranged such that the same characters are at least ‘K’ distance apart from each other. 3 | 4 | # Example: 5 | # Input: "mmpp", K=2 6 | # Output: "mpmp" or "pmpm" 7 | # Explanation: All same characters are 2 distance apart. 8 | 9 | # O(N * logN) space: O(N) 10 | from heapq import * 11 | 12 | def rearrange_string_k_distance_apart(input_str, k): 13 | maxheap = [] 14 | hashmap = dict() 15 | for char in input_str: 16 | hashmap[char] = hashmap.get(char, 0) + 1 17 | 18 | for char, frequency in hashmap.items(): 19 | heappush(maxheap, (-frequency, char)) 20 | 21 | result = [] 22 | previous_char, previous_frequency = [], [] 23 | count = 1 24 | while maxheap: 25 | frequency, char = heappop(maxheap) 26 | if count >= k and previous_char[count - k] and -previous_frequency[count - k] > 0: 27 | heappush(maxheap, (previous_frequency[count - k], previous_char[count - k])) 28 | result.append(char) 29 | previous_char.append(char) 30 | previous_frequency.append(frequency + 1) 31 | count += 1 32 | 33 | if len(result) == len(input_str): 34 | return "".join(result) 35 | else: 36 | return "" 37 | 38 | print(rearrange_string_k_distance_apart("mmpp", 2)) 39 | print(rearrange_string_k_distance_apart("Programming", 3)) 40 | print(rearrange_string_k_distance_apart("aab", 2)) 41 | print(rearrange_string_k_distance_apart("aappa", 3)) 42 | 43 | 44 | # to refine the solution, we can replace result = [] with deque() 45 | # O(N * logN) space: O(N) 46 | 47 | from collections import deque 48 | 49 | def rearrange_string_k_distance_apart_2(input_str, k): 50 | maxheap = [] 51 | hashmap = dict() 52 | for char in input_str: 53 | hashmap[char] = hashmap.get(char, 0) + 1 54 | 55 | for char, frequency in hashmap.items(): 56 | heappush(maxheap, (-frequency, char)) 57 | 58 | result = [] 59 | queue = deque() 60 | 61 | while maxheap: 62 | frequency, char = heappop(maxheap) 63 | result.append(char) 64 | queue.append((frequency + 1, char)) 65 | if len(queue) == k: 66 | frequency, char = queue.popleft() 67 | if -frequency > 0: 68 | heappush(maxheap, (-frequency, char)) 69 | 70 | if len(result) == len(input_str): 71 | return "".join(result) 72 | else: 73 | return "" 74 | 75 | 76 | print(rearrange_string_k_distance_apart_2("mmpp", 2)) 77 | print(rearrange_string_k_distance_apart_2("Programming", 3)) 78 | print(rearrange_string_k_distance_apart_2("aab", 2)) 79 | print(rearrange_string_k_distance_apart_2("aappa", 3)) -------------------------------------------------------------------------------- /top k elements/rearrange string.py: -------------------------------------------------------------------------------- 1 | # Given a string, find if its letters can be rearranged in such a way that no two same characters come next to each other. 2 | 3 | # Example: 4 | # Input: "aappp" 5 | # Output: "papap" 6 | # Explanation: In "papap", none of the repeating characters come next to each other. 7 | 8 | # O(N * logN) space: O(N) 9 | from heapq import * 10 | 11 | def rearrange_string(input_str): 12 | maxheap = [] 13 | hashmap = dict() 14 | for char in input_str: 15 | hashmap[char] = hashmap.get(char, 0) + 1 16 | 17 | for char, frequency in hashmap.items(): 18 | heappush(maxheap, (-frequency, char)) 19 | 20 | result = [] 21 | previous_char, previous_frequency = None, 0 22 | while maxheap: 23 | frequency, char = heappop(maxheap) 24 | if previous_char and -previous_frequency > 0: 25 | heappush(maxheap, (previous_frequency, previous_char)) 26 | result.append(char) 27 | previous_char = char 28 | previous_frequency = frequency + 1 29 | 30 | if len(result) == len(input_str): 31 | return "".join(result) 32 | else: 33 | return "" 34 | 35 | 36 | print(rearrange_string("aappp")) 37 | print(rearrange_string("Programming")) 38 | print(rearrange_string("aapa")) -------------------------------------------------------------------------------- /top k elements/scheduling tasks.py: -------------------------------------------------------------------------------- 1 | # You are given a list of tasks that need to be run, in any order, on a server. 2 | # Each task will take one CPU interval to execute but once a task has finished, 3 | # it has a cooling period during which it can’t be run again. 4 | # If the cooling period for all tasks is ‘K’ intervals, 5 | # find the minimum number of CPU intervals that the server needs to finish all tasks. 6 | # If at any time the server can’t execute any task then it must stay idle. 7 | 8 | # Example: 9 | # Input: [a, a, a, b, c, c], K=2 10 | # Output: 7 11 | # Explanation: a -> c -> b -> a -> c -> idle -> a 12 | 13 | # O(N * logN) space: O(N) 14 | from heapq import * 15 | 16 | 17 | def schedule_tasks(tasks, k): 18 | hashmap = dict() 19 | for task in tasks: 20 | hashmap[task] = hashmap.get(task, 0) + 1 21 | 22 | maxheap = [] 23 | for task, frequency in hashmap.items(): 24 | heappush(maxheap, (-frequency, task)) 25 | 26 | result = 0 27 | 28 | while maxheap: 29 | waitlist = [] 30 | n = k + 1 31 | while n > 0 and maxheap: 32 | frequency, task = heappop(maxheap) 33 | result += 1 34 | if -frequency > 1: 35 | waitlist.append((frequency + 1, task)) 36 | n -= 1 37 | 38 | for frequency, task in waitlist: 39 | heappush(maxheap, (frequency, task)) 40 | 41 | if maxheap: 42 | result += n 43 | 44 | return result 45 | 46 | 47 | print(schedule_tasks(['a', 'a', 'a', 'b', 'c', 'c'], 2)) 48 | print(schedule_tasks(['a', 'b', 'a'], 3)) 49 | -------------------------------------------------------------------------------- /top k elements/sum of elements.py: -------------------------------------------------------------------------------- 1 | # Given an array, find the sum of all numbers between the K1’th and K2’th smallest elements of that array. 2 | 3 | # Example: 4 | # Input: [1, 3, 12, 5, 15, 11], and K1=3, K2=6 5 | # Output: 23 6 | # Explanation: The 3rd smallest number is 5 and 6th smallest number 15. The sum of numbers coming 7 | # between 5 and 15 is 23 (11+12). 8 | 9 | 10 | # O(N * logN) space: O(N) 11 | from heapq import * 12 | 13 | def sum_of_elements(nums, k1, k2): 14 | minheap = [] 15 | for num in nums: 16 | heappush(minheap, num) 17 | 18 | for _ in range(k1): 19 | heappop(minheap) 20 | 21 | numsum = 0 22 | for _ in range(k2 - k1 - 1): 23 | numsum += heappop(minheap) 24 | 25 | return numsum 26 | 27 | print(sum_of_elements([1, 3, 12, 5, 15, 11], 3, 6)) 28 | print(sum_of_elements([3, 5, 8, 7], 1, 4)) 29 | 30 | # to reduce the time complexity 31 | # O(N * logk2) space:O(k2) 32 | def sum_of_elements_2(nums, k1, k2): 33 | maxheap = [] 34 | for i in range(len(nums)): 35 | if i < k2 - 1: 36 | heappush(maxheap, -nums[i]) 37 | elif nums[i] < -maxheap[0]: 38 | heappop(maxheap) 39 | heappush(maxheap, -nums[i]) 40 | 41 | numsum = 0 42 | for _ in range(k2 - k1 - 1): 43 | numsum += -heappop(maxheap) 44 | 45 | return numsum 46 | 47 | print(sum_of_elements_2([1, 3, 12, 5, 15, 11], 3, 6)) 48 | print(sum_of_elements_2([3, 5, 8, 7], 1, 4)) 49 | 50 | -------------------------------------------------------------------------------- /top k elements/top k frequent numbers.py: -------------------------------------------------------------------------------- 1 | # Given an unsorted array of numbers, find the top ‘K’ frequently occurring numbers in it. 2 | 3 | # Example: 4 | # Input: [1, 3, 5, 12, 11, 12, 11], K = 2 5 | # Output: [12, 11] 6 | # Explanation: Both '11' and '12' apeared twice. 7 | 8 | # O(N + N * logK) space: O(N) 9 | from heapq import * 10 | 11 | class WordFrequency: 12 | def __init__(self, word, frequency) -> None: 13 | self.word = word 14 | self.frequency = frequency 15 | 16 | def __lt__(self, other): 17 | return self.frequency < other.frequency 18 | 19 | 20 | def top_k_frequent_elements(nums, k): 21 | hashmap = dict() 22 | for num in nums: 23 | if num not in hashmap: 24 | hashmap[num] = 1 25 | else: 26 | hashmap[num] += 1 27 | 28 | minheap = [] 29 | for key, val in hashmap.items(): 30 | heappush(minheap, WordFrequency(key, val)) 31 | if len(minheap) > k: 32 | heappop(minheap) 33 | 34 | result = [] 35 | while minheap: 36 | result.append(heappop(minheap).word) 37 | 38 | return result 39 | 40 | print(top_k_frequent_elements([1, 3, 5, 12, 11, 12, 11], 2)) 41 | print(top_k_frequent_elements([5, 12, 11, 3, 11], 2)) 42 | -------------------------------------------------------------------------------- /top k elements/top k numbers.py: -------------------------------------------------------------------------------- 1 | # Given an unsorted array of numbers, find the ‘K’ largest numbers in it. 2 | 3 | # Example: 4 | # Input: [3, 1, 5, 12, 2, 11], K = 3 5 | # Output: [5, 12, 11] 6 | 7 | # O(K * logK + (N - K) * logK) = O(N * logK) 8 | # space: O(K) 9 | from heapq import * 10 | 11 | def find_k_largest_numbers(nums, k): 12 | minheap = [] 13 | for i in range(k): 14 | heappush(minheap, nums[i]) 15 | 16 | for i in range(k, len(nums)): 17 | if nums[i] > minheap[0]: 18 | heappop(minheap) 19 | heappush(minheap, nums[i]) 20 | 21 | return list(minheap) 22 | 23 | print(find_k_largest_numbers([3, 1, 5, 12, 2, 11], 3)) -------------------------------------------------------------------------------- /topological sort/alien dictionary.py: -------------------------------------------------------------------------------- 1 | # There is a dictionary containing words from an alien language for which we don’t know the ordering of the characters. 2 | # Write a method to find the correct order of characters in the alien language. 3 | 4 | # Example: 5 | # Input: Words: ["ba", "bc", "ac", "cab"] 6 | # Output: bac 7 | # Explanation: Given that the words are sorted lexicographically by the rules of the alien language, so 8 | # from the given words we can conclude the following ordering among its characters: 9 | # 1. From "ba" and "bc", we can conclude that 'a' comes before 'c'. 10 | # 2. From "bc" and "ac", we can conclude that 'b' comes before 'a' 11 | # From the above two points, we can conclude that the correct character order is: "bac" 12 | 13 | # O(V + N) space: O(V + N) 14 | from collections import deque 15 | 16 | def alien_language(words): 17 | if words is None or len(words) == 0: 18 | return "" 19 | 20 | indegree = dict() 21 | graph = dict() 22 | 23 | for word in words: 24 | for char in word: 25 | indegree[char] = 0 26 | graph[char] = [] 27 | 28 | for i in range(len(words) - 1): 29 | word1, word2 = words[i], words[i + 1] 30 | for j in range(min(len(word1), len(word2))): 31 | parent, child = word1[j], word2[j] 32 | if parent != child: 33 | graph[parent].append(child) 34 | indegree[child] += 1 35 | break 36 | 37 | sources = deque() 38 | for key, value in indegree.items(): 39 | if value == 0: 40 | sources.append(key) 41 | 42 | order = [] 43 | 44 | while sources: 45 | parent = sources.popleft() 46 | order.append(parent) 47 | children = graph[parent] 48 | for child in children: 49 | indegree[child] -= 1 50 | if indegree[child] == 0: 51 | sources.append(child) 52 | 53 | if len(order) != len(indegree): 54 | return "" 55 | 56 | return "".join(order) 57 | 58 | print(alien_language(["ba", "bc", "ac", "cab"])) 59 | print(alien_language(["cab", "aaa", "aab"])) 60 | print(alien_language(["ywx", "wz", "xww", "xz", "zyy", "zwz"])) -------------------------------------------------------------------------------- /topological sort/all tasks scheduling orders.py: -------------------------------------------------------------------------------- 1 | # There are ‘N’ tasks, labeled from ‘0’ to ‘N-1’. 2 | # Each task can have some prerequisite tasks which need to be completed before it can be scheduled. 3 | # Given the number of tasks and a list of prerequisite pairs, 4 | # write a method to print all possible ordering of tasks meeting all prerequisites. 5 | 6 | # Example: 7 | # Input: Tasks=4, Prerequisites=[3, 2], [3, 0], [2, 0], [2, 1] 8 | # Output: 9 | # 1) [3, 2, 0, 1] 10 | # 2) [3, 2, 1, 0] 11 | # Explanation: There are two possible orderings of the tasks meeting all prerequisites. 12 | 13 | # O(V! * E) space: O(V! * E) 14 | from collections import deque 15 | 16 | def tasks_scheduling_order(tasks, prerequisites): 17 | if tasks <= 0: 18 | return "" 19 | 20 | indegree = dict() 21 | graph = dict() 22 | 23 | for i in range(tasks): 24 | indegree[i] = 0 25 | graph[i] = [] 26 | 27 | for i in range(len(prerequisites)): 28 | parent, child = prerequisites[i][0], prerequisites[i][1] 29 | graph[parent].append(child) 30 | indegree[child] += 1 31 | 32 | sources = deque() 33 | for key, value in indegree.items(): 34 | if value == 0: 35 | sources.append(key) 36 | 37 | result = [] 38 | print_all_toposort(graph, indegree, sources, [],result) 39 | return result 40 | 41 | def print_all_toposort(graph, indegree, sources, sorted_order,result): 42 | if sources: 43 | for vertex in sources: 44 | sorted_order.append(vertex) 45 | sources_for_next_call = deque(sources) 46 | sources_for_next_call.remove(vertex) 47 | 48 | for child in graph[vertex]: 49 | indegree[child] -= 1 50 | if indegree[child] == 0: 51 | sources_for_next_call.append(child) 52 | 53 | print_all_toposort(graph, indegree, sources_for_next_call, sorted_order, result) 54 | 55 | sorted_order.remove(vertex) 56 | for child in graph[vertex]: 57 | indegree[child] += 1 58 | 59 | if len(sorted_order) == len(indegree): 60 | result.append(list(sorted_order)) 61 | 62 | print(tasks_scheduling_order(4, [[3, 2], [3, 0], [2, 0], [2, 1]])) 63 | print(tasks_scheduling_order(3, [[0, 1], [1, 2]])) 64 | result = tasks_scheduling_order(6, [[2, 5], [0, 5], [0, 4], [1, 4], [3, 2], [1, 3]]) 65 | print(result, len(result)) -------------------------------------------------------------------------------- /topological sort/minimum height trees.py: -------------------------------------------------------------------------------- 1 | # We are given an undirected graph that has characteristics of a k-ary tree. 2 | # In such a graph, we can choose any node as the root to make a k-ary tree. 3 | # The root (or the tree) with the minimum height will be called Minimum Height Tree (MHT). 4 | # There can be multiple MHTs for a graph. 5 | # In this problem, we need to find all those roots which give us MHTs. 6 | # Write a method to find all MHTs of the given graph and return a list of their roots. 7 | 8 | # Example: 9 | # Input: vertices: 5, Edges: [[0, 1], [1, 2], [1, 3], [2, 4]] 10 | # Output:[1, 2] 11 | # Explanation: Choosing '1' or '2' as roots give us MHTs. In the below diagram, 12 | # we can see that the height of the trees with roots '1' or '2' is three which is minimum. 13 | 14 | # O(V + E) space: O(V + E) 15 | from collections import deque 16 | 17 | def minimum_height_trees(vertices, edges): 18 | if vertices <= 0: 19 | return [] 20 | 21 | if vertices == 1: 22 | return [0] 23 | 24 | indegree = {i: 0 for i in range(vertices)} 25 | graph = {i: [] for i in range(vertices)} 26 | 27 | for edge in edges: 28 | parent, child = edge[0], edge[1] 29 | graph[parent].append(child) 30 | graph[child].append(parent) 31 | indegree[parent] += 1 32 | indegree[child] += 1 33 | 34 | leaves = deque() 35 | for key, value in indegree.items(): 36 | if value == 1: 37 | leaves.append(key) 38 | 39 | total_nodes = vertices 40 | while total_nodes > 2: 41 | leave_size = len(leaves) 42 | total_nodes -= leave_size 43 | for _ in range(leave_size): 44 | vertex = leaves.popleft() 45 | for child in graph[vertex]: 46 | indegree[child] -= 1 47 | if indegree[child] == 1: 48 | leaves.append(child) 49 | 50 | return list(leaves) 51 | 52 | print(minimum_height_trees(5, [[0, 1], [1, 2], [1, 3], [2, 4]])) 53 | print(minimum_height_trees(4, [[0, 1], [0, 2], [2, 3]])) 54 | print(minimum_height_trees(4, [[0, 1], [1, 2], [1, 3]])) -------------------------------------------------------------------------------- /topological sort/reconstructing a sequence.py: -------------------------------------------------------------------------------- 1 | # Given a sequence originalSeq and an array of sequences, 2 | # write a method to find if originalSeq can be uniquely reconstructed from the array of sequences. 3 | 4 | # Unique reconstruction means that we need to find if originalSeq is the only sequence 5 | # such that all sequences in the array are subsequences of it. 6 | 7 | # Example: 8 | # Input: originalSeq: [1, 2, 3, 4], seqs: [[1, 2], [2, 3], [3, 4]] 9 | # Output: true 10 | # Explanation: The sequences [1, 2], [2, 3], and [3, 4] can uniquely reconstruct 11 | # [1, 2, 3, 4], in other words, all the given sequences uniquely define the order of numbers in the 'originalSeq'. 12 | 13 | # O(V + N) space: O(V + N) 14 | from collections import deque 15 | 16 | 17 | def can_construct(original_seq, seqs): 18 | sorted_order = [] 19 | if len(original_seq) <= 0: 20 | return False 21 | 22 | indegree = dict() 23 | graph = dict() 24 | for seq in seqs: 25 | for num in seq: 26 | indegree[num] = 0 27 | graph[num] = [] 28 | 29 | for seq in seqs: 30 | for i in range(1, len(seq)): 31 | parent, child = seq[i - 1], seq[i] 32 | graph[parent].append(child) 33 | indegree[child] += 1 34 | 35 | if len(indegree) != len(original_seq): 36 | return False 37 | 38 | sources = deque() 39 | for key, value in indegree.items(): 40 | if value == 0: 41 | sources.append(key) 42 | 43 | while sources: 44 | if len(sources) > 1: 45 | return False 46 | 47 | if original_seq[len(sorted_order)] != sources[0]: 48 | return False 49 | 50 | vertex = sources.popleft() 51 | sorted_order.append(vertex) 52 | for child in graph[vertex]: 53 | indegree[child] -= 1 54 | if indegree[child] == 0: 55 | sources.append(child) 56 | 57 | return len(sorted_order) == len(original_seq) 58 | 59 | print(can_construct([1, 2, 3, 4], [[1, 2], [2, 3], [3, 4]])) 60 | print(can_construct([1, 2, 3, 4], [[1, 2], [2, 3], [2, 4]])) 61 | print(can_construct([3, 1, 4, 2, 5], [[3, 1, 5], [1, 4, 2, 5]])) 62 | -------------------------------------------------------------------------------- /topological sort/task scheduling order.py: -------------------------------------------------------------------------------- 1 | # There are ‘N’ tasks, labeled from ‘0’ to ‘N-1’. 2 | # Each task can have some prerequisite tasks which need to be completed before it can be scheduled. 3 | # Given the number of tasks and a list of prerequisite pairs, 4 | # write a method to find the ordering of tasks we should pick to finish all tasks. 5 | 6 | # Example: 7 | # Input: Tasks=3, Prerequisites=[0, 1], [1, 2] 8 | # Output: [0, 1, 2] 9 | # Explanation: To execute task '1', task '0' needs to finish first. Similarly, task '1' needs to finish 10 | # before '2' can be scheduled. A possible scheduling of tasks is: [0, 1, 2] 11 | 12 | # O(V + E) space: O(V + E) 13 | from collections import deque 14 | 15 | def tasks_scheduling_order(tasks, prerequisites): 16 | if tasks <= 0: 17 | return [] 18 | 19 | indegree = dict() 20 | graph = dict() 21 | 22 | for i in range(tasks): 23 | indegree[i] = 0 24 | graph[i] = [] 25 | 26 | for i in range(len(prerequisites)): 27 | parent, child = prerequisites[i][0], prerequisites[i][1] 28 | graph[parent].append(child) 29 | indegree[child] += 1 30 | 31 | sources = deque() 32 | for key, value in indegree.items(): 33 | if value == 0: 34 | sources.append(key) 35 | 36 | order = [] 37 | 38 | while sources: 39 | parent = sources.popleft() 40 | order.append(parent) 41 | children = graph[parent] 42 | for child in children: 43 | indegree[child] -= 1 44 | if indegree[child] == 0: 45 | sources.append(child) 46 | 47 | if len(order) != tasks: 48 | return [] 49 | 50 | return order 51 | 52 | print(tasks_scheduling_order(3, [[0, 1], [1, 2]])) 53 | print(tasks_scheduling_order(3, [[0, 1], [1, 2], [2, 0]])) 54 | print(tasks_scheduling_order(6, [[2, 5], [0, 5], [0, 4], [1, 4], [3, 2], [1, 3]])) -------------------------------------------------------------------------------- /topological sort/tasks scheduling.py: -------------------------------------------------------------------------------- 1 | # There are ‘N’ tasks, labeled from ‘0’ to ‘N-1’. 2 | # Each task can have some prerequisite tasks which need to be completed before it can be scheduled. 3 | # Given the number of tasks and a list of prerequisite pairs, find out if it is possible to schedule all the tasks. 4 | 5 | # Example: 6 | # Input: Tasks=3, Prerequisites=[0, 1], [1, 2] 7 | # Output: true 8 | # Explanation: To execute task '1', task '0' needs to finish first. Similarly, task '1' needs to finish 9 | # before '2' can be scheduled. A possible sceduling of tasks is: [0, 1, 2] 10 | 11 | # O(V + E) space: O(V + E) 12 | from collections import deque 13 | 14 | def tasks_scheduling(tasks, prerequisites): 15 | if tasks <= 0: 16 | return [] 17 | 18 | indegree = dict() 19 | graph = dict() 20 | 21 | for i in range(tasks): 22 | indegree[i] = 0 23 | graph[i] = [] 24 | 25 | for i in range(len(prerequisites)): 26 | parent, child = prerequisites[i][0], prerequisites[i][1] 27 | graph[parent].append(child) 28 | indegree[child] += 1 29 | 30 | sources = deque() 31 | for key, value in indegree.items(): 32 | if value == 0: 33 | sources.append(key) 34 | 35 | order = [] 36 | 37 | while sources: 38 | parent = sources.popleft() 39 | order.append(parent) 40 | children = graph[parent] 41 | for child in children: 42 | indegree[child] -= 1 43 | if indegree[child] == 0: 44 | sources.append(child) 45 | 46 | return len(order) == tasks 47 | 48 | print(tasks_scheduling(3, [[0, 1], [1, 2]])) 49 | print(tasks_scheduling(3, [[0, 1], [1, 2], [2, 0]])) 50 | print(tasks_scheduling(6, [[2, 5], [0, 5], [0, 4], [1, 4], [3, 2], [1, 3]])) 51 | -------------------------------------------------------------------------------- /topological sort/topological sort.py: -------------------------------------------------------------------------------- 1 | # Topological Sort of a directed graph (a graph with unidirectional edges) is a linear ordering of its vertices 2 | # such that for every directed edge (U, V) from vertex U to vertex V, U comes before V in the ordering. 3 | 4 | # Given a directed graph, find the topological ordering of its vertices. 5 | 6 | # Example: 7 | # Input: Vertices=4, Edges=[3, 2], [3, 0], [2, 0], [2, 1] 8 | # Output: Following are the two valid topological sorts for the given graph: 9 | # 1) 3, 2, 0, 1 10 | # 2) 3, 2, 1, 0 11 | 12 | # O(V + E) space: O(V + E) 13 | from collections import deque 14 | 15 | 16 | def toposort(vertices, edges): 17 | sorted_order = [] 18 | if vertices <= 0: 19 | return sorted_order 20 | 21 | indegree = dict() 22 | graph = dict() 23 | 24 | for i in range(vertices): 25 | indegree[i] = 0 26 | graph[i] = [] 27 | 28 | for i in range(len(edges)): 29 | parent, child = edges[i][0], edges[i][1] 30 | graph[parent].append(child) 31 | indegree[child] += 1 32 | 33 | sources = deque() 34 | for key, value in indegree.items(): 35 | if value == 0: 36 | sources.append(key) 37 | 38 | while sources: 39 | vertex = sources.popleft() 40 | sorted_order.append(vertex) 41 | children = graph[vertex] 42 | for child in children: 43 | indegree[child] -= 1 44 | if indegree[child] == 0: 45 | sources.append(child) 46 | 47 | if len(sorted_order) != vertices: 48 | return [] 49 | 50 | return sorted_order 51 | 52 | print(toposort(4, [[3, 2], [3, 0], [2, 0], [2, 1]])) 53 | print(toposort(5, [[4, 2], [4, 3], [2, 0], [2, 1], [3, 1]])) 54 | print(toposort(7, [[6, 4], [6, 2], [5, 3], [5, 4], [3, 0], [3, 1], [3, 2], [4, 1]])) 55 | 56 | # follow up 57 | # Find if a given Directed Graph has a cycle in it or not. 58 | # Solution: If we can’t determine the topological ordering of all the vertices of a directed graph, 59 | # the graph has a cycle in it. This was also referred to in the above code: 60 | # if len(sorted_order) != vertices: return [] -------------------------------------------------------------------------------- /two heaps/find the median of a number stream.py: -------------------------------------------------------------------------------- 1 | # Design a class to calculate the median of a number stream. The class should have the following two methods: 2 | 3 | # insertNum(int num): stores the number in the class 4 | # findMedian(): returns the median of all numbers inserted in the class 5 | # If the count of numbers inserted in the class is even, the median will be the average of the middle two numbers. 6 | 7 | # Example 1: 8 | 9 | # 1. insertNum(3) 10 | # 2. insertNum(1) 11 | # 3. findMedian() -> output: 2 12 | # 4. insertNum(5) 13 | # 5. findMedian() -> output: 3 14 | # 6. insertNum(4) 15 | # 7. findMedian() -> output: 3.5 16 | 17 | # O(logN) space:O(N) 18 | from heapq import * 19 | 20 | class Median_Stream: 21 | max_heap = [] # contain the smaller part 22 | min_heap = [] # contain the larger part 23 | 24 | def insert_number(self, num): 25 | if not self.max_heap or -self.max_heap[0] >= num: 26 | heappush(self.max_heap, -num) 27 | else: 28 | heappush(self.min_heap, num) 29 | 30 | if len(self.max_heap) > len(self.min_heap) + 1: 31 | heappush(self.min_heap, -heappop(self.max_heap)) 32 | elif len(self.max_heap) < len(self.min_heap): 33 | heappush(self.max_heap, -heappop(self.min_heap)) 34 | 35 | def find_median(self): 36 | if len(self.max_heap) == len(self.min_heap): 37 | return (-self.max_heap[0] + self.min_heap[0])/2 38 | 39 | return -self.max_heap[0] 40 | 41 | median_stream = Median_Stream() 42 | median_stream.insert_number(3) 43 | median_stream.insert_number(1) 44 | print(median_stream.find_median()) 45 | median_stream.insert_number(5) 46 | print(median_stream.find_median()) 47 | median_stream.insert_number(4) 48 | print(median_stream.find_median()) 49 | -------------------------------------------------------------------------------- /two heaps/maximize capital.py: -------------------------------------------------------------------------------- 1 | # Given a set of investment projects with their respective profits, 2 | # we need to find the most profitable projects. 3 | # We are given an initial capital and are allowed to invest only in a fixed number of projects. 4 | # Our goal is to choose projects that give us the maximum profit. 5 | # We can start an investment project only when we have the required capital. 6 | # Once a project is selected, we can assume that its profit has become our capital. 7 | 8 | # Example: 9 | # Input: Project Capitals=[0,1,2], Project Profits=[1,2,3], Initial Capital=1, Number of Projects=2 10 | # Output: 6 11 | # Explanation: 12 | # With initial capital of ‘1’, we will start the second project which will give us profit of ‘2’. 13 | # Once we selected our first project, our total capital will become 3 (profit + initial capital). 14 | # With ‘3’ capital, we will select the third project, which will give us ‘3’ profit. 15 | # After the completion of the two projects, our total capital will be 6 (1+2+3). 16 | 17 | # O(NlogN + KlogN) where ‘N’ is the total number of projects and ‘K’ is the number of projects we are selecting. 18 | # space: O(N) 19 | from heapq import * 20 | 21 | def find_maximize_capital(capitals, profits, initial_capital, num_projects): 22 | max_heap = [] 23 | min_heap = [] 24 | 25 | for i in range(len(capitals)): 26 | heappush(min_heap, (capitals[i], i)) 27 | 28 | available_capital = initial_capital 29 | for _ in range(num_projects): 30 | while min_heap and min_heap[0][0] <= available_capital: 31 | _, i = heappop(min_heap) 32 | heappush(max_heap, (-profits[i], i)) 33 | 34 | if not max_heap: 35 | break 36 | 37 | available_capital += -heappop(max_heap)[0] 38 | 39 | return available_capital 40 | 41 | print(find_maximize_capital([0, 1, 2], [1, 2, 3], 1, 2)) 42 | print(find_maximize_capital([0, 1, 2, 3], [1, 2, 3, 5], 0, 3)) -------------------------------------------------------------------------------- /two heaps/next interval.py: -------------------------------------------------------------------------------- 1 | # Next Interval (hard) 2 | # Given an array of intervals, find the next interval of each interval. In a list of intervals, 3 | # for an interval ‘i’ its next interval ‘j’ will have the smallest ‘start’ greater than or equal to the ‘end’ of ‘i’. 4 | # Write a function to return an array containing indices of the next interval of each input interval. 5 | # If there is no next interval of a given interval, return -1. 6 | # It is given that none of the intervals have the same start point. 7 | 8 | # Example: 9 | # Input: Intervals [[2,3], [3,4], [5,6]] 10 | # Output: [1, 2, -1] 11 | # Explanation: The next interval of [2,3] is [3,4] having index ‘1’. 12 | # Similarly, the next interval of [3,4] is [5,6] having index ‘2’. 13 | # There is no next interval for [5,6] hence we have ‘-1’. 14 | 15 | # brute force: O(N^2) space: O(N) 16 | import math 17 | def next_interval(arr): 18 | result = [-1 for _ in range(len(arr))] 19 | min_interval = [math.inf for _ in range(len(arr))] 20 | for i in range(len(arr)): 21 | for j in range(len(arr)): 22 | if i == j: 23 | continue 24 | if arr[i][1] <= arr[j][0]: 25 | if min_interval[i] > arr[j][0]: 26 | min_interval[i] = arr[j][0] 27 | result[i] = j 28 | return result 29 | 30 | print(next_interval([[2,3], [3,4], [5,6]])) 31 | print(next_interval([[3,4], [1,5], [4,6]])) 32 | 33 | # O(NlogN) space: O(N) 34 | from heapq import * 35 | 36 | def find_next_interval(arr): 37 | start_list = [] 38 | end_list = [] 39 | result = [-1] * len(arr) 40 | for i in range(len(arr)): 41 | heappush(start_list, (arr[i][0], i)) 42 | heappush(end_list, (arr[i][1], i)) 43 | 44 | while end_list: 45 | end_value, end_index = heappop(end_list) 46 | 47 | while start_list and start_list[0][0] < end_value: 48 | heappop(start_list) 49 | 50 | if start_list: 51 | start_value, start_index = heappop(start_list) 52 | result[end_index] = start_index 53 | heappush(start_list, (start_value, start_index)) 54 | 55 | return result 56 | 57 | print(find_next_interval([[2,3], [3,4], [5,6]])) 58 | print(find_next_interval([[3,4], [1,5], [4,6]])) 59 | -------------------------------------------------------------------------------- /two heaps/sliding window median.py: -------------------------------------------------------------------------------- 1 | # Problem Statement 2 | # Given an array of numbers and a number ‘k’, 3 | # find the median of all the ‘k’ sized sub-arrays (or windows) of the array. 4 | 5 | # Example 1: 6 | # Input: nums=[1, 2, -1, 3, 5], k = 2 7 | # Output: [1.5, 0.5, 1.0, 4.0] 8 | # Explanation: Lets consider all windows of size ‘2’: 9 | # [1, 2, -1, 3, 5] -> median is 1.5 10 | # [1, 2, -1, 3, 5] -> median is 0.5 11 | # [1, 2, -1, 3, 5] -> median is 1.0 12 | # [1, 2, -1, 3, 5] -> median is 4.0 13 | 14 | # Example 2: 15 | # Input: nums=[1, 2, -1, 3, 5], k = 3 16 | # Output: [1.0, 2.0, 3.0] 17 | # Explanation: Lets consider all windows of size ‘3’: 18 | # [1, 2, -1, 3, 5] -> median is 1.0 19 | # [1, 2, -1, 3, 5] -> median is 2.0 20 | # [1, 2, -1, 3, 5] -> median is 3.0 21 | 22 | # O(NK) where ‘N’ is the total number of elements in the input array and ‘K’ is the size of the sliding window. 23 | 24 | # This is due to the fact that we are going through all the ‘N’ numbers and, while doing so, we are doing two things: 25 | # 1. Inserting/removing numbers from heaps of size ‘K’. This will take O(logK) 26 | # 2. Removing the element going out of the sliding window. 27 | # This will take O(K) as we will be searching this element in an array of size ‘K’ (i.e., a heap). 28 | 29 | # space: O(K) 30 | 31 | from heapq import * 32 | import heapq 33 | 34 | class Sliding_Median: 35 | def __init__(self) -> None: 36 | self.max_heap = [] 37 | self.min_heap = [] 38 | 39 | def find_sliding_window_median(self, arr, k): 40 | result = [0.0 for i in range(len(arr) - k + 1)] 41 | for i in range(len(arr)): 42 | if not self.max_heap or -self.max_heap[0] >= arr[i]: 43 | heappush(self.max_heap, -arr[i]) 44 | else: 45 | heappush(self.min_heap, arr[i]) 46 | 47 | self.rebalance_heap() 48 | 49 | if i - k + 1 >= 0: 50 | if len(self.max_heap) == len(self.min_heap): 51 | result[i - k + 1] = (-self.max_heap[0] + self.min_heap[0]) /2.0 52 | else: 53 | result[i - k + 1] = -self.max_heap[0] / 1.0 54 | 55 | element_to_removed = arr[i - k + 1] 56 | if element_to_removed <= -self.max_heap[0]: 57 | self.remove_element(self.max_heap, -element_to_removed) 58 | else: 59 | self.remove_element(self.min_heap, element_to_removed) 60 | 61 | self.rebalance_heap() 62 | 63 | return result 64 | 65 | def rebalance_heap(self): 66 | if len(self.max_heap) > len(self.min_heap) + 1: 67 | heappush(self.min_heap, -heappop(self.max_heap)) 68 | elif len(self.max_heap) < len(self.min_heap): 69 | heappush(self.max_heap, -heappop(self.min_heap)) 70 | 71 | def remove_element(self, heap, element): 72 | index = heap.index(element) 73 | heap[index] = heap[-1] 74 | del heap[-1] 75 | if index < len(heap): 76 | heapq._siftup(heap, index) 77 | heapq._siftdown(heap, 0, index) 78 | 79 | sliding_window = Sliding_Median() 80 | print(sliding_window.find_sliding_window_median([1, 2, -1, 3, 5], 2)) 81 | 82 | sliding_window = Sliding_Median() 83 | print(sliding_window.find_sliding_window_median([1, 2, -1, 3, 5], 3)) 84 | -------------------------------------------------------------------------------- /two pointers/comparing strings containing backspaces.py: -------------------------------------------------------------------------------- 1 | # Given two strings containing backspaces (identified by the character ‘#’), 2 | # check if the two strings are equal. 3 | 4 | # Example: 5 | # Input: str1="xy#z", str2="xzz#" 6 | # Output: true 7 | # Explanation: After applying backspaces the strings become "xz" and "xz" respectively. 8 | 9 | # O(M+N) where ‘M’ and ‘N’ are the lengths of the two input strings respectively. 10 | # space:O(1) 11 | def comparing_string_contain_backspaces(str1, str2): 12 | pointer1, pointer2 = len(str1) - 1, len(str2) - 1 13 | while pointer1 >= 0 and pointer2 >= 0: 14 | i1 = get_next_valid_index(str1, pointer1) 15 | i2 = get_next_valid_index(str2, pointer2) 16 | if i1 < 0 and i2 < 0: 17 | return True 18 | 19 | if i1< 0 or i2 < 0: 20 | return False 21 | 22 | if str1[i1] != str2[i2]: 23 | return False 24 | 25 | pointer1 = i1 - 1 26 | pointer2 = i2 - 1 27 | 28 | return True 29 | 30 | def get_next_valid_index(str, index): 31 | backspace_count = 0 32 | while index >= 0: 33 | if str[index] == '#': 34 | backspace_count += 1 35 | elif backspace_count > 0: 36 | backspace_count -= 1 37 | else: 38 | break 39 | 40 | index -= 1 41 | return index 42 | 43 | print(comparing_string_contain_backspaces("xy#z", "xzz#")) 44 | print(comparing_string_contain_backspaces("xy#z", "xyz#")) 45 | print(comparing_string_contain_backspaces("xp#", "xyz##")) 46 | print(comparing_string_contain_backspaces("xywrrmp", "xywrrmu#p")) -------------------------------------------------------------------------------- /two pointers/dutch national flag problem.py: -------------------------------------------------------------------------------- 1 | # Given an array containing 0s, 1s and 2s, sort the array in-place. 2 | # You should treat numbers of the array as objects, hence, we can’t count 0s, 1s, and 2s to recreate the array. 3 | 4 | # The flag of the Netherlands consists of three colors: red, white and blue; 5 | # and since our input array also consists of three different numbers that is why it is called Dutch National Flag problem. 6 | 7 | # Example: 8 | # Input: [1, 0, 2, 1, 0] 9 | # Output: [0 0 1 1 2] 10 | 11 | def dutch_national_flag(arr): 12 | left, right = 0, len(arr) - 1 13 | i = 0 14 | while i <= right: 15 | if arr[i] == 0: 16 | arr[left],arr[i] = arr[i], arr[left] 17 | left += 1 18 | i += 1 19 | elif arr[i] == 2: 20 | arr[right], arr[i] = arr[i], arr[right] 21 | right -= 1 22 | else: 23 | i += 1 24 | return arr 25 | 26 | print(dutch_national_flag([1, 0, 2, 1, 0])) 27 | print(dutch_national_flag([2, 2, 0, 1, 2, 0])) -------------------------------------------------------------------------------- /two pointers/minimum window sort.py: -------------------------------------------------------------------------------- 1 | # Given an array, find the length of the smallest subarray in it which when sorted will sort the whole array. 2 | 3 | # Example: 4 | # Input: [1, 2, 5, 3, 7, 10, 9, 12] 5 | # Output: 5 6 | # Explanation: We need to sort only the subarray [5, 3, 7, 10, 9] to make the whole array sorted 7 | 8 | import math 9 | def minimum_window_sort(arr): 10 | left, right = 0, len(arr) - 1 11 | while left < len(arr)-1 and arr[left] <= arr[left + 1]: 12 | left += 1 13 | 14 | if left == len(arr) - 1: 15 | return 0 16 | 17 | while right > 0 and arr[right] >= arr[right - 1]: 18 | right -= 1 19 | 20 | subarray_min = math.inf 21 | subarray_max = -math.inf 22 | 23 | for i in range(left, right+1): 24 | subarray_min = min(subarray_min, arr[i]) 25 | subarray_max = max(subarray_max, arr[i]) 26 | 27 | while left > 0 and arr[left - 1] > subarray_min: 28 | left -= 1 29 | 30 | while right < len(arr) - 1 and arr[right + 1] < subarray_max: 31 | right += 1 32 | 33 | return right - left + 1 34 | 35 | print(minimum_window_sort([1, 2, 5, 3, 7, 10, 9, 12])) 36 | print(minimum_window_sort([1, 3, 2, 0, -1, 7, 10])) 37 | print(minimum_window_sort([1, 2, 3])) 38 | print(minimum_window_sort([3, 2 ,1])) 39 | 40 | -------------------------------------------------------------------------------- /two pointers/pair with target sum.py: -------------------------------------------------------------------------------- 1 | # Given an array of sorted numbers and a target sum, find a pair in the array whose sum is equal to the given target. 2 | # Write a function to return the indices of the two numbers (i.e. the pair) such that they add up to the given target. 3 | 4 | # Example: 5 | # Input: [1, 2, 3, 4, 6], target=6 6 | # Output: [1, 3] 7 | # Explanation: The numbers at index 1 and 3 add up to 6: 2+4=6 8 | 9 | # Input: [2, 5, 9, 11], target=11 10 | # Output: [0, 2] 11 | # Explanation: The numbers at index 0 and 2 add up to 11: 2+9=11 12 | 13 | # brute force: O(N^2) 14 | # two pointers: O(N) space:O(1) 15 | 16 | def pair_with_target_sum(arr, target_sum): 17 | left, right = 0, len(arr)-1 18 | while left < right: 19 | current_sum = arr[left] + arr[right] 20 | if current_sum == target_sum: 21 | return [left, right] 22 | elif current_sum > target_sum: 23 | right -= 1 24 | else: 25 | left += 1 26 | 27 | return [-1, -1] 28 | 29 | print(pair_with_target_sum([1, 2, 3, 4, 6],6)) 30 | print(pair_with_target_sum([2, 5, 9, 11], 11)) 31 | 32 | # hashTable: O(N) space:O(N) 33 | def pair_with_target_sum_method_2(arr, target_sum): 34 | nums = dict() 35 | for index, num in enumerate(arr): 36 | if target_sum - num in nums: 37 | return [nums[target_sum-num], index] 38 | else: 39 | nums[num] = index 40 | 41 | return [-1, -1] 42 | 43 | print(pair_with_target_sum_method_2([1, 2, 3, 4, 6],6)) 44 | print(pair_with_target_sum_method_2([2, 5, 9, 11], 11)) 45 | -------------------------------------------------------------------------------- /two pointers/quadruple sum to target.py: -------------------------------------------------------------------------------- 1 | # Given an array of unsorted numbers and a target number, 2 | # find all unique quadruplets in it, whose sum is equal to the target number. 3 | 4 | # Example: 5 | # Input: [4, 1, 2, -1, 1, -3], target=1 6 | # Output: [-3, -1, 1, 4], [-3, 1, 1, 2] 7 | # Explanation: Both the quadruplets add up to the target. 8 | 9 | def quadruple_sum_to_target(arr, target_sum): 10 | arr.sort() 11 | quadrupts = [] 12 | for i in range(len(arr)-3): 13 | if i > 0 and arr[i] == arr[i-1]: 14 | continue 15 | for j in range(i+1, len(arr)-2): 16 | if j > i+1 and arr[j] == arr[j-1]: 17 | continue 18 | search_pairs(arr, target_sum, i, j, quadrupts) 19 | return quadrupts 20 | 21 | def search_pairs(arr, target_sum, first, second, quadrupts): 22 | left, right = second + 1, len(arr) - 1 23 | while left < right: 24 | current_sum = arr[left] + arr[right] + arr[first] + arr[second] 25 | if current_sum == target_sum: 26 | quadrupts.append([arr[first], arr[second], arr[left], arr[right]]) 27 | left += 1 28 | right -= 1 29 | while left < right and arr[left] == arr[left - 1]: 30 | left += 1 31 | while left < right and arr[right] == arr[right + 1]: 32 | right -= 1 33 | elif current_sum > target_sum: 34 | right -= 1 35 | else: 36 | left += 1 37 | 38 | print(quadruple_sum_to_target([4, 1, 2, -1, 1, 1, -3], 1)) 39 | print(quadruple_sum_to_target([2, 0, -1, 1, -2, 2], 2)) -------------------------------------------------------------------------------- /two pointers/remove duplicates.py: -------------------------------------------------------------------------------- 1 | # Given an array of sorted numbers, remove all duplicates from it. 2 | # You should not use any extra space; after removing the duplicates in-place return the new length of the array. 3 | 4 | # Example: 5 | # Input: [2, 3, 3, 3, 6, 9, 9] 6 | # Output: 4 7 | # Explanation: The first four elements after removing the duplicates will be [2, 3, 6, 9]. 8 | 9 | # O(N) space:O(1) 10 | def remove_duplicates(arr): 11 | if len(arr) == 0: 12 | return 0 13 | 14 | next_non_duplicate = 0 15 | for i in range(len(arr)): 16 | if arr[next_non_duplicate-1] != arr[i]: 17 | arr[next_non_duplicate] = arr[i] 18 | next_non_duplicate += 1 19 | 20 | return next_non_duplicate 21 | 22 | print(remove_duplicates([2, 3, 3, 3, 6, 9, 9])) 23 | print(remove_duplicates([2, 2, 2, 11])) 24 | 25 | # Given an unsorted array of numbers and a target ‘key’, 26 | # remove all instances of ‘key’ in-place and return the new length of the array. 27 | 28 | # Example: 29 | # Input: [3, 2, 3, 6, 3, 10, 9, 3], Key=3 30 | # Output: 4 31 | # Explanation: The first four elements after removing every 'Key' will be [2, 6, 10, 9]. 32 | 33 | def remove_element(arr, key): 34 | next_pointer = 0 35 | for i in range(len(arr)): 36 | if arr[i] != key: 37 | arr[next_pointer] = arr[i] 38 | next_pointer += 1 39 | return next_pointer 40 | 41 | print(remove_element([3, 2, 3, 6, 3, 10, 9, 3], 3)) 42 | print(remove_element([2, 11, 2, 2, 1], 2)) -------------------------------------------------------------------------------- /two pointers/squaring a sorted array.py: -------------------------------------------------------------------------------- 1 | # Given a sorted array, 2 | # create a new array containing squares of all the number of the input array in the sorted order. 3 | 4 | # Example: 5 | # Input: [-2, -1, 0, 2, 3] 6 | # Output: [0, 1, 4, 4, 9] 7 | 8 | # O(N) space:O(N) 9 | 10 | def squaring_sorted_array(arr): 11 | left, right = 0, len(arr) - 1 12 | result = [0]*len(arr) 13 | count = len(arr) - 1 14 | while left < right: 15 | if abs(arr[left]) == abs(arr[right]): 16 | result[count] = arr[left] * arr[left] 17 | result[count - 1] = result[count] 18 | left += 1 19 | right -= 1 20 | count -= 2 21 | elif abs(arr[left]) > abs(arr[right]): 22 | result[count] = arr[left] * arr[left] 23 | count -= 1 24 | left += 1 25 | else: 26 | result[count] = arr[right] * arr[right] 27 | count -= 1 28 | right -= 1 29 | return result 30 | 31 | print(squaring_sorted_array([-2, -1, 0, 2, 3])) 32 | print(squaring_sorted_array([-3, -1, 0, 1, 2])) -------------------------------------------------------------------------------- /two pointers/triplet sum close to target.py: -------------------------------------------------------------------------------- 1 | # Given an array of unsorted numbers and a target number, 2 | # find a triplet in the array whose sum is as close to the target number as possible, return the sum of the triplet. 3 | # If there are more than one such triplet, return the sum of the triplet with the smallest sum. 4 | 5 | # Example: 6 | # Input: [-2, 0, 1, 2], target=2 7 | # Output: 1 8 | # Explanation: The triplet [-2, 1, 2] has the closest sum to the target. 9 | 10 | # sort:O(N*logN) searchpair:O(N) for each iteration-> O(N^2) space:O(N) for sorting 11 | 12 | # abs(X+Y+Z - target_sum) 13 | import math 14 | def triplet_sum_close_to_target(arr, target_sum): 15 | result = math.inf 16 | arr.sort() 17 | for i in range(len(arr)): 18 | if i > 0 and arr[i] == arr[i-1]: 19 | continue 20 | current_result = smallest_sum(arr, target_sum-arr[i], i+1) 21 | result = min(result, current_result) 22 | return abs(result - target_sum) 23 | 24 | def smallest_sum(arr, target_sum, left): 25 | right = len(arr) - 1 26 | result = math.inf 27 | while left < right: 28 | current_sum = arr[left] + arr[right] 29 | now_sum =abs(target_sum-current_sum) 30 | result = min(result, now_sum) 31 | if current_sum == target_sum: 32 | return 0 33 | elif current_sum > target_sum: 34 | right -= 1 35 | else: 36 | left += 1 37 | return result 38 | 39 | print(triplet_sum_close_to_target([-2, 0, 1, 2],2)) 40 | print(triplet_sum_close_to_target([-3, -1, 1, 2],1)) 41 | print(triplet_sum_close_to_target([1, 0, 1, 1],100)) 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /two pointers/triplet sum to zero.py: -------------------------------------------------------------------------------- 1 | # Given an array of unsorted numbers, find all unique triplets in it that add up to zero. 2 | 3 | # Example: 4 | # Input: [-3, 0, 1, 2, -1, 1, -2] 5 | # Output: [-3, 1, 2], [-2, 0, 2], [-2, 1, 1], [-1, 0, 1] 6 | # Explanation: There are four unique triplets whose sum is equal to zero. 7 | 8 | # sort:O(N*logN) searchpair:O(N) for each iteration-> O(N^2) space:O(N) for sorting 9 | def search_triplets(arr): 10 | triplets = [] 11 | arr.sort() 12 | for i in range(len(arr)): 13 | if i > 0 and arr[i] == arr[i-1]: 14 | continue 15 | search_pair(arr, -arr[i],i+1,triplets) 16 | return triplets 17 | 18 | def search_pair(arr, target_sum, left, triplets): 19 | right = len(arr) - 1 20 | while left < right: 21 | current_sum = arr[left] + arr[right] 22 | if current_sum == target_sum: 23 | triplets.append([-target_sum, arr[left], arr[right]]) 24 | left += 1 25 | right -= 1 26 | while left < right and arr[left] == arr[left - 1]: 27 | left += 1 28 | while left < right and arr[right] == arr[right + 1]: 29 | right -= 1 30 | elif current_sum > target_sum: 31 | right -= 1 32 | else: 33 | left += 1 34 | 35 | print(search_triplets([-3, 0, 1, 2, -1, 1, -2])) 36 | print(search_triplets([-5, 2, -1, -2, 3])) 37 | -------------------------------------------------------------------------------- /two pointers/triplet with smaller sum.py: -------------------------------------------------------------------------------- 1 | # Given an array arr of unsorted numbers and a target sum, 2 | # count all triplets in it such that arr[i] + arr[j] + arr[k] < target where i, j, and k are three different indices. 3 | # Write a function to return the count of such triplets. 4 | 5 | # Example: 6 | # Input: [-1, 0, 2, 3], target=3 7 | # Output: 2 8 | # Explanation: There are two triplets whose sum is less than the target: [-1, 0, 3], [-1, 0, 2] 9 | 10 | # sort:O(N*logN) searchpair:O(N) for each iteration-> O(N^2) space:O(N) for sorting 11 | def triplet_with_smaller_sum(arr, target_sum): 12 | result = 0 13 | arr.sort() 14 | for i in range(len(arr)): 15 | result += search_pair(arr, target_sum-arr[i], i+1) 16 | return result 17 | 18 | def search_pair(arr, target_sum, left): 19 | right = len(arr) - 1 20 | count = 0 21 | while left < right: 22 | current_sum = arr[left] + arr[right] 23 | if current_sum < target_sum: 24 | count += right - left 25 | left += 1 26 | else: 27 | right -= 1 28 | return count 29 | 30 | print(triplet_with_smaller_sum([-1, 0, 2, 3],3)) 31 | print(triplet_with_smaller_sum([-1, 4, 2, 1, 3],5)) 32 | 33 | # follow up : 34 | # Problem: Write a function to return the list of all such triplets instead of the count. 35 | # How will the time complexity change in this case? 36 | 37 | # O(N^3) 38 | def triplet_with_smaller_sum_2(arr, target_sum): 39 | result = [] 40 | arr.sort() 41 | for i in range(len(arr)): 42 | search_pair_2(arr, target_sum-arr[i], i+1, result) 43 | return result 44 | 45 | def search_pair_2(arr, target_sum, left, result): 46 | right = len(arr) - 1 47 | first = left - 1 48 | while left < right: 49 | current_sum = arr[left] + arr[right] 50 | if current_sum < target_sum: 51 | for i in range(right, left, -1): 52 | result.append([arr[first], arr[left], arr[i]]) 53 | left += 1 54 | else: 55 | right -= 1 56 | return result 57 | 58 | 59 | print(triplet_with_smaller_sum_2([-1, 0, 2, 3],3)) 60 | print(triplet_with_smaller_sum_2([-1, 4, 2, 1, 3],5)) --------------------------------------------------------------------------------