├── .gitignore ├── 1d_dynamic_programming ├── climbing_stairs.py ├── coin_change.py ├── decode_ways.py ├── house_robber.py ├── house_robber_ii.py ├── longest_increasing_subsequence.py ├── longest_palindromic_substring.py ├── max_product_subarray.py ├── palindromic_substrings.py └── word_break.py ├── LICENSE ├── README.md ├── arrays_and_hashing ├── contains_duplicate.py ├── encode_decode_strings.py ├── group_anagrams.py ├── longest_consecutive_sequence.py ├── product_of_array_except_self.py ├── top_k_frequent_elements.py ├── two_sum.py ├── valid_anagram.py └── valid_sudoku.py ├── backtracking ├── combination_sum.py └── word_search.py ├── binary_search └── min_in_rotated_array.py ├── bit_manipulation ├── counting_bits.py ├── missing_number.py ├── number_of_1_bits.py └── reverse_bits.py ├── graphs ├── clone_graph.py ├── course_schedule.py ├── number_of_islands.py └── pacific_atlantic_water_flow.py ├── greedy ├── jump_game.py └── maximum_subarray.py ├── intervals ├── insert_interval.py └── merge_intervals.py ├── linked_lists ├── linked_list_cycle.py ├── merge_k_sorted_lists.py ├── merge_two_sorted_lists.py ├── remove_nth_node_from_end_of_list.py └── reverse_linked_list.py ├── questions_list.md ├── setup.py ├── sliding_window ├── best_buy_sell_stock.py ├── longest_repeating_character_replacement.py ├── longest_substring_without_repeating_chars.py ├── minimum_window_substring.py ├── permutation_in_strings.py └── sliding_window_maximum.py ├── stack └── valid_parentheses.py ├── trees ├── binary_tree_level_order_traversal.py ├── invert_binary_tree.py ├── lowest_common_ancestor_of_bst.py ├── max_depth_of_binary_tree.py ├── same_tree.py ├── subtree_of_another_tree.py └── valid_bst.py ├── tries ├── add_search_word.py ├── implement_trie_prefix_tree.py └── word_search_ii.py ├── two_pointers ├── container_with_most_water.py ├── three_sum.py ├── trapping_rain_water.py ├── two_sum_ii_input_sorted.py └── valid_palindrome.py └── utils └── get_time_complexity.py /.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 | .DS_Store 131 | .vscode/settings.json 132 | .vscode/* 133 | /.idea/.gitignore 134 | /.idea/git_toolbox_blame.xml 135 | /.idea/material_theme_project_new.xml 136 | /.idea/misc.xml 137 | /.idea/modules.xml 138 | /.idea/neetcode-solutions.iml 139 | /.idea/inspectionProfiles/profiles_settings.xml 140 | /.idea/inspectionProfiles/Project_Default.xml 141 | /.idea/vcs.xml 142 | /.idea/* 143 | -------------------------------------------------------------------------------- /1d_dynamic_programming/climbing_stairs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https//leetcode.com/problems/climbing-stairs/ 3 | ''' 4 | 5 | class Solution: 6 | def climbStairs(self, n: int) -> int: 7 | 8 | ''' Solve using DP Bottom up approach. i.e. solve subproblems until you reach the end. 9 | This is basicially a Fibonacci series. Create it for n iterations and then the result is in the first variable. 10 | ''' 11 | 12 | # Start with both values as 1. 13 | a, b = 1, 1 14 | 15 | for _ in range(n): 16 | # Assign new 'a' to last value i.e. 'b' and 17 | # now 'b' to 'a+b' (to create fibonacci series) 18 | a, b = b, a+b 19 | 20 | # The number of distinct ways to climb the stairs is stored in 'a' 21 | # How? 22 | return a 23 | 24 | ''' Another way to write the same code ''' 25 | # one, two = 1, 1 26 | 27 | # for i in range(n - 1): 28 | # # Doesn't matter how you swap, as long as it is consistent 29 | # one, two = one + two, one 30 | 31 | # return one 32 | 33 | -------------------------------------------------------------------------------- /1d_dynamic_programming/coin_change.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/coin-change/ 3 | ''' 4 | 5 | class Solution: 6 | def coinChange(self, coins: List[int], amount: int) -> int: 7 | ''' Use a bottom up approach i.e. DP approach -> Time Complexity = O (amount * num of coins)''' 8 | 9 | # Init `dp` of size `amount` with each initial set at max (can use either infinity or amount + 1 for this) 10 | dp = [amount+1]*(amount+1) 11 | 12 | # Base case - amount 0 will require zero coins 13 | dp[0] = 0 14 | 15 | # Iterate over the amount, starting at 1 (bottom-up) 16 | for a in range(1, amount+1): 17 | # Also iterate over the coins available 18 | for c in coins: 19 | ''' For each (a, c) combination check if we have a match and store the 20 | min value so far (for the amount `a`) in the dp ''' 21 | 22 | # If the current amount 'a' minus the current coin value is valid (i.e. not less than 0), 23 | # then we store the minimunm number of coins required in the dp 24 | if a - c >= 0: 25 | # Update the value in 'dp[a]' 26 | # `1` is added since we use the current coin `c` for getting the current result. 27 | # So dp[a] will store the value of the min of itself and the new value 28 | # i.e. 1+dp[a-c] (using the previous value from the DP) 29 | dp[a] = min(dp[a], 1 + dp[a-c]) 30 | 31 | # Return the solution stored in dp[amount] only if it is not the default value we had saved, 32 | # cause if default value in dp[amount], then we could not find a solution so we return -1 33 | return dp[amount] if dp[amount] != amount + 1 else -1 34 | -------------------------------------------------------------------------------- /1d_dynamic_programming/decode_ways.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/decode-ways/ 3 | ''' 4 | 5 | class Solution: 6 | def numDecodings(self, s: str) -> int: 7 | '''Approach 1: Brute force - more space complexity''' 8 | # # Split into subproblems 9 | # dp = {len(s) : 1} 10 | 11 | # def dfs(i): 12 | # # Base case 1 - already in dp 13 | # if i in dp: 14 | # return dp[i] 15 | 16 | # # Base case 2 - invalid string 17 | # if s[i] == "0": 18 | # return 0 19 | 20 | # # Check for next digit only 21 | # res = dfs(i+1) 22 | 23 | # # We also have to check if we can take the next two digits 24 | # # Conditions - i+1 exists, and the two digits problem is between 10 to 26 25 | # if (i+1 < len(s)) and (s[i] == "1" or 26 | # s[i] == "2" and s[i+1] in "0123456" ): 27 | # # If yes, then check for the two digits problem 28 | # res += dfs(i+2) 29 | 30 | # # Remember to store the result in the dp 31 | # dp[i] = res 32 | # return res 33 | 34 | # # Run the dfs starting from index 0 35 | # return dfs(0) 36 | 37 | '''Approach 2: Use Dynamic Programming''' 38 | # Split into subproblems, save in a DP of size = len(s), init all values to 1 39 | dp = {len(s) : 1} 40 | 41 | # Here we iterate in reverse (solve subproblems) and store the results in dp, building up to index 0 42 | for i in range(len(s)-1, -1, -1): 43 | 44 | # Base cases where the answer is "0" i.e. invalid character 45 | if s[i] == "0": 46 | dp[i] = 0 47 | 48 | # Here, check for one digit case (only one char) 49 | # Save the value stored dp (from a previous subproblem - memoization) 50 | else: 51 | dp[i] = dp[i+1] 52 | 53 | # Also, check for the two digit problem 54 | # Conditions - i+1 should exist, and if two digits problem is between 10 to 26 (since we have only 26 alphabets) 55 | # so anything 27 onwards is not an alphabet (will be an invalid character/two chararcter string) 56 | if (i+1 < len(s)) and (s[i] == "1" or 57 | s[i] == "2" and s[i+1] in "0123456"): 58 | dp[i] += dp[i+2] 59 | 60 | # Return the result stored in dp at index 0 (since we iterate in reverse, result will be saved in dp[0] in the end) 61 | return dp[0] 62 | -------------------------------------------------------------------------------- /1d_dynamic_programming/house_robber.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/house-robber/ 3 | ''' 4 | 5 | class Solution: 6 | def rob(self, nums: List[int]) -> int: 7 | # Look at the max value you can rob up until the last two nodes of the current node 8 | rob1, rob2 = 0, 0 9 | 10 | # List looks like this -> [rob1, rob2, n, n+1, ...] 11 | # Remember: cannot rob adjacent houses 12 | for n in nums: 13 | # Save the max value of next robbery in a temp variable (since we need to swap) 14 | newMaxRob = max(rob1+n, rob2) 15 | # Move rob1 to value of rob2 AND update rob2 to newMaxRob value! 16 | rob1 = rob2 17 | rob2 = newMaxRob 18 | 19 | # The final max value is stored in 'rob2' (which has the value of newMaxRob) 20 | return rob2 21 | -------------------------------------------------------------------------------- /1d_dynamic_programming/house_robber_ii.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/house-robber-ii/ 3 | ''' 4 | 5 | class Solution: 6 | def rob(self, nums: List[int]) -> int: 7 | ''' 8 | Similar to Robber 1, but now the list is circular (first and last elements are connected)! 9 | So, reuse the solution and run the robber 1 method for all values except first and last in the list. 10 | i.e. exclude nums[1:] and nums[:-1] (since the first and last element are connected! i.e. circular) 11 | ''' 12 | 13 | # Edge case - if only one house, then return the value of that house 14 | if len(nums) == 1: 15 | return nums[0] 16 | 17 | # Just run the rob function excluding the first and last elements in 'nums' 18 | return max(self.robNonCircular(nums[1:]), self.robNonCircular(nums[:-1])) 19 | 20 | # Helper Function to find max rob value for the given list of houses 'arr' 21 | def robNonCircular(self, arr): 22 | # Same approach as https://leetcode.com/problems/house-robber/ 23 | rob1, rob2 = 0, 0 24 | for n in arr: 25 | newRob = max(rob1+n, rob2) 26 | rob1 = rob2 27 | rob2 = newRob 28 | 29 | return rob2 -------------------------------------------------------------------------------- /1d_dynamic_programming/longest_increasing_subsequence.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/longest-increasing-subsequence/ 3 | ''' 4 | 5 | class Solution: 6 | def lengthOfLIS(self, nums: List[int]) -> int: 7 | 8 | ''' DP Approach -> Time Complexity = O(n^2) ''' 9 | 10 | n = len(nums) 11 | 12 | # Create array of 1 with length = len(nums) 13 | # This DP maintains the length of increasing sequences at each index 14 | dp = [1 for _ in range(n)] 15 | 16 | # Since this is a DP approach, we iterate in reverse i.e. Bottom-up 17 | # `i` iterates over len(nums) to 0 (in reverse) 18 | for i in range(n, -1, -1): 19 | # `j` iterates over all elements after the ith index (until `n`) 20 | for j in range(i+1, n): 21 | # Check if 'ith' element is lesser than every jth element 22 | # i.e. check if it fits the increasing sub-sequence 23 | if nums[i] < nums[j]: 24 | # If it does, store the subseq with the longest length 25 | # Note: the 1 is added because we include the current 26 | # element in the subsequence 27 | dp[i] = max(dp[i], 1 + dp[j]) 28 | 29 | # Return the length of the longest sequence 30 | return max(dp) -------------------------------------------------------------------------------- /1d_dynamic_programming/longest_palindromic_substring.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/longest-palindromic-substring/ 3 | ''' 4 | 5 | class Solution: 6 | def longestPalindrome(self, s: str) -> str: 7 | ''' 8 | Check for palindrome by starting in the middle and expanding outwards. 9 | We do this by mid = ith index in the string ('i' is each char in the string). 10 | Remember to check for odd and even both, since the mid will be different in both cases. 11 | ''' 12 | 13 | longest = "" 14 | 15 | for i in range(len(s)): 16 | # Consider `i`th index as the middle element and check if palindrome by moving outwards 17 | # We consider both cases, i.e. `i` is odd and `i` is even 18 | 19 | # For odd length palindromes -> l == r == i, i.e. same middle element 20 | odd = self.palindromeAt(i, i, s) 21 | 22 | # For even length palindromes -> l = i and r = i+1 23 | even = self.palindromeAt(i, i+1, s) 24 | 25 | # Update the `longest` palindrome string from odd and even checks 26 | longest = max(longest, even, odd, key=len) 27 | 28 | # Return the result 29 | return longest 30 | 31 | # Helper method to find palindrom between two pointers `l` and `r` in string `s` 32 | def palindromeAt(self, l, r, s): 33 | # Iterate with two pointers outwards, until the characters don't match i.e. end of palindromic substring 34 | # Start in the middle and move pointers in opposite directions 35 | while l >= 0 and r < len(s) and s[l] == s[r]: 36 | l -= 1 37 | r += 1 38 | 39 | # When the comparison stops, you return the palidrome string 40 | # i.e. the string in between `l` and `r` pointers 41 | return s[l+1:r] 42 | -------------------------------------------------------------------------------- /1d_dynamic_programming/max_product_subarray.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: http://www.lintcode.com/en/problem/maximum-product-subarray/ 3 | ''' 4 | 5 | class Solution: 6 | def maxProduct(self, nums: List[int]) -> int: 7 | ''' 8 | Maintain a current max and min since we have negative values in the array as well. 9 | So, a negative multiplied by min value (negative mostly), will result in a positive product 10 | And vice-versa - a positive value multiplied by a max value (mostly positive), will 11 | also give a positive product 12 | ''' 13 | 14 | # init `res` with max value in array, for cases where array length is 1 15 | res = max(nums) 16 | # `currMax` and `currMin` start with value 1 (not zero since we are multiplying) 17 | currMax, currMin = 1, 1 18 | 19 | # Iterate over all the numbers in the array `nums` 20 | for n in nums: 21 | ''' 22 | Store the currMax before recomputing it! 23 | Why? Notice how currMax is changed here, before using for currMin 24 | 25 | -> currMax = max(currMax * n, currMin * n, n) 26 | -> currMin = min(currMax * n, currMin * n, n) 27 | ''' 28 | 29 | # So, we save the value of currMax and then use it for the calculation of currMin 30 | temp = currMax * n 31 | currMax = max(currMax * n, currMin * n, n) 32 | currMin = min(temp, currMin * n, n) 33 | 34 | # Save the max value b/w currMax, currMin and result -> this is the maxProduct so far 35 | res = max(res, currMax, currMin, res) 36 | 37 | # Return the final maxProduct value 38 | return res 39 | -------------------------------------------------------------------------------- /1d_dynamic_programming/palindromic_substrings.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: http://www.lintcode.com/en/problem/palindromic-substrings/ 3 | ''' 4 | 5 | class Solution: 6 | def countSubstrings(self, s: str) -> int: 7 | ''' 8 | Approach 1: Count even and odd in different loops and combine the results from both 9 | Time Complexity: Linear O(n) (recheck but pretty sure it's correct!) 10 | ''' 11 | numPalis = 0 12 | n = len(s) 13 | 14 | # Count odd length palidromes 15 | for i in range(n): 16 | # For odd length palindromes -> l == r == i 17 | numPalis += self.countPalindromes(i, i, s) 18 | # For even length palindromes -> l = i and r = i+1 19 | numPalis += self.countPalindromes(i, i+1, s) 20 | 21 | return numPalis 22 | 23 | # Helper function to find palindromes between two pointers `l` and `r`, given a string `s` 24 | def countPalindromes(self, l, r, s): 25 | count = 0 26 | # Start at the same midpoint and expand outwards (in opposite directions) 27 | while l >= 0 and r < len(s) and s[l] == s[r]: 28 | count += 1 29 | l -= 1 30 | r += 1 31 | 32 | return count 33 | 34 | ''' 35 | Approach 2: Count even and odd length palindromes in the same loop - Same logic, seems a little more complicated 36 | ''' 37 | # N = len(s) 38 | # result = 0 39 | 40 | # # Iterate through all substrings of `s` 41 | # # Why 2N? 42 | # for i in range(2*N-1): 43 | # # Find the mid of the current substring and move in opposite directions 44 | # left = i // 2 # mid 45 | # right = left + (i % 2) # mid + 1 for even string, and mid for odd string 46 | 47 | # # Loop to find palindrome strings using `left` and `right` i.e. mid and mid-1 48 | # while left >= 0 and right < N and s[left] == s[right]: 49 | # result += 1 50 | # left -= 1 51 | # right += 1 52 | 53 | # # Result is the total number of palindrome substrings 54 | # return result 55 | -------------------------------------------------------------------------------- /1d_dynamic_programming/word_break.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/word-break/ 3 | ''' 4 | 5 | class Solution: 6 | def wordBreak(self, s: str, wordDict: List[str]) -> bool: 7 | ''' Use DP i.e. bottom-up approach to solve this ''' 8 | 9 | # DP stores the possibility of solution at every index in the string 10 | dp = [False] * (len(s) + 1) 11 | 12 | # Base case where if we reach the end of the string, then we can segment the given string 13 | # (iteration starts in reverse) so after this i.e. from `len(s) - 1` 14 | dp[len(s)] = True 15 | 16 | # Iterate bottom-up (i.e. in reverse) 17 | for i in range(len(s)-1, -1, -1): 18 | 19 | # Iterate over every word in 'wordDict' 20 | for w in wordDict: 21 | 22 | # Check if `ith index + word length` is within the string bounds 23 | # AND if it matches the word in the dict 24 | if (i + len(w)) <= len(s) and s[i: i + len(w)] == w: 25 | # If it does, then you make the `index+word len` equal to dp[i] i.e. True 26 | dp[i] = dp[i + len(w)] 27 | 28 | # We can move to the next word in 'wordDict', if we 29 | # at least one word broken for curr 'w' at index 'i' 30 | if dp[i]: 31 | break 32 | 33 | # Check if we can word break at index 0, where the final boolean value is saved (since we iterated in reverse) 34 | return dp[0] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Darpan Jain 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 | # Neetcode Solutions + Explanations 2 | 3 | ## What is this repo? 4 | 5 | This is a list of 150 solutions _(in progress)_ for the list by [Neetcode](https://neetcode.io/practice). The solutions are in Python3 and contain intuitive explanations for each code block. 6 | 7 | ## How to use this repo? 8 | - I recommend using the solutions here as a pointer for your interview preparation for Software Engineering roles. 9 | - I would recommend you try to solve the questions on your own first and then refer to the solutions here. This will help you understand the concepts better. 10 | - You can find the list of questions that are covered in this repo [here](questions_list.md) 11 | 12 | _Pro tip_: Try to explain the approach to another person or to yourself (out loud). You'd be surprised how much you learn by doing this! 13 | 14 | ## How to contribute? 15 | 16 | - If you have a better solution to any of the questions, please feel free to open a PR. 17 | - I will review it and merge it if it is better than the current solution. 18 | - Please do remember to add a neat explanation for your solution with the time and space complexities. 19 | 20 | ## Issues 21 | If you face any issues or have any suggestions, please feel free to open an issue. I will try to get back to you as soon as possible. 22 | 23 | ## License 24 | [MIT License](https://choosealicense.com/licenses/mit/) 25 | 26 | ## More questions? 27 | If you want to contact me, you can reach me via [email](mailto:darpannjainn@gmail.com) or visit the [`Reach Out`](https://darpanjain.com/#contact-section) section on [my website](https://darpanjain.com). 28 | -------------------------------------------------------------------------------- /arrays_and_hashing/contains_duplicate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/contains-duplicate/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def containsDuplicate(nums: List[int]) -> bool: 9 | 10 | """ Approach 1: Use set since it only holds unique values """ 11 | 12 | # If length is 1, then obvs it is a unique number! 13 | if len(nums) < 2: 14 | return False 15 | 16 | # The length of `set(nums)` will not change if all elements are unique 17 | return len(nums) != len(set(nums)) 18 | 19 | """ Approach 2: Use hashmap and create counter. Exit when count of a value goes above 1 """ 20 | # counter = {} 21 | # for i in nums: 22 | # if i in counter: # Number already exists in counter, therefore duplicate 23 | # return True 24 | # # Else add the char and init its counter 25 | # else: 26 | # counter[i] = 1 27 | 28 | # return False 29 | -------------------------------------------------------------------------------- /arrays_and_hashing/encode_decode_strings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/encode-and-decode-strings/ (Leetcode Premium) 3 | OR 4 | https://neetcode.io/problems/string-encode-and-decode (Free by Neetcode) 5 | """ 6 | 7 | 8 | class Solution: 9 | 10 | def encode(self, input_str: str): 11 | """ 12 | Encode the individual strings as a single string by adding an integer and delimiter to the start of each string. 13 | 14 | The integer indicates number of characters in the current string, 15 | and the delimiter ('#') will separate the integer and the actual string. 16 | 17 | Eg. ["lint","code","love","you"] will become "4#lint4#code4#love3#you" as the encoded string. 18 | """ 19 | 20 | encoded_str = "" 21 | 22 | for s in input_str: 23 | encoded_str += str(len(s)) + '#' + s 24 | 25 | return encoded_str 26 | 27 | def decode(self, encoded_str: str): 28 | """ 29 | We decode the encoded string as described in the `encode` method. 30 | """ 31 | 32 | decoded_str = [] 33 | # We use pointer `i` to keep track of where we are in the encoded str 34 | i = 0 35 | 36 | # Keep iterating until you go through the entire encoded string 37 | while i < len(encoded_str): 38 | # Introduce another pointer to find the delimiter we encoded 39 | j = i 40 | 41 | while encoded_str[j] != '#': 42 | # We keep incrementing `j` until we find the delimiter 43 | j += 1 44 | 45 | # Here, we have encountered a delimiter. So we start decoding... 46 | 47 | # Get the length of the current string 48 | length = int(encoded_str[i:j]) 49 | # Splice the encoded string and append the extracted string to `decoded_strs` 50 | decoded_str.append(encoded_str[j+1: j+1+length]) 51 | 52 | # Once you have the string extracted, update `i` to resume from the next string 53 | i = j+1+length 54 | 55 | return decoded_str 56 | -------------------------------------------------------------------------------- /arrays_and_hashing/group_anagrams.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/group-anagrams/ 3 | """ 4 | 5 | from collections import defaultdict 6 | from typing import List 7 | 8 | 9 | class Solution: 10 | def groupAnagrams(self, strs: List[str]) -> List[List[str]]: 11 | 12 | """ 13 | Approach: Group strings which have the same letter counts in a `result` dict. 14 | Similar approach to 'Valid Anagrams' problem. 15 | 16 | Time Complexity: O (M * N) 17 | where, M = number of strings 18 | N = average length of a string 19 | """ 20 | 21 | # Dict to map character counts to list of Anagrams 22 | # Why `defaultdict`? If key doesn't exist, it creates one, with a default value of an empty `list`. 23 | result = defaultdict(list) 24 | 25 | for s in strs: 26 | # List for char count for each of the 26 letters 27 | count = [0] * 26 28 | 29 | # Populate character frequency for current string 30 | for c in s: 31 | # Increment counter for current character using (ASCII of current letter) - (ASCII of 'a') 32 | char_idx = ord(c) - ord("a") 33 | count[char_idx] += 1 34 | 35 | # Add strings with similar counts to the result dict 36 | # Why 'tuple(count)'? Since Python doesn't allow lists as keys 37 | result[tuple(count)].append(s) 38 | 39 | # Return only the values of the result dict i.e. the groups of anagrams 40 | return result.values() 41 | -------------------------------------------------------------------------------- /arrays_and_hashing/longest_consecutive_sequence.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/longest-consecutive-sequence/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | 10 | def longestConsecutive(self, nums: List[int]) -> int: 11 | """ 12 | Approach: Notice that a start of a sequence has no number before it. 13 | So we check start of a seq by checking if there is a number before the element, 14 | and end of sequence by checking if we have a number after the element. 15 | Do this for all elements in the list and keep track of the longest sequence. 16 | 17 | Time and Space Complexity: O(N) 18 | """ 19 | 20 | # Create a set out of the list of numbers - so that we don't have any duplicates 21 | # This is an important consideration for edge cases, especially when `len(nums)` is extremely large. 22 | num_set = set(nums) 23 | longest = 0 24 | 25 | for curr in nums: 26 | 27 | # Check if the current number is the start of a sequence 28 | # by checking if the `curr-1` number exists 29 | if curr-1 not in num_set: 30 | 31 | # If not, then this is the start of a sequence 32 | # Initialize the length of the current sequence 33 | seq_len = 1 34 | 35 | # Keep incrementing `seq_len` until the next number exists 36 | while (curr + seq_len) in num_set: 37 | seq_len += 1 38 | 39 | # Once you have reached the end of the sequence (when the `while` loop breaks), 40 | # Update the length of the longest sequence 41 | longest = max(longest, seq_len) 42 | 43 | return longest 44 | -------------------------------------------------------------------------------- /arrays_and_hashing/product_of_array_except_self.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/product-of-array-except-self/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | def productExceptSelf(self, nums: List[int]) -> List[int]: 10 | 11 | """ 12 | Approach: Use a prefix and postfix for each element and multiply 13 | the two values to get the product for the ith element. 14 | `prefix` -> stores product of all values before current element 15 | `postfix` -> stores product of all values after current element 16 | 17 | Intuition: prefix[i] * postfix[i] = product of all elements except self[i] 18 | 19 | Time Complexity: O(N), where N is the number of elements in the array `nums` 20 | Space Complexity: O(1), since we don't create any new arrays for storing prefix and postfix values 21 | """ 22 | 23 | # Store the length of the array since it will be used multiple times 24 | n = len(nums) 25 | 26 | # Init a result array with all 1s 27 | result = [1] * n 28 | # Init a prefix value with 1 (Why not `0`? Since we have to find the product!) 29 | prefix = 1 30 | 31 | """ 32 | Step 1 - Calculating Prefix of each element and store it in the `result` array. Each element's prefix is stored 33 | in the next element of the results array, i.e., `result[i]` stores the prefix of `nums[i+1]` 34 | """ 35 | # Store the prefix of ith element in `i+1` position of results array 36 | for i in range(n): 37 | # Store the previous element's prefix in the results array 38 | result[i] = prefix 39 | # Update the prefix for the next element 40 | prefix *= nums[i] 41 | 42 | # DEBUG: Now the `result` array has all the prefix values for each element stored. 43 | # Trying printing this on a few test cases to understand the intuition behind the solution. 44 | # print(result) 45 | 46 | """ Step 2 - Calculating Postfix and multiplying with prefix """ 47 | # Now, we multiply the postfix of each element with the prefix (stored in `result` array) 48 | 49 | # Again, start with an initial postfix value of 1 (remember: we are finding product) 50 | postfix = 1 51 | 52 | # Iterate in reverse since postfix (since we want every element's POSTfix value, so we start from the end) 53 | for i in range(n-1, -1, -1): 54 | # Update and store value in ith position of `res` by multiplying with postfix value 55 | result[i] *= postfix 56 | # Update postfix value for the next element 57 | postfix *= nums[i] 58 | 59 | ''' Step 3 - Return final `result` array ''' 60 | return result 61 | -------------------------------------------------------------------------------- /arrays_and_hashing/top_k_frequent_elements.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/top-k-frequent-elements/ 3 | """ 4 | 5 | from collections import defaultdict, Counter 6 | from typing import List 7 | 8 | 9 | class Solution: 10 | def topKFrequent(self, nums: List[int], k: int) -> List[int]: 11 | """ 12 | Approach: Use Bucket Sort - create `dict` with key as the frequency and the values as the elements that 13 | occurred index number of times. 14 | Time & Space complexity -> O(N) i.e. linear time 15 | """ 16 | 17 | # Create a `defaultdict` with default value as a `list`. 18 | # In this, we store the element count as `key and the values as list of all elements with that 19 | # count or frequency of occurrence. 20 | frq = defaultdict(list) 21 | 22 | # Count the frequency of elements in `nums` and store in the dict (as defined above) 23 | # If the interviewer permits, you can also use `collections.Counter` to get the same result. 24 | for key, count in Counter(nums).items(): 25 | frq[count].append(key) 26 | 27 | # Now we go through the frequency list and iterate until we have the top K frequent elements 28 | result = [] 29 | 30 | # We iterate in reverse since we need the top most occurring element first 31 | # Why iterate over `len(nums)`? Since the max freq of an element can't be 32 | # more than the length of the list 33 | for num_occurrence in reversed(range(len(nums)+1)): 34 | # Extend adds the element at index 0 instead of the end of the list 35 | # The element being added is the element that occurs `num_occurrence` times 36 | result.extend(frq[num_occurrence]) 37 | # print(f"{num_occurrence}: {res}") 38 | 39 | # We stop once we have the top-K elements 40 | if len(result) >= k: 41 | return result[:k] 42 | 43 | return result[:k] 44 | -------------------------------------------------------------------------------- /arrays_and_hashing/two_sum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/two-sum/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | 10 | def twoSum(self, nums: List[int], target: int) -> List[int]: 11 | 12 | # Dictionary to store elements/numbers in `nums` as keys and their indices as value 13 | d = {} 14 | 15 | for idx, num in enumerate(nums): 16 | # If `target - curr num` is already parsed (and in the dictionary), you have a Two Sum pair! 17 | if target-num in d: 18 | return [d[target-num], idx] 19 | 20 | # Else, you keep saving the number (as key) and its index (as value) 21 | d[num] = idx 22 | 23 | # No final return statement needed since a solution is guaranteed (as per the problem statement). 24 | -------------------------------------------------------------------------------- /arrays_and_hashing/valid_anagram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/valid-anagram/ 3 | """ 4 | 5 | from collections import defaultdict 6 | 7 | 8 | class Solution: 9 | def isAnagram(self, s: str, t: str) -> bool: 10 | """ Approach 1: Compare the count of the characters in each string """ 11 | 12 | # If lengths don't match, then `s` and `t` definitely won't be anagrams 13 | if len(s) != len(t): 14 | return False 15 | 16 | # Define `defaultdict` to store count of each letter 17 | # Note: using `defaultdict(int)` will have a default value of integer zero if a key does not exist. 18 | count = defaultdict(int) 19 | 20 | # Iterate through first string `s` and add count each letter's occurrence 21 | for i in s: 22 | count[i] += 1 23 | 24 | # Iterate through second string `t` and decrement for each count of the letter 25 | for j in t: 26 | count[j] -= 1 27 | 28 | # If any character's count < 0, that means that it didn't occur in 1st string (`s`) 29 | # This would be a deal-breaker i.e. the strings are not anagrams! 30 | if count[j] < 0: 31 | return False 32 | 33 | # If all goes well (the for loop through `t` completes), then they are anagrams! 34 | return True 35 | 36 | """ 37 | Approach 2: Same as 1, but using `collections.Counter` 38 | `collections.Counter` returns a dictionary with elements as keys and their counts as values. 39 | If `s` and `t` are anagrams, then the count of each character in both strings will be the same, and hence the equality will be True! 40 | """ 41 | # return collections.Counter(s) == collections.Counter(t) 42 | -------------------------------------------------------------------------------- /arrays_and_hashing/valid_sudoku.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/valid-sudoku/ 3 | """ 4 | 5 | import collections 6 | from typing import List 7 | 8 | 9 | def isValidSudoku(self, board: List[List[str]]) -> bool: 10 | """ 11 | Approach: Check for duplicates in every row, column and 3x3 grid (sub_squares) 12 | by maintaining a hashset for each row, col and sub-square. 13 | 14 | Time Complexity: O(9^2) = O(81) i.e. the size of the 9x9 board 15 | """ 16 | 17 | # Hashset to record all values in each row, col and 3x3 sub-square 18 | rows = collections.defaultdict(set) 19 | cols = collections.defaultdict(set) 20 | # For squares, the key will be the location of the sub-square on the 9x9 board 21 | # represented by (r//3, c//3) -> using the board's (r, c) and int division by 3, 22 | # we can find the location of the 3x3 sub-square 23 | squares = collections.defaultdict(set) 24 | 25 | # Iterate through the board and look for duplicates 26 | for r in range(9): 27 | for c in range(9): 28 | # First check if the current position has a number. If not, skip iteration. 29 | if board[r][c] == ".": 30 | continue 31 | 32 | # Now, check if the current element already exists in the hashsets 33 | # Note the key being used for `squares` -> integer division by 3 to get the sub-square's location 34 | if board[r][c] in rows[r] or board[r][c] in cols[c] or board[r][c] in squares[(r//3, c//3)]: 35 | return False 36 | 37 | # If current element not a duplicate, add it to the hashsets 38 | rows[r].add(board[r][c]) 39 | cols[c].add(board[r][c]) 40 | squares[(r//3, c//3)].add(board[r][c]) 41 | 42 | # If not duplicates found in the entire board, it's a Valid Sudoku board! 43 | return True 44 | -------------------------------------------------------------------------------- /backtracking/combination_sum.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/combination-sum/ 3 | ''' 4 | 5 | class Solution: 6 | def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: 7 | ''' Perform a DFS on the given list of candidates ''' 8 | 9 | res = [] 10 | self.dfs(candidates, target, [], res) 11 | return res 12 | 13 | # Helper function for DFS 14 | def dfs(self, nums, target, curr_path, res): 15 | # Base case - Return since target not an exact match 16 | if (target < 0): 17 | return 18 | 19 | # If target exactly zero, we found a combination 20 | if (target == 0): 21 | # Add the current path to the result 22 | res.append(curr_path) 23 | # print(f'\nNew Path added -> {res}\n') 24 | return 25 | 26 | # Iterate over the list of nums from i to n 27 | for i in range(len(nums)): 28 | # print(f'Curr = {nums[i]}, Nums = {nums}, New Target = {target}, Path = {curr_path}, res = {res}') 29 | 30 | # Pass 4 params to the DFS function for each element in 'nums' 31 | # 1. New candidates will be 'i'th element onwards -> nums[i:] 32 | # 2. Subtract current number from target -> 'target - nums[i]' before recursive call 33 | # 3. Add the curr nums to the new potential path -> 'path+[nums[i]]' 34 | # 4. Pass the result array 35 | new_target = target - nums[i] 36 | self.dfs(nums[i:], new_target, curr_path+[nums[i]], res) 37 | -------------------------------------------------------------------------------- /backtracking/word_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/word-search/ 3 | ''' 4 | 5 | class Solution: 6 | 7 | def exist(self, board: List[List[str]], word: str) -> bool: 8 | 9 | # Checks for empty conditions 10 | if not board: 11 | return False 12 | 13 | if not word: 14 | return True 15 | 16 | # Iterate over each row and column on the board 17 | for i in range(len(board)): 18 | for j in range(len(board[0])): 19 | if self.dfs(board, i, j, word): 20 | return True 21 | 22 | # If DFS doesn't return a True, word not found! 23 | return False 24 | 25 | # Helper function to perform DFS on the board to search a given word 26 | def dfs(self, board, r, c, word): 27 | # Base case - Empty word i.e. all characters checked and matched 28 | if len(word) == 0: 29 | return True 30 | 31 | # Check if r and c i.e. row and column pointers are not going out of the board 32 | # or if current character doesn't match the curr char from the board 33 | if r < 0 or r >= len(board) or c < 0 or c >= len(board[0]) or word[0] != board[r][c]: 34 | return False 35 | 36 | # Assign current word in the ith row and jth column to a temporary variable 37 | tmp = board[r][c] 38 | # Make the current word on board non-alphabetic to avoid visits again 39 | board[r][c] = '#' 40 | 41 | # Continue to check for other letters of the word in every direction. 42 | # Remember to search for all the words AFTER the current word! 43 | res = self.dfs(board, r+1, c, word[1:]) or \ 44 | self.dfs(board, r-1, c, word[1:]) or \ 45 | self.dfs(board, r, c+1, word[1:]) or \ 46 | self.dfs(board, r, c-1, word[1:]) 47 | 48 | # Make the current word alphabetic again 49 | board[r][c] = tmp 50 | 51 | return res -------------------------------------------------------------------------------- /binary_search/min_in_rotated_array.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/ 3 | ''' 4 | 5 | class Solution: 6 | def findMin(self, nums: List[int]) -> int: 7 | ''' 8 | Use the binary_search, while looking for the minimum. 9 | Time Complexity: O(log n) 10 | ''' 11 | 12 | # Init two pointers to perform binary_search 13 | start, end = 0, len(nums) - 1 14 | 15 | # Init result to +infinity (since we are looking for minimum) 16 | minNum = float('inf') 17 | 18 | while start <= end: 19 | 20 | # Edge case - when array is sorted! Just return the min value! 21 | if nums[start] < nums[end]: 22 | return min(minNum, nums[start]) 23 | 24 | # Get the mid index 25 | mid = (start + end) // 2 26 | 27 | # Update the `minNum` by comparing with mid index element 28 | minNum = min(minNum, nums[mid]) 29 | 30 | ## Check which part you want to search in the rotated array 31 | 32 | # We compare the mid value and the leftmost value 33 | 34 | # If mid is greater (we have overshot), usually we decrement right pointer (i.e. search left part), 35 | # BUT since array is rotated, we search the RIGHT portion as it will have smaller values. 36 | if nums[mid] >= nums[start]: 37 | start = mid + 1 38 | 39 | # Similarly, if not (nums[m] <= nums[l] is satisfied), we have undershot, we search 40 | # the LEFT portion (as opposed to usual right portion search), as now that will have smaller values. 41 | else: 42 | end = mid - 1 43 | 44 | return minNum 45 | -------------------------------------------------------------------------------- /bit_manipulation/counting_bits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/counting-bits/ 3 | ''' 4 | 5 | class Solution: 6 | def countBits(self, n: int) -> List[int]: 7 | ''' 8 | Time Complexity = O(n) 9 | ''' 10 | 11 | # Create a `dp` of length `n+1` 12 | dp = [0] * (n + 1) 13 | # Offset determines where the significant bit changes 14 | offset = 1 15 | 16 | for i in range(1, n+1): 17 | # Offset changes only if i reaches `offset*2` 18 | if offset * 2 == i: 19 | offset = i 20 | 21 | # Number of ones is 1 + the number of ones in the i-offset position 22 | dp[i] = 1 + dp[i - offset] 23 | 24 | return dp 25 | 26 | -------------------------------------------------------------------------------- /bit_manipulation/missing_number.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/missing-number/ 3 | ''' 4 | 5 | class Solution: 6 | def missingNumber(self, nums: List[int]) -> int: 7 | ''' Approach 1: Brute Force O(N) approach''' 8 | 9 | # # Initialize array of -1 of len of 'nums' 10 | # res = [-1] * (len(nums) + 1) 11 | 12 | # # Replace -1 with actual number present in the list 13 | # for i in nums: 14 | # res[i] = i 15 | 16 | # # Now find the missing number (which will be set to -1) 17 | # for j in range(len(res)): 18 | 19 | # if res[j] == -1: 20 | # return j 21 | 22 | ''' Approach 2: Sum of 0..n minus sum of the given numbers is the missing one.''' 23 | # n = len(nums) 24 | # # Formula for sum of (0 to n) = ( n * (n + 1) ) // 2 25 | # sum_n = ( n * (n+1) // 2 ) 26 | # return sum_n - sum(nums) 27 | 28 | ''' Approach 3: Same approach as 2 but without formula 29 | Time Complexity: O(n) 30 | Space Complexity: O(1) 31 | ''' 32 | # Set result to be equal to len of the nums array 33 | result = len(nums) 34 | 35 | # iterate through each index from 0 to n 36 | for i in range(len(nums)): 37 | # Add to the result the index and subtract the nums[i] element 38 | result += (i - nums[i]) 39 | # All the numbers except the missing number will become zero 40 | 41 | return result 42 | -------------------------------------------------------------------------------- /bit_manipulation/number_of_1_bits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/number-of-1-bits/ 3 | ''' 4 | 5 | class Solution: 6 | def hammingWeight(self, n: int) -> int: 7 | ''' Approach 1: Use mod 2 ( % 2) to get the value of the last bit ''' 8 | count = 0 9 | 10 | # While `n` does not become 0 11 | while n: 12 | # Do a `mod 2` operation and add the value (either 1 or 0) to the count 13 | count += n % 2 14 | # Shift the values by 1 bit on the right 15 | n = n >> 1 16 | # Can also do `n = n / 2` (but bit shifting is more efficient) 17 | 18 | # Finally return the result 19 | return count 20 | 21 | ''' 22 | Approach 2: & bit operator is: 1 if both bits are 1, else 0 23 | 24 | Think of a number in binary n = XXXXXX1000, n - 1 is XXXXXX0111. 25 | n & (n - 1) will be XXXXXX0000 which is just remove the last significant 1 26 | 27 | Consider n and n-1 -> compared with n,n-1 has one bit-place difference 28 | which makes a 1 in n becoming 0 in n-1. 29 | 30 | And with counting recursively, we get all 1s canceled 31 | (while incrementing count), and then return count. 32 | ''' 33 | 34 | # count = 0 35 | # while n: 36 | # n &= n-1 37 | # count += 1 38 | # return count 39 | -------------------------------------------------------------------------------- /bit_manipulation/reverse_bits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/reverse-bits/ 3 | ''' 4 | 5 | class Solution: 6 | def reverseBits(self, n: int) -> int: 7 | result = 0 8 | 9 | # Iterate over the 32 bit input `n` 10 | for i in range(32): 11 | 12 | # Extract the bit value from `ith` index by doing a Logical AND operation 13 | # >> left shifts the bits by i locations 14 | bit = (n >> i) & 1 15 | 16 | # Store the extracted bit value in the `ith` position in the result 17 | # This is done using right shift and then performing a Logical OR 18 | # `(31 - i)` ensures that the bit is saved in the `ith` position of the result 19 | result = result | (bit << (31 - i)) 20 | 21 | return result 22 | -------------------------------------------------------------------------------- /graphs/clone_graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Definition for a Node. 3 | class Node: 4 | def __init__(self, val = 0, neighbors = None): 5 | self.val = val 6 | self.neighbors = neighbors if neighbors is not None else [] 7 | """ 8 | 9 | from typing import Optional 10 | class Solution: 11 | def cloneGraph(self, node: Optional['Node']) -> Optional['Node']: 12 | ''' 13 | Use Hashmap and DFS to create a deepcopy of the input graph. 14 | Hashmap is created to ensure that we are not cloning nodes repeatedly. 15 | 16 | Time complexity: O(n) where n = number of edges(E) + number of vertices(V) 17 | ''' 18 | 19 | # Hashmap to keep track of the all clones being made of the original node 20 | oldToNew = {} 21 | 22 | # Clone input `curr_node` using Recursive DFS 23 | def clone(curr_node): 24 | 25 | # Return the already created copy in the `node` is present in `oldToNew` 26 | if curr_node in oldToNew: 27 | return oldToNew[curr_node] 28 | 29 | # Create a deepcopy of the node and add it to the hashmap 30 | copy = Node(curr_node.val) 31 | # Add `copy` to the cloned node 32 | oldToNew[curr_node] = copy 33 | 34 | # Now recursively populate the neighbors of the cloned node 35 | for nei in curr_node.neighbors: 36 | copy.neighbors.append(clone(nei)) 37 | 38 | # Finally, return the completed deepcopy of the current `node` 39 | return copy 40 | 41 | ## Clone the `root node` if its not empty 42 | return clone(node) if node else None 43 | -------------------------------------------------------------------------------- /graphs/course_schedule.py: -------------------------------------------------------------------------------- 1 | class Solution: 2 | def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: 3 | ''' 4 | Use an adjacency list (and run DFS) to check all the courses and if their prequisites can be completed. 5 | ''' 6 | 7 | # Init a dict which will be the adjaceny list for all courses 8 | preMap = { i: [] for i in range(numCourses) } 9 | 10 | # Populate the `preMap` (the adjacency list) with the prereqs for each course 11 | for crs, pre in prerequisites: 12 | preMap[crs].append(pre) 13 | 14 | # visitedSet = all courses along the curr DFS path. Used to avoid any loops in the DFS path. 15 | visitedSet = set() 16 | 17 | ## Helper method to check if the current `crs` can be completed. Using recursive DFS. 18 | def dfs(crs): 19 | 20 | # Base case 1 - course already visited 21 | if crs in visitedSet: 22 | return False 23 | 24 | # Base case 2 - has no other prereqs/all prereqs can be completed for current course 25 | if preMap[crs] == []: 26 | return True 27 | 28 | # Here the current course has preqs remaining. 29 | # Add the current course to the visited set 30 | visitedSet.add(crs) 31 | 32 | # And check if all the preqs for the current course can be completed 33 | for pre in preMap[crs]: 34 | # Recursive DFS on the current prerequisite to check if it cannot be completed. 35 | if not dfs(pre): 36 | return False 37 | 38 | # Remove from visited since not checking anymore 39 | visitedSet.remove(crs) 40 | # Make the prereqs list empty in the adjacency list, since now we know 41 | # that prerequisites for this course can be completed. 42 | preMap[crs] = [] 43 | 44 | return True 45 | 46 | ## Main loop to check if can finish all courses 47 | # Run the DFS to check if you can finish prerequisites for all the courses 48 | for crs in range(numCourses): 49 | if not dfs(crs): 50 | return False 51 | 52 | # If all okay, the you can finish all courses 53 | return True -------------------------------------------------------------------------------- /graphs/number_of_islands.py: -------------------------------------------------------------------------------- 1 | class Solution: 2 | def numIslands(self, grid: List[List[str]]) -> int: 3 | 4 | ''' Approach 1: Using Recursive DFS ''' 5 | 6 | # Empty case 7 | if not grid or not grid[0]: 8 | return 0 9 | 10 | ROWS, COLS = len(grid), len(grid[0]) 11 | islands = 0 12 | # Since we are doing a DFS, we go down the search diagonally, 13 | # so we do not go back up at all! 14 | visit = set() 15 | 16 | # Use Recursive DFS to find the number of islands 17 | def dfs(r, c): 18 | 19 | # Base case & main condition 20 | # Return if we encounter water i.e. "0", since island is over! 21 | if r not in range(ROWS) or c not in range(COLS) or grid[r][c] == "0" or (r, c) in visit: 22 | return 23 | 24 | # Remember to add current element to `visit` 25 | visit.add((r, c)) 26 | 27 | # Recursively iterate in all four directions for DFS 28 | directions = [[0, 1], [0, -1], [1, 0], [-1, 0]] 29 | for dr, dc in directions: 30 | # Recursively call DFS in all four directions 31 | dfs(r + dr, c + dc) 32 | 33 | 34 | ## Main loop while running DFS - used to update the `islands` result 35 | for r in range(ROWS): 36 | for c in range(COLS): 37 | # Perform DFS ONLY IF you encounter land i.e. "1" 38 | if grid[r][c] == "1" and (r, c) not in visit: 39 | # Increment `islands` count and iterate until its edges (or until we reach water) using DFS 40 | islands += 1 41 | dfs(r, c) 42 | 43 | # Finally return result 44 | return islands 45 | 46 | ''' 47 | ## Approach 2: Using Iterative BFS 48 | 49 | # Edge case for empty grid 50 | if not grid: 51 | return 0 52 | 53 | rows, cols = len(grid), len(grid[0]) 54 | # Create a set for all the visited nodes in the grid 55 | visited = set() 56 | islands = 0 57 | 58 | # Iterative BFS - done using queue 59 | def bfs(r, c): 60 | q = collections.deque() 61 | 62 | # Add current node to visited and also to the queue 63 | visited.add((r, c)) 64 | q.append((r, c)) 65 | 66 | # Iterate until all the nodes in queue are visited 67 | while q: 68 | # NOTE: Change to `q.pop()` to make this iterative DFS. Rest of the code is the same. 69 | # Since it'll now pop from the end of the queue instead of the start! 70 | curr_row, curr_col = q.popleft() 71 | 72 | # All the directions of current (r, c) to perform search in 73 | directions = [[1, 0], [-1, 0], [0, 1], [0, -1]] 74 | for dr, dc in directions: 75 | r, c = curr_row + dr, curr_col + dc 76 | 77 | # Check if (r, c) position not out of grid, is land and is not visited 78 | if r in range(rows) and \ 79 | c in range(cols) and \ 80 | grid[r][c] == "1" and \ 81 | (r, c) not in visited: 82 | 83 | # If not, then we can add the node to the queue and the visited set 84 | q.append((r, c)) 85 | visited.add((r, c)) 86 | 87 | ## Main loop for counting the number of islands 88 | # Go through every row and column 89 | for r in range(rows): 90 | for c in range(cols): 91 | # Do BFS only when the node is land i.e. "1" and has not been visited already 92 | if grid[r][c] == "1" and (r, c) not in visited: 93 | bfs(r, c) 94 | islands += 1 95 | 96 | return islands 97 | ''' -------------------------------------------------------------------------------- /graphs/pacific_atlantic_water_flow.py: -------------------------------------------------------------------------------- 1 | class Solution: 2 | def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]: 3 | 4 | ''' 5 | We iterate from water to inside and check which nodes can do that for 6 | the two oceans separately. Note that here it means that the previous Height should be 7 | greater than the current height since we are going in reverse (water to land). 8 | 9 | Finally, the result will contain nodes that can do it for both oceans, i.e. water flow 10 | from one ocean to the other! 11 | ''' 12 | 13 | ROWS, COLS = len(heights), len(heights[0]) 14 | # Maintain sets for nodes that can reach the Pacific and Atlantic ocean 15 | pac, atl = set(), set() 16 | 17 | # Run DFS on all nodes on the ocean boundaries and check their neighbors using DFS 18 | def dfs(r, c, visited, prevHeight): 19 | if ((r, c) in visited or 20 | r < 0 or c < 0 or 21 | r == ROWS or c == COLS or 22 | heights[r][c] < prevHeight): 23 | return 24 | 25 | # Add to visited 26 | visited.add((r, c)) 27 | 28 | # Run DFS recursively in all four directions 29 | dfs(r + 1, c, visited, heights[r][c]) 30 | dfs(r - 1, c, visited, heights[r][c]) 31 | dfs(r, c + 1, visited, heights[r][c]) 32 | dfs(r, c - 1, visited, heights[r][c]) 33 | 34 | ''' 35 | Main loop - Run DFS for the boundaries of the two oceans (starting once from all directions) 36 | ''' 37 | 38 | # Iterate nodes from RIGHT and LEFT (in both directions) 39 | # So, note that rows stays constant and UPDATE COLUMNS (moving left and right) 40 | for c in range(COLS): 41 | # All nodes in the first row (left -> right) - starting from pacific to atlantic 42 | dfs(0, c, pac, heights[0][c]) 43 | # All nodes in the last row (right to left ->) - starting from atlantic to pacific 44 | dfs(ROWS-1, c, atl, heights[ROWS-1][c]) 45 | 46 | # Iterate node from TOP and BOTTOM (in both directions) 47 | # So, note that columns stays constant and UPDATE ROWS (moving up and down) 48 | for r in range(ROWS): 49 | # All nodes in first column (on the top) - starting from pacific to atlantic 50 | dfs(r, 0, pac, heights[r][0]) 51 | # All nodes in the last column (on the bottom) - starting from atlantic to pacific 52 | dfs(r, COLS-1, atl, heights[r][COLS-1]) 53 | 54 | # Finally, check for nodes that are a part of both sets. 55 | # These are the nodes where water can flow from one ocean to the other! 56 | res = [] 57 | for r in range(ROWS): 58 | for c in range(COLS): 59 | if (r, c) in pac and (r, c) in atl: 60 | res.append([r, c]) 61 | 62 | return res 63 | -------------------------------------------------------------------------------- /greedy/jump_game.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/jump-game/ 3 | ''' 4 | 5 | class Solution: 6 | def canJump(self, nums: List[int]) -> bool: 7 | ''' 8 | Approach: Iterate in reverse and keep moving the goal 9 | post closer to the start of the list. If we can reach the 10 | start index i.e. 0, then return True 11 | Time Complexity = O(n) 12 | ''' 13 | 14 | # Set the goal as the last index in the list 15 | goal = len(nums) - 1 16 | 17 | # We iterate in reverse 18 | for i in range(len(nums) - 1, -1, -1): 19 | ''' 20 | If we can reach the current goal index using the max steps in the current 21 | element(nums[i]), then update the goal index i.e. move it to the ith index 22 | 23 | What does the condition mean? We check the next index we can reach 24 | i.e. ith index plus the steps allowed using nums[i] 25 | So, `i + nums[i]` should be greater than the current goal index we have set! 26 | ''' 27 | if (i + nums[i]) >= goal: 28 | goal = i 29 | 30 | # Finally, we check if the goal has reached to the start 31 | # if yes, then we can reach the end of the array 32 | return True if goal == 0 else False 33 | -------------------------------------------------------------------------------- /greedy/maximum_subarray.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/maximum-subarray/ 3 | ''' 4 | 5 | class Solution: 6 | def maxSubArray(self, nums: List[int]) -> int: 7 | ''' Approach 1: 8 | Kadane's Algorithm: Time Complexity = O(n) 9 | 10 | - If the sum of a subarray is positive, it is possible to 11 | make the next value bigger, so we keep do it until it turns negative. 12 | - If the sum is negative, it has no use to the next element, so we break. 13 | It is a game of sum, not the elements. 14 | ''' 15 | 16 | if not nums: 17 | return 0 18 | 19 | # for i in range(1, len(nums)): 20 | # # Add to current sum only if the value is positive 21 | # if nums[i-1] > 0: 22 | # nums[i] += nums[i-1] 23 | 24 | # return max(nums) 25 | 26 | ''' Approach 2: (same idea) Time Complexity = O(n) ''' 27 | 28 | # Init current sum to 0 and maxSum to first element 29 | currSum = 0 30 | maxSum = nums[0] 31 | 32 | # Iterate over the remaining elements 33 | for n in nums: 34 | # Add current number to currSum 35 | currSum += n 36 | # and compare with existing maxSum 37 | maxSum = max(currSum, maxSum) 38 | 39 | # If currSum goes negative, make the currSum zero 40 | # since we cannot use a subarray with negative values (larger than currSum) 41 | # as they will result in a negative sum (obvs won't be max) 42 | if currSum < 0: 43 | currSum = 0 44 | 45 | return maxSum 46 | -------------------------------------------------------------------------------- /intervals/insert_interval.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/insert-interval/ 3 | ''' 4 | 5 | class Solution: 6 | def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]: 7 | ''' 8 | When inserting, check if they are merging. 9 | If they are, the new interval is the `min of starts` and `max of ends` 10 | 11 | How to check where to add the new interval (in case of no-overlap)? 12 | IF 13 | - end of new interval < start of curr interval, then add before 14 | ELSE IF 15 | - start of new interval > end of curr interval, then add after 16 | 17 | Time Complexity = O(n) 18 | ''' 19 | 20 | res = [] 21 | 22 | for i in range(len(intervals)): 23 | # No overlap, to be merged before current interval 24 | if newInterval[1] < intervals[i][0]: 25 | # Here, we don't need to check, so we return the result 26 | res.append(newInterval) 27 | return res + intervals[i:] 28 | 29 | # No overlap, new to be merged after current interval 30 | elif newInterval[0] > intervals[i][1]: 31 | # Here we add the current interval, and keep checking since the newInterval 32 | # could be merged AFTER other intervals too! 33 | res.append(intervals[i]) 34 | 35 | # Overlap case, we create the new merged interval - min of starts and max of ends 36 | else: 37 | newInterval = [ min(newInterval[0], intervals[i][0]), 38 | max(newInterval[1], intervals[i][1]) ] 39 | 40 | # We add the new merged interval to the result (for elif and else cases) 41 | res.append(newInterval) 42 | 43 | return res 44 | -------------------------------------------------------------------------------- /intervals/merge_intervals.py: -------------------------------------------------------------------------------- 1 | class Solution: 2 | def merge(self, intervals: List[List[int]]) -> List[List[int]]: 3 | 4 | ''' 5 | Two intervals i1 and i2 overlap only if i1[1] >= i2[0] and i2[1] >= i1[0] or vice-versa. 6 | i.e. Both intervals' ENDS >= STARTS 7 | 8 | So, whenever we find two such intervals, we need to merge them as - 9 | new `start` is min of the starts, and the new `end` is the max of the ends! 10 | -> newLimits = [min(i1[0], i2[0]), max(i1[1], i2[1])] 11 | 12 | Sorting ensures that we just compare adjacent pairs of intervals, 13 | instead of restarting the iteration everytime we merge two intervals and then restart until 14 | we find no more overlapping intervals. This approach would lead to O(n^2) time complexity. 15 | 16 | By sorting, one of the condition of overlap is already satisfied -> i2[1] >= i1[0] (the second condition for merging!) 17 | i.e. End of second interval will always be greater that the start of the first. 18 | 19 | Time Complexity = O(n log n) -> 'log n' for the sorting and 'n' to merge 20 | Space Complexity = O(n) (since saving the result in a new list) 21 | ''' 22 | 23 | # Sort based on START limit to compare only adjacent intervals 24 | intervals.sort(key = lambda pair: pair[0] ) 25 | # Maintain a list of sorted and merged intervals 26 | merged = [intervals[0]] 27 | 28 | # After sorting we only check current STARTs being less than the last interval's end. 29 | for start, end in intervals[1:]: 30 | 31 | lastEnd = merged[-1][1] 32 | # If overlapping, we merge the intervals by updating the last interval of the sorted list 33 | if lastEnd >= start: 34 | # the new `end` is the max of the two ends 35 | merged[-1][1] = max(lastEnd, end) 36 | 37 | # If no overlap, then we just add the current interval to the end of the sorted list 38 | else: 39 | merged.append([start, end]) 40 | 41 | return merged 42 | -------------------------------------------------------------------------------- /linked_lists/linked_list_cycle.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/linked-list-cycle/ 3 | ''' 4 | 5 | # Definition for singly-linked list. 6 | class ListNode: 7 | def __init__(self, x): 8 | self.val = x 9 | self.next = None 10 | 11 | 12 | class Solution: 13 | def hasCycle(self, head: Optional[ListNode]) -> bool: 14 | ''' 15 | Use two pointers - slow and fast, if they meet, we have a cycle 16 | ''' 17 | 18 | # Create two pointers - `fast` and `slow`. They start at the same node - head 19 | slow, fast = head, head 20 | 21 | # Check if the `fast` pointer and the one after is not empty 22 | # i.e. make sure that the `fast` pointer has next nodes to jump to 23 | while fast and fast.next: 24 | 25 | # `slow` moves only one node at a time -> moves slower 26 | slow = slow.next 27 | # `fast` jumps two nodes at a time -> moves faster 28 | fast = fast.next.next 29 | 30 | # At some point, the pointers will be at the same node if it's a cycle! 31 | if slow == fast: 32 | return True 33 | 34 | # If no cycle, then the `fast` pointer reaches the end of the LL and it's not a loop! 35 | return False 36 | -------------------------------------------------------------------------------- /linked_lists/merge_k_sorted_lists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/merge-k-sorted-lists/ 3 | 4 | You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. 5 | Merge all the linked-lists into one sorted linked-list and return it. 6 | ''' 7 | 8 | # Definition for singly-linked list. 9 | class ListNode: 10 | def __init__(self, val=0, next=None): 11 | self.val = val 12 | self.next = next 13 | 14 | class Solution: 15 | def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]: 16 | ''' 17 | Keep merging two adjacent lists until you have one big merged list remaining! 18 | Time Complexity: O( n log k) 19 | ''' 20 | 21 | # Empty case 22 | if not lists or len(lists) == 0: 23 | return None 24 | 25 | # Keep merging the pairs of Linked Lists until only one (the merged one) is left 26 | while (len(lists)) > 1: 27 | 28 | # Create a new list to store the merged pairs of LLs 29 | mergedLists = [] 30 | 31 | # Take pairs of LLs - two at a time 32 | for i in range(0, len(lists), 2): 33 | # First LL is `i` 34 | l1 = lists[i] 35 | # Second is `i+1` or else `None` if we are at the end of the `lists` 36 | l2 = lists[i+1] if (i+1) < len(lists) else None 37 | # Merge these two linkedLists 38 | mergedLists.append(self.mergeTwoLists(l1, l2)) 39 | 40 | # Update the list of LinkedLists i.e. the results stored in `lists` 41 | lists = mergedLists 42 | 43 | # Return the first list which now contains all the lists merged 44 | return lists[0] 45 | 46 | # Helper function to merge two linked lists 47 | def mergeTwoLists(self, l1, l2): 48 | # Done before, so refer https://leetcode.com/problems/merge-two-sorted-lists/ 49 | 50 | # Why a `dummy` node? Since it points to the head of the `result` LL 51 | dummy = ListNode() 52 | result = dummy 53 | 54 | while l1 and l2: 55 | # Remember: new value to be stored in `result.next` 56 | 57 | if l1.val < l2.val: 58 | result.next = l1 59 | l1 = l1.next 60 | 61 | else: 62 | result.next = l2 63 | l2 = l2.next 64 | 65 | result = result.next 66 | 67 | if l1: 68 | result.next = l1 69 | elif l2: 70 | result.next = l2 71 | 72 | return dummy.next 73 | -------------------------------------------------------------------------------- /linked_lists/merge_two_sorted_lists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/merge-two-sorted-lists/ 3 | ''' 4 | 5 | # Definition for singly-linked list. 6 | class ListNode: 7 | def __init__(self, val=0, next=None): 8 | self.val = val 9 | self.next = next 10 | 11 | class Solution: 12 | def mergeTwoLists(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]: 13 | ''' 14 | Approach 1: Iterative 15 | Time Complexity: O(n) & Space Complexity: O(1) 16 | ''' 17 | 18 | # Create an empty node as the first node in the result LL 19 | dummy = ListNode() 20 | 21 | # 'dummy' is the node that points to the start of the merged 'result' LL. 22 | # In the end, we return the 'dummy.next' as the result, which pointed to the merged LL 23 | result = dummy 24 | 25 | # Iterate until one of the lists is empty 26 | while l1 and l2: 27 | # Smaller value gets assigned to new sorted list 28 | if l1.val < l2.val: 29 | result.next = l1 30 | l1 = l1.next 31 | 32 | # Here, l2.val is <= l1.val 33 | else: 34 | result.next = l2 35 | l2 = l2.next 36 | 37 | # After each iteration, update the result pointer 38 | result = result.next 39 | 40 | # If either of the lists is not empty i.e. still has elements, add them to the result 41 | if l1: 42 | result.next = l1 43 | elif l2: 44 | result.next = l2 45 | 46 | # Finally return the node after the dummy (empty node) which 47 | # is now pointing to the merged and sorted linked list 48 | return dummy.next 49 | 50 | ''' 51 | Approach 2: Recursive 52 | Time Complexity: O(n) & Space Complexity: O(1) 53 | ''' 54 | 55 | # # Return the non-empty LL when you reach with end of either one 56 | # if not l1 or not l2: 57 | # return l1 or l2 58 | 59 | # # Check if l1's curr value smaller than l2's curr value 60 | # if (l1.val < l2.val): 61 | # # LL of the smaller value is treated as l1 and other as l2 for recursive call 62 | # # Result assigned to next node of the smaller LL -> in this case it is assigned to l1.next 63 | # l1.next = self.mergeTwoLists(l1.next, l2) 64 | # # Final result is stored in l1 (i.e. the LL with the smaller value) over all recursive calls 65 | # return l1 66 | 67 | # else: 68 | # # Same but for l2's curr val being smaller (or equal) to l1's curr val 69 | # l2.next = self.mergeTwoLists(l2.next, l1) 70 | # # Final result is stored in l2 (i.e. the LL with the smaller value) over all recursive calls 71 | # return l2 72 | -------------------------------------------------------------------------------- /linked_lists/remove_nth_node_from_end_of_list.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/remove-nth-node-from-end-of-list/ 3 | ''' 4 | 5 | # Definition for singly-linked list 6 | class ListNode: 7 | def __init__(self, val=0, next=None): 8 | self.val = val 9 | self.next = next 10 | 11 | class Solution: 12 | def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]: 13 | ''' 14 | Use pointers - `left` is at the start of the list, and `right` is shifted by `n` 15 | By the time we reach the end of the list, the left pointer is at the node that is to be removed! 16 | Then we go on to remove that node. 17 | Trick: start left pointer with a dummy node. Makes it easier to delete the node when you reach the end. 18 | 19 | Time Complexity: O(n) & Space Complexity: O(1) 20 | ''' 21 | 22 | # Set next of dummy node at the start of the list 23 | dummy = ListNode(0, head) 24 | # Set left pointer to the start of the LL 25 | left, right = dummy, head 26 | 27 | # shift the `right` pointer by `n` 28 | while n > 0 and right: 29 | right = right.next 30 | n -= 1 31 | 32 | # Iterate until we reach the end of the list 33 | while right: # since `right` is ahead of `left` (and shifted by `n` nodes) 34 | left = left.next 35 | right = right.next 36 | 37 | # At this point, since `right` is shifted by `n` nodes (from first while loop), 38 | # `left` will now be at the node we need to delete 39 | 40 | # Now we can delete the node that is after `left` pointer 41 | # i.e. remove the link to `left.next` 42 | left.next = left.next.next 43 | 44 | # Return the node after the dummy i.e. the original head 45 | # of the now modified linked list 46 | return dummy.next 47 | -------------------------------------------------------------------------------- /linked_lists/reverse_linked_list.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/reverse-linked-list/ 3 | ''' 4 | 5 | # Definition for singly-linked list. 6 | class ListNode: 7 | def __init__(self, val=0, next=None): 8 | self.val = val 9 | self.next = next 10 | 11 | class Solution: 12 | def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: 13 | ''' 14 | Iterative approach - Reverse using two pointers 15 | Time Complexity = O(n) & Space Complexity: O(1) since using only pointers 16 | ''' 17 | 18 | # Set 'curr' to head i.e. the first node and the 'prev' to a Null 19 | # The reverse LL is being stored in the 'prev' pointer 20 | prev, curr = None, head 21 | 22 | # Iterate until we reach the end of the linked_lists 23 | while curr: 24 | ''' We move prev -> curr (the reversal step) and move curr -> curr.next ''' 25 | 26 | # We save the value of 'curr.next' before swapping 'curr' 27 | temp = curr.next 28 | 29 | # Now we shift pointers 30 | curr.next = prev 31 | prev = curr # reversal step 32 | curr = temp # has the value of curr.next 33 | 34 | # Return the result that is stored in prev i.e. the reversed list 35 | return prev 36 | 37 | ''' 38 | Recursive approach (more space complexity) 39 | Time & Space Complexity: O(n) 40 | ''' 41 | 42 | # # Base case 43 | # if not head: 44 | # return None 45 | 46 | # # Maintain the current head node 47 | # newHead = head 48 | # if head.next: 49 | # newHead = self.reverseList(head.next) 50 | # # Set the new head to previous node's next 51 | # head.next.next = head 52 | 53 | # # If end of the list, point the node's next to a null 54 | # head.next = None 55 | 56 | # return newHead 57 | -------------------------------------------------------------------------------- /questions_list.md: -------------------------------------------------------------------------------- 1 | # List of Questions & Solutions 2 | 3 | A full list of the topics and questions covered in this repo (so far). **Solutions counter - 46/75** 4 | 5 | --- 6 | 7 | ## [Arrays & Hashing](arrays_and_hashing) 8 | | | | 9 | | --- | ----------- | 10 | | [Contains Duplicate](https://leetcode.com/problems/contains-duplicate/) | [contains_duplicate.py](arrays_and_hashing/contains_duplicate.py) | 11 | | [Valid Anagram](https://leetcode.com/problems/valid-anagram/) | [valid_anagram.py](arrays_and_hashing/valid_anagram.py) | 12 | | [Two Sum](https://leetcode.com/problems/two-sum/) | [two_sum.py](arrays_and_hashing/two_sum.py) | 13 | | [Group Anagrams](https://leetcode.com/problems/group-anagrams/) | [group_anagrams.py](arrays_and_hashing/group_anagrams.py) | 14 | | [Top K Frequent Elements](https://leetcode.com/problems/top-k-frequent-elements/) | [top_k_frequent_elements.py](arrays_and_hashing/top_k_frequent_elements.py) | 15 | | [Product of Array Except Self](https://leetcode.com/problems/product-of-array-except-self/) | [product_of_array_except_self.py](arrays_and_hashing/product_of_array_except_self.py) | 16 | | [Valid Sudoku](https://leetcode.com/problems/valid-sudoku/) | [valid_sudoku.py](arrays_and_hashing/valid_sudoku.py) | 17 | | [Encode & Decode Strings](https://neetcode.io/problems/string-encode-and-decode) | [encode_decode_strings.py](arrays_and_hashing/encode_decode_strings.py) | 18 | | [Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/) | [longest_consecutive_sequence.py](arrays_and_hashing/longest_consecutive_sequence.py) | 19 | 20 | ## [Two Pointers](two_pointers) 21 | | | | 22 | | --- |---------------------------------------------------------------------------| 23 | | [Valid Palindrome](https://leetcode.com/problems/valid-palindrome/) | [valid_palindrome.py](two_pointers/valid_palindrome.py) | 24 | | [Two Sum II (Input Array Sorted)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/) | [two_sum_ii_input_sorted.py](two_pointers/two_sum_ii_input_sorted.py) | 25 | | [Three Sum](https://leetcode.com/problems/3sum/) | [three_sum.py](two_pointers/three_sum.py) | 26 | | [Container with Most Water](https://leetcode.com/problems/container-with-most-water/) | [container_with_most_water.py](two_pointers/container_with_most_water.py) | 27 | | [Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/) | [trapping_rain_water.py](two_pointers/trapping_rain_water.py) | 28 | 29 | ## [Sliding Window](sliding_window) 30 | | | | 31 | | --- | ----------- | 32 | | [Best time to Buy and Sell Stock](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/) | [best_buy_sell_stock.py](sliding_window/best_buy_sell_stock.py) | 33 | | [Longest Substring w/o Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/) | [longest_substr_without_repeating_chars.py](sliding_window/longest_substr_without_repeating_chars.py) | 34 | | [Longest Repeating Character Replacement](https://leetcode.com/problems/longest-repeating-character-replacement/) | [longest_repeating_character_replacement.py](sliding_window/longest_repeating_character_replacement.py) | 35 | | [Permuatation in String](https://leetcode.com/problems/permutation-in-string/submissions/) | [permutation_in_strings.py](sliding_window/permutation_in_strings.py) | 36 | | [Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/) | [minimum_window_substring.py](sliding_window/minimum_window_substring.py) | 37 | | [Sliding Window Maximum](https://leetcode.com/problems/sliding-window-maximum/) | [sliding_window_maximum.py](sliding_window/sliding_window_maximum.py) | 38 | 39 | ## [Stack](stack) 40 | | | | 41 | | --- | ----------- | 42 | | [Valid Parentheses](https://leetcode.com/problems/valid-parentheses/) | [valid_parentheses.py](stack/valid_parentheses.py) | 43 | 44 | ## [Binary Search](binary_search) 45 | | | | 46 | | --- | ----------- | 47 | | [Find Minimum in Rotated Sorted Array](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/) | [min_in_rotated_array.py](binary_search/min_in_rotated_array.py) | 48 | | [Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/) | [search_in_rotated_array.py](binary_search/search_in_rotated_sorted_array.py) | 49 | 50 | ## [Linked Lists](linked_lists) 51 | | | | 52 | | --- | ----------- | 53 | | [Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) | [reverse_linked_list.py](linked_lists/reverse_linked_list.py) | 54 | | [Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) | [merge_two_sorted_lists.py](linked_lists/merge_two_sorted_lists.py) | 55 | | [Remove Nth Node From End of List](https://leetcode.com/problems/remove-nth-node-from-end-of-list/) | [remove_nth_node_from_end_of_list.py](linked_lists/remove_nth_node_from_end_of_list.py) | 56 | | [Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/) | [linked_list_cycle.py](linked_lists/linked_list_cycle.py) | 57 | | [Merge K Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) | [merge_k_sorted_lists.py](linked_lists/merge_k_sorted_lists.py) | 58 | 59 | ## [Trees](trees) 60 | | | | 61 | | --- | ----------- | 62 | | [Invert Binary Tree](https://leetcode.com/problems/invert-binary-tree/) | [invert_binary_tree.py](trees/invert_binary_tree.py) | 63 | | [Maximum Depth of Binary Tree](https://leetcode.com/problems/maximum-depth-of-binary-tree/) | [max_depth_of_binary_tree.py](trees/max_depth_of_binary_tree.py) | 64 | | [Same Tree](https://leetcode.com/problems/same-tree/) | [same_tree.py](trees/same_tree.py) | 65 | | [Subtree of Another Tree](https://leetcode.com/problems/subtree-of-another-tree/) | [subtree_of_another_tree.py](trees/subtree_of_another_tree.py) | 66 | | [Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/) | [lowest_common_ancestor_of_bst.py](trees/lowest_common_ancestor_of_bst.py) | 67 | | [Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/) | [binary_tree_level_order_traversal.py](trees/binary_tree_level_order_traversal.py) | 68 | | [Validate Binary Search Tree](https://leetcode.com/problems/valid-binary-search-tree/) | [valid_bst.py](trees/valid_bst.py) | 69 | 70 | ## [Backtracking](backtracking) 71 | | | | 72 | | --- | ----------- | 73 | | [Combination Sum](https://leetcode.com/problems/combination-sum/) | [combination_sum.py](backtracking/combination_sum.py) | 74 | | [Word Search](https://leetcode.com/problems/word-search/) | [word_search.py](backtracking/word_search.py) | 75 | 76 | ## [Tries](tries) 77 | | | | 78 | | --- | ----------- | 79 | | [Implement Trie (Prefix Tree)](https://leetcode.com/problems/implement-trie-prefix-tree/) | [implement_trie.py](tries/implement_trie.py) | 80 | | [Add and Search Word - Data Structure Design](https://leetcode.com/problems/design-add-and-search-words-data-structure/description/) | [add_search_word.py](tries/add_search_word.py) | 81 | | [Word Search II](https://leetcode.com/problems/word-search-ii/) | [word_search_ii.py](tries/word_search_ii.py) | 82 | 83 | ## [1-D Dynamic Programming](1d_dynamic_programming) 84 | | | | 85 | | --- | ----------- | 86 | | [Climbing Stairs](https://leetcode.com/problems/climbing-stairs/) | [climbing_stairs.py](1d_dynamic_programming/climbing_stairs.py) | 87 | | [House Robber](https://leetcode.com/problems/house-robber/) | [house_robber.py](1d_dynamic_programming/house_robber.py) | 88 | | [House Robber II](https://leetcode.com/problems/house-robber-ii/) | [house_robber_ii.py](1d_dynamic_programming/house_robber_ii.py) | 89 | | [Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring/) | [longest_palindromic_substring.py](1d_dynamic_programming/longest_palindromic_substring.py) | 90 | |[Palindromic Substrings](https://leetcode.com/problems/palindromic-substrings/) | [palindromic_substrings.py](1d_dynamic_programming/palindromic_substrings.py) | 91 | | [Decode Ways](https://leetcode.com/problems/decode-ways/) | [decode_ways.py](1d_dynamic_programming/decode_ways.py) | 92 | | [Coin Change](https://leetcode.com/problems/coin-change/) | [coin_change.py](1d_dynamic_programming/coin_change.py) | 93 | | [Maximum Product Subarray](https://leetcode.com/problems/maximum-product-subarray/) | [max_product_subarray.py](1d_dynamic_programming/max_product_subarray.py) | 94 | | [Word Break](https://leetcode.com/problems/word-break/) | [word_break.py](1d_dynamic_programming/word_break.py) | 95 | | [Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/) | [longest_increasing_subsequence.py](1d_dynamic_programming/longest_increasing_subsequence.py) | 96 | 97 | ## [2-D Dynamic Programming](2d_dynamic_programming) 98 | | | | 99 | | --- | ----------- | 100 | | [Unique Paths](https://leetcode.com/problems/unique-paths/) | [unique_paths.py](2d_dynamic_programming/unique_paths.py) | 101 | | [Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) | [longest_common_subsequence.py](2d_dynamic_programming/longest_common_subsequence.py) | 102 | 103 | ## [Greedy](greedy) 104 | | | | 105 | | --- | ----------- | 106 | | [Maximum Subarray](https://leetcode.com/problems/maximum-subarray/) | [maximum_subarray.py](greedy/maximum_subarray.py) | 107 | | [Jump Game](https://leetcode.com/problems/jump-game/) | [jump_game.py](greedy/jump_game.py) 108 | 109 | ## [Intervals](intervals) 110 | | | | 111 | | --- | ----------- | 112 | | [Insert Interval](https://leetcode.com/problems/insert-interval/) | [insert_interval.py](intervals/insert_interval.py) | 113 | | [Merge Intervals](https://leetcode.com/problems/merge-intervals/) | [merge_intervals.py](intervals/merge_intervals.py) | 114 | 115 | ## [Math & Geometry](math_and_geometry) 116 | | | | 117 | |------------------| ----------- | 118 | | [Rotate Image](https://leetcode.com/problems/rotate-image/) | [rotate_image.py](math_and_geometry/rotate_image.py) | 119 | | [Spiral Matrix](https://leetcode.com/problems/spiral-matrix/) | [spiral_matrix.py](math_and_geometry/spiral_matrix.py) | 120 | 121 | ## [Bit Manipulation](bit_manipulation) 122 | | | | 123 | | --- | ----------- | 124 | | [Number of 1 Bits](https://leetcode.com/problems/number-of-1-bits/) | [number_of_1_bits.py](bit_manipulation/number_of_1_bits.py) | 125 | | [Counting Bits](https://leetcode.com/problems/counting-bits/) | [counting_bits.py](bit_manipulation/counting_bits.py) | 126 | | [Reverse Bits](https://leetcode.com/problems/reverse-bits/) | [reverse_bits.py](bit_manipulation/reverse_bits.py) | 127 | | [Missing Number](https://leetcode.com/problems/missing-number/) | [missing_number.py](bit_manipulation/missing_number.py) | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='neetcode-solutions', 5 | version='1.0.0', 6 | packages=[''], 7 | url='https://github.com/darpan-jain/neetcode-solutions', 8 | license='MIT', 9 | author='Darpan Jain', 10 | author_email='', 11 | description='A collection of solutions to common Leetcode questions, based on a list compiled by neetcode.io.' 12 | ) 13 | -------------------------------------------------------------------------------- /sliding_window/best_buy_sell_stock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/best-time-to-buy-and-sell-stock/ 3 | """ 4 | 5 | from typing import List 6 | 7 | class Solution: 8 | def maxProfit(self, prices: List[int]) -> int: 9 | # Choose first element as buy price and initial profit as 0 10 | buy, profit = prices[0], 0 11 | 12 | # Iterate over the prices but skip first element since that is assigned to current buy price 13 | for curr_price in prices[1:]: 14 | 15 | # If you find a lower price than current buy price, that's your new buy price 16 | if curr_price < buy: 17 | buy = curr_price 18 | 19 | # Keep updating profit to 'max b/w (curr buy price - previous buy price) and profit' 20 | profit = max((curr_price - buy), profit) 21 | 22 | return profit -------------------------------------------------------------------------------- /sliding_window/longest_repeating_character_replacement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/longest-repeating-character-replacement/ 3 | """ 4 | 5 | class Solution: 6 | def characterReplacement(self, s: str, k: int) -> int: 7 | """ 8 | Sliding window approach with two pointers. 9 | The pointers start from element 0 and right pointer is moved right everytime until 10 | we meet the condition -> number of replacements required in current window <= k 11 | 12 | Here, the number of required to replace should be less than the max allowed replacements (i.e. k) 13 | calculated using -> (window_size - count of most occuring character in the window) 14 | 15 | Time complexity: O(N) 16 | """ 17 | 18 | # Dict `count` to store the frequency of the characters in the current window 19 | count = {} 20 | 21 | max_len = 0 22 | 23 | # Left pointer that stays 24 | l = 0 25 | 26 | # Right pointer moves to the right, expanding the sliding window 27 | for r, curr_char in enumerate(s): 28 | 29 | # Increment the frequency of the current character by 1 30 | count[curr_char] = 1 + count.get(curr_char, 0) 31 | 32 | # Check if the `(windowLen - max occuring char in the window)` is <= k 33 | # i.e. check if this is a valid window maximizing the sequence length 34 | # Using this we check if the required replacements are 35 | # less than the allowed 'k' replacements 36 | while (r - l + 1) - max(count.values()) > k: 37 | 38 | # If not, then move the left pointer to the right (and decrement the 39 | # count of the char that was taken out of the window by moving 40 | # the left pointer) and make the window valid again 41 | count[s[l]] -= 1 42 | l += 1 43 | 44 | # Update the max_len by comparing with current window size 45 | max_len = max(max_len, r-l+1) 46 | 47 | return max_len 48 | -------------------------------------------------------------------------------- /sliding_window/longest_substring_without_repeating_chars.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/longest-substring-without-repeating-characters/ 3 | """ 4 | 5 | 6 | class Solution: 7 | def lengthOfLongestSubstring(self, s: str) -> int: 8 | 9 | """ 10 | Approach 1: Sliding window with two pointers. Maintain a character set representing all the 11 | characters inside the window. If character repeats in a window, move the left pointer until 12 | the window is valid again i.e., no repeating characters in the window. 13 | """ 14 | 15 | # Charset to maintain the non-repeating sequence in the sliding window 16 | charSet = set() 17 | 18 | # Left pointer that does not move until we encounter a duplicate character 19 | l = 0 20 | 21 | max_len = 0 22 | 23 | # Move the right pointer while the left stays constant for the sliding window 24 | for r in range(len(s)): 25 | 26 | # If we encounter a duplicate character, increment the left pointer 27 | while s[r] in charSet: 28 | # Remove the char at left pointer and increment the left pointer by 1 29 | charSet.remove(s[l]) 30 | l += 1 31 | # We move the left pointer until we reach a non-duplicate character 32 | # i.e. until the window is valid again 33 | 34 | # If not duplicate, then add `s[r]` to the `charSet` 35 | charSet.add(s[r]) 36 | 37 | # Update the max length of the subseq so far 38 | curr_len = r-l+1 39 | max_len = max(max_len, curr_len) 40 | 41 | return max_len 42 | 43 | 44 | """ Approach 2: Also sliding window approach: O(n) time complexity """ 45 | 46 | # # Init two pointers - 'start' stays constant (represents start of the current substring), 47 | # # and 'j' moves to the right checking to compare the characters and trying to 48 | # # build the longest substring 49 | # start = 0 50 | # # Dict that stores the last seen index of the characters in the string 51 | # chars = {} 52 | # # Stores the max length of the non-repeating substring 53 | # max_len = 0 54 | 55 | # # 'j' moves from start to end of the string 56 | # for j, c in enumerate(s): 57 | # # Checks if the current char has been repeated or not 58 | # if c in chars: 59 | # ''' 60 | # If repeated, move 'start' to the right. 61 | # Why do 'max'? Because 'start' should always move to the right and doing 62 | # a 'max' ensures that (since it'll always pick the bigger value i.e. to the right) 63 | # ''' 64 | # start = max ( start, chars[c] + 1 ) 65 | 66 | # # If curr character is not repeated, then calculate the new 'max_len' 67 | # max_len = max( max_len, j - start + 1 ) 68 | # # and also add the curr character to the used 'chars' dict with 69 | # # the index of latest occurrence 70 | # chars[c] = j 71 | 72 | # return max_len 73 | 74 | 75 | """ Approach 3: Same approach 2, more concise implementation """ 76 | 77 | # # Dict to store the index of each character while iterating the input string 78 | # chars = {} 79 | # # 'start' is the start index of the current longest substring (slow pointer), 80 | # # 'maxLen' is well self-explanatory 81 | # start = max_len = 0 82 | 83 | # for j, c in enumerate(s): 84 | # if c in chars and start <= chars[c]: 85 | # # Here instead of using 'max' to update start when we see a repeated character, 86 | # # we use the conditon 'start <= chars[c]' to ensure that start moves to the right only! 87 | # start = chars[c] + 1 88 | # else: 89 | # # If not repeated char, check if curr substring length greater than max_len and update 90 | # max_len = max(max_len, j-start+1) 91 | 92 | # # In either case, update the index of the occurence of the current character 93 | # chars[c] = j 94 | 95 | # return max_len 96 | -------------------------------------------------------------------------------- /sliding_window/minimum_window_substring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/minimum-window-substring/ 3 | """ 4 | 5 | class Solution: 6 | def minWindow(self, s: str, t: str) -> str: 7 | """ 8 | Approach: Sliding window with two pointers - 9 | 10 | 1. Count the frequency of characters in `t` 11 | 2. Maintain two counters - `have` & `need` 12 | 3. Sliding window over the main string `s` and keep moving right pointer `r` 13 | 3.1. For each char in `s`, check if belongs to `t`. If it does, increment `have` counter 14 | 3.2 Also check if the `have` what we `need` i.e. if have == need 15 | 3.3 If they do, update result by taking a `min`. 16 | 3.4 Start popping characters from the left to make the window smaller and keep checking if `have == need` 17 | 4. Repeat until you go over all the characters in `s` 18 | 19 | So, we move `right` pointer in `have != need` (the for loop at line 43) and `left` pointer if `have == need` (line 56) 20 | 21 | Time complexity: O(N) 22 | """ 23 | 24 | # Edge case 25 | if not t: 26 | return "" 27 | 28 | countT, window = {}, {} 29 | 30 | ## 1. Populate the frequency count for string `t` - the substring we need to find the window for 31 | for c in t: 32 | countT[c] = 1 + countT.get(c, 0) 33 | 34 | ## 2. 'have' is the current window, and 'need' is for the string `t` 35 | have, need = 0, len(countT) 36 | left = 0 37 | 38 | # Indices are set to -1 and result length set to infinity 39 | res, resLen = [-1, -1], float('inf') 40 | 41 | ## 3. Iterate over the main string `s` 42 | # Left pointer stays while right pointer moves - expanding the sliding window 43 | for right, c in enumerate(s): 44 | 45 | # Increment frequency of `c` in the 'window' dict 46 | window[c] = 1 + window.get(c, 0) 47 | 48 | ## 3.1 If the current char `c` is the char we need (i.e. a part of string `t` i.e. `countT`) 49 | # and the count of the char is equal in the current window and `countT` 50 | # then `have` counter is incremented by 1 51 | if c in countT and window[c] == countT[c]: 52 | have += 1 53 | 54 | ## 3.2 check if `have` what we `need` i.e. if `have == need` 55 | # So update result and move the left pointer! 56 | while have == need: 57 | if (right - left + 1) < resLen: 58 | ## 3.3 Update only if current window smaller than current `resLen` 59 | res = [left, right] 60 | resLen = (right - left + 1) 61 | 62 | ## 3.4 Now pop characters from the left (to check other windows) and find the smallest window. 63 | # Before incrementing left pointer (moving to the right), update count values 64 | # and check if `have` counter needs to be updated 65 | 66 | # Decrement the count of the char being removed from the window 67 | left_char = s[left] 68 | window[left_char] -= 1 69 | 70 | # Also, decrement `have` by removing the character that we took out of the sliding window (by moving right) 71 | if s[left] in countT and window[left_char] < countT[left_char]: 72 | have -= 1 73 | 74 | # Finally, increment the left pointer 75 | left += 1 76 | 77 | ## Finally, at the end of the for loop, extract the left and right indices for the minimum window (stored in `res`) 78 | l, r = res 79 | 80 | # Check if `resLen` actually exists or else return empty string 81 | return s[l:r+1] if resLen != float('inf') else "" 82 | -------------------------------------------------------------------------------- /sliding_window/permutation_in_strings.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/permutation-in-string/ 3 | ''' 4 | 5 | class Solution: 6 | def checkInclusion(self, s1: str, s2: str) -> bool: 7 | ''' 8 | Use a sliding window to compare the count of the characters in each string. 9 | Time Complexity = O(26) + O(n) = O(n) 10 | ''' 11 | 12 | # Length of `s1` needs to be less than length of `s2` 13 | if len(s1) > len(s2): 14 | return False 15 | 16 | # Counters for the characters in each string 17 | s1Count, s2Count = [0]*26, [0]*26 18 | 19 | # Populate the character count arrays - only need to do it for the length of `s1` 20 | for i in range(len(s1)): 21 | # Get the index of the character in the `count` array 22 | s1Count[ord(s1[i]) - ord('a')] += 1 23 | s2Count[ord(s2[i]) - ord('a')] += 1 24 | 25 | # Now check the number of matches of characters in `s1Count` and `s2Count` 26 | matches = 0 27 | for i in range(26): 28 | if s1Count[i] == s2Count[i]: 29 | matches += 1 30 | 31 | # Now, sliding window over the remaining characters of `s2` to find the permutation string 32 | # Sliding window implemented using two pointers (left at 0 and right moves from 0 to end) 33 | # Note: window start at len(s1), since we already checked the characters 34 | # from 0 to len(s1) - 1 in the previous (initial) window 35 | l = 0 36 | for r in range(len(s1), len(s2)): 37 | 38 | # Return if the current window is a perfect match between `s1` and permutation in `s2` 39 | if matches == 26: 40 | return True 41 | 42 | ## Add the right character (at index `r`) into the window and update the `matches` counter 43 | index = ord(s2[r]) - ord('a') 44 | s2Count[index] += 1 45 | # If adding the character makes the count in both counters equal, increment `matches` 46 | if s1Count[index] == s2Count[index]: 47 | matches += 1 48 | # But if adding the character makes the count in s1Count one less than s2Count, 49 | # then we created a mismatch, so decrement `matches` 50 | elif s1Count[index] + 1 == s2Count[index]: 51 | matches -= 1 52 | 53 | ## Same operation for left character, but here we are removing a character (since the sliding window moves to the right) 54 | index = ord(s2[l]) - ord('a') 55 | s2Count[index] -= 1 56 | # Same condition check as done for left character 57 | if s1Count[index] == s2Count[index]: 58 | matches += 1 59 | # Here, if removing the left character, made the s1Count of the index one less than s2Count 60 | # we created a mismatch, so we decrement `matches` 61 | elif s1Count[index] - 1 == s2Count[index]: 62 | matches -= 1 63 | 64 | # Remember to update the left pointer! 65 | l += 1 66 | 67 | # Finally, check if the matches are 26, then permutation exists, else doesn't! 68 | return True if matches == 26 else False 69 | -------------------------------------------------------------------------------- /sliding_window/sliding_window_maximum.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/sliding-window-maximum/ 3 | ''' 4 | 5 | from typing import List 6 | import collections 7 | 8 | class Solution: 9 | def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: 10 | ''' 11 | Use a monotonically decerasing deque to keep track of the max value in each window. 12 | The deque will have the maxValue at the start of the queue (rightmost position). 13 | 14 | Sliding window implemented using two pointers. 15 | 16 | Time Complexity: O(n) 17 | ''' 18 | 19 | output = [] 20 | # Pointers for sliding window 21 | l = r = 0 22 | # Monotonically decreasing deque (left to right) i.e. max value at index 0 of the queue 23 | q = collections.deque() # Store the indices NOT the actual value 24 | 25 | # Iterate until the right pointer exceeds length of `nums` 26 | while r < len(nums): 27 | 28 | # If current number greater than rightmost value in queue, pop values from right until this is False 29 | while q and nums[r] > nums[q[-1]]: 30 | q.pop() 31 | 32 | # Then add the current number to the queue, this now adds the new max value to the queue 33 | # Note: We append indices of the element to the queue, not the value! 34 | q.append(r) 35 | 36 | # Also, check if the left value is within the window bounds. If not, remove it. 37 | if l > q[0]: 38 | q.popleft() 39 | 40 | # Check if the window is at size `k` and add the current max (stored in the leftmost pos i.e. index 0) from queue 41 | if (r + 1) >= k: 42 | output.append(nums[q[0]]) 43 | # Also increment the left pointer (i.e. move the window to the right) 44 | l += 1 45 | 46 | # Also, do the same for the left pointer 47 | r += 1 48 | 49 | # Finally, return the `output` array 50 | return output 51 | -------------------------------------------------------------------------------- /stack/valid_parentheses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/valid-parentheses/ 3 | """ 4 | 5 | 6 | class Solution: 7 | def isValid(self, s: str) -> bool: 8 | ## Implemented using stacks 9 | 10 | # Create a dict of `closing` brackets as `keys` and 'opening' as `values` 11 | closeToOpen = {')':'(', '}':'{', ']':'['} 12 | # List as an empty stack 13 | stack = [] 14 | 15 | # Iterate through the input string 16 | for b in s: 17 | """ 18 | If `stack` isn't empty AND we have a valid bracket 19 | AND if last brack from stack is equal to closing brack's key 20 | from dict i.e. opening, 21 | then remove that pair of brackets from the stack. 22 | """ 23 | 24 | # We check if it's a valid bracket and if the stack is non-empty 25 | if b in closeToOpen and stack: 26 | 27 | # Finally we check if the current closing parantheses matches the opening in the stack 28 | if stack[-1] == closeToOpen[b]: 29 | # If it is, then pop and move to the next bracket 30 | stack.pop() 31 | 32 | # If not, then invalid case, so we return False 33 | else: 34 | return False 35 | 36 | # Here, we have another opening bracket or the stack is empty 37 | else: 38 | stack.append(b) 39 | 40 | """ Returns True if stack is empty (checked using 'not stack') and vice-versa """ 41 | return True if not stack else False 42 | 43 | """ Alternative code for same approach """ 44 | 45 | # closeToOpen = {")": "(", "]": "[", "}": "{"} 46 | # stack = [] 47 | 48 | # for c in s: 49 | 50 | # # If c not in `closeToOpen` dict, that means it's an opening bracket 51 | # if c not in closeToOpen: 52 | # # Add to stack and skip to next iteration 53 | # stack.append(c) 54 | # continue 55 | 56 | # # If not previous condition, then it's a closing bracket 57 | # # Now, check for non-valid condition i.e. top of stack doesn't match 58 | # # current bracket's opening bracket 59 | # if not stack or stack[-1] != closeToOpen[c]: 60 | # # If doesn't match, invalid condition -> return False 61 | # return False 62 | 63 | # # If none of the previous conditions satisfied, valid bracket 64 | # # So we pop from the stack! 65 | # stack.pop() 66 | 67 | # # `not stack` means stack is empty i.e. valid else False if stack is not empty 68 | # return True if not stack else False 69 | -------------------------------------------------------------------------------- /trees/binary_tree_level_order_traversal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/binary-tree-level-order-traversal/ 3 | """ 4 | 5 | 6 | from typing import List, Optional 7 | from collections import deque 8 | 9 | 10 | class TreeNode: 11 | """ 12 | Definition for a binary tree node. 13 | """ 14 | def __init__(self, val=0, left=None, right=None): 15 | self.val = val 16 | self.left = left 17 | self.right = right 18 | 19 | 20 | class Solution: 21 | """ 22 | Approach: Use iterative BFS, since it is 'level' order (BFS -> breadth == level) 23 | Remember DFS is for Depth-First traversal/search! 24 | 25 | Time complexity: O(N) since we do a BFS on the tree. 26 | Space complexity: O(N), since the queue stores upto N/2 nodes in it. 27 | This makes O(N/2) -> O(N) space complexity 28 | """ 29 | 30 | def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: 31 | # Empty case, return empty result 32 | if not root: 33 | return [] 34 | 35 | # Create a new deque for BFS with only the root and a result array 36 | queue = deque([root]) 37 | result = [] 38 | 39 | # Iterate until the queue is empty 40 | while queue: 41 | # Create a new array for saving the nodes in the current level 42 | level = [] 43 | 44 | # Iterate through the nodes in current level 45 | for i in range(len(queue)): 46 | 47 | """ 48 | Remember the steps for Iterative BFS (done using a Queue) -> 49 | - Pop the root from queue (leftmost element since FIFO) 50 | - Add the left and right children to the queue 51 | - Increment the level by 1 (since we are going level by level i.e. breadth-wise) 52 | - Continue until queue is empty (which the for loop handles) 53 | """ 54 | 55 | # Pop the root node from the left of the queue i.e. from start of the queue 56 | node = queue.popleft() 57 | # Append this value to the current level of traversal (appends to the end of the queue) 58 | level.append(node.val) 59 | 60 | # Add the left and right children to the right of the queue (if they exist) 61 | if node.left: 62 | queue.append(node.left) 63 | 64 | if node.right: 65 | queue.append(node.right) 66 | 67 | # Finally append ALL the nodes in the `level` array to the result 68 | # Added as a list, to the `result` list 69 | result.append(level) 70 | 71 | return result 72 | -------------------------------------------------------------------------------- /trees/invert_binary_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/invert-binary-tree/ 3 | """ 4 | 5 | from typing import Optional 6 | 7 | class TreeNode: 8 | """ 9 | Definition for a binary tree node. 10 | """ 11 | def __init__(self, val=0, left=None, right=None): 12 | self.val = val 13 | self.left = left 14 | self.right = right 15 | 16 | 17 | class Solution: 18 | def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: 19 | """ 20 | Approach: Use Recursive DFS to swap the children of the root. 21 | """ 22 | 23 | # Base case for recursion 24 | if not root: 25 | return None 26 | 27 | # Swap the Children of the current `root` 28 | root.left, root.right = root.right, root.left 29 | 30 | # Run invertion on left and right subtrees 31 | self.invertTree(root.left) 32 | self.invertTree(root.right) 33 | 34 | # At the end of all recursions, the final `root` will be the inverted tree. 35 | return root 36 | -------------------------------------------------------------------------------- /trees/lowest_common_ancestor_of_bst.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ 3 | """ 4 | 5 | 6 | # Definition for a binary tree node. 7 | class TreeNode: 8 | """ 9 | Definition for a binary tree node. 10 | """ 11 | def __init__(self, x): 12 | self.val = x 13 | self.left = None 14 | self.right = None 15 | 16 | 17 | class Solution: 18 | def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: 19 | """ 20 | Approach: The Lowerst Common Ancestor (LCA) is the node in the tree where the split 21 | occurs for `p` and `q` nodes in a Binary Search Tree (BST). 22 | 23 | Time complexity: O(log N), since we search the BST with DFS instead of 24 | entirely iterating it (which would take O(N) time). 25 | """ 26 | 27 | # Iterate the tree until we find the LCA (result is guaranteed to exist) 28 | while root: 29 | 30 | # If `p` and `q` are both greater than current value, 31 | # continue searching on the right of the current node of the BST 32 | if root.val < p.val and root.val < q.val: 33 | root = root.right 34 | 35 | # Vice-versa of previous condition. i.e. p and q are both less than current value 36 | elif root.val > p.val and root.val > q.val: 37 | root = root.left 38 | 39 | # If not previous cases, then we have found our split, i.e. the LCA 40 | else: 41 | return root 42 | -------------------------------------------------------------------------------- /trees/max_depth_of_binary_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/maximum-depth-of-binary-tree/ 3 | """ 4 | 5 | 6 | from typing import Optional 7 | 8 | 9 | class TreeNode: 10 | """ 11 | Definition for a binary tree node 12 | """ 13 | def __init__(self, val=0, left=None, right=None): 14 | self.val = val 15 | self.left = left 16 | self.right = right 17 | 18 | 19 | class Solution: 20 | def maxDepth(self, root: Optional[TreeNode]) -> int: 21 | """ 22 | Approach 1: Recursive DFS 23 | From a root node, find the max between the left and the right subtrees. 24 | The final max depth is `1 + the maximum between left subtree max depth and right subtree max depth` 25 | """ 26 | 27 | # Base case 28 | if not root: 29 | return 0 30 | 31 | # Find the max between the left and right subtrees 32 | # 1 is added here for the current level's depth! 33 | return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right)) 34 | 35 | """ 36 | Approach 2: Iterative BFS 37 | Count the number of levels which will be maximum depth of the tree. 38 | BFS done using a Queue, where for every iteration -> 39 | - Pop the root from queue (leftmost element since FIFO) 40 | - Add the left and right children to the queue 41 | - Increment the level by 1 (since we are going level by level i.e. breadth-wise) 42 | - Continue until queue is empty 43 | """ 44 | 45 | # # Init level and add the root to the deque 46 | # level = 0 47 | # q = deque([root]) 48 | 49 | # # Keep going until you reach the leaf node i.e. until deque is empty 50 | # while q: 51 | 52 | # # Iterate through the current node 53 | # for i in range(len(q)): 54 | # # Pop the root from the left (start) 55 | # node = q.popleft() 56 | # # Insert the left and right children to the right (end) of the deque 57 | # if node.left: 58 | # q.append(node.left) 59 | # if node.right: 60 | # q.append(node.right) 61 | 62 | # # Finally increment the level by 1 63 | # level += 1 64 | 65 | # return level 66 | -------------------------------------------------------------------------------- /trees/same_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/same-tree/ 3 | """ 4 | 5 | 6 | from typing import Optional 7 | 8 | 9 | class TreeNode: 10 | """ 11 | Definition for a binary tree node. 12 | """ 13 | def __init__(self, val=0, left=None, right=None): 14 | self.val = val 15 | self.left = left 16 | self.right = right 17 | 18 | 19 | 20 | class Solution: 21 | def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: 22 | """ 23 | Approach: Use Recursive DFS, with stopping conditions for when the nodes are null or their values are unequal. 24 | 25 | Time Complexity: O(P+Q), where P and Q are the number of nodes in the two trees 26 | """ 27 | 28 | # Base case for recursive call 29 | # # If both are null, then equal 30 | if not p and not q: 31 | return True 32 | 33 | # If only one of them null (both being null is covered in previous condition), then not equal 34 | # OR both not null BUT have unequal values 35 | if (not p or not q) or (p.val != q.val): 36 | return False 37 | 38 | # Recursive DFS calls 39 | # Here, both `p` and `q` are not null and their values match, 40 | # so recursively check the left and right subtrees of each for equality. 41 | return (self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)) 42 | -------------------------------------------------------------------------------- /trees/subtree_of_another_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/subtree-of-another-tree/ 3 | """ 4 | 5 | 6 | from typing import Optional 7 | 8 | 9 | class TreeNode: 10 | """ 11 | Definition for a binary tree node 12 | """ 13 | def __init__(self, val=0, left=None, right=None): 14 | self.val = val 15 | self.left = left 16 | self.right = right 17 | 18 | class Solution: 19 | 20 | """ 21 | Approach: Recursive DFS with helper function to check if two given trees are the same. 22 | Time Complexity: O(root * subRoot) 23 | """ 24 | 25 | def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool: 26 | 27 | # If `subRoot` is empty, then always a subtree of `root` 28 | # Since an empty tree is a subtree of any tree. 29 | if not subRoot: 30 | return True 31 | 32 | # If `root` is empty (and `subRoot` is non-empty cause it skipped the previous IF statment), 33 | # then `subRoot` CAN'T be a subtree of `root` 34 | if not root: 35 | return False 36 | 37 | # Here we check if `root` and `subRoot` are the same Tree -> Base case for recursion 38 | if self.isSameTree(root, subRoot): 39 | return True 40 | 41 | # Finally we check if the left or the right subtrees of the `root` are equal to the `subRoot`. 42 | # Note the `OR` condition, since the subRoot can be on the left OR the right of the `root` 43 | return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot) 44 | 45 | 46 | # Helper function to check if `p` and `q` are the same trees. 47 | # Refer to https://leetcode.com/problems/same-tree/ and `trees/same_tree.py` for explanation. 48 | def isSameTree(self, p, q): 49 | 50 | if not p and not q: 51 | return True 52 | 53 | if (not p or not q) and (p.val != q.val): 54 | return False 55 | 56 | return self.isSameTree(p.right, q.right) and self.isSameTree(p.left, q.left) 57 | -------------------------------------------------------------------------------- /trees/valid_bst.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/validate-binary-search-tree/ 3 | """ 4 | 5 | 6 | from typing import Optional 7 | 8 | 9 | class TreeNode: 10 | """ 11 | Definition for a binary tree node. 12 | """ 13 | def __init__(self, val=0, left=None, right=None): 14 | self.val = val 15 | self.left = left 16 | self.right = right 17 | 18 | 19 | class Solution: 20 | """ 21 | Approach: Recusrive DFS using helper function. 22 | 23 | Time complexity = O(N), since visiting all nodes at least once. 24 | Space complexity = O(N), since storing all the node values in stack during recursion. 25 | """ 26 | 27 | def isValidBST(self, root: Optional[TreeNode]) -> bool: 28 | # Initial values for boundaries of root node are + and - infinity! 29 | return self.isValid(root, float('-inf'), float('inf')) 30 | 31 | # Helper function 32 | def isValid(self, root: Optional[TreeNode], left: int, right: int) -> bool: 33 | ''' 34 | left -> int boundary that means that all nodes in the the current subtree must be smaller than this value 35 | right -> int boundary that means all values in the current subtree are greater than this value 36 | ''' 37 | 38 | # Base case 39 | if not root: 40 | return True 41 | 42 | # Check if current root follows the BST rule 43 | if not left < root.val < right: 44 | return False 45 | 46 | # Traverse to new BST with left and right nodes as new roots 47 | # i.e. recursively check the left and right subtrees of the current root. 48 | 49 | # For left subtree (LEFT node is now ROOT), every value should be LESS than root val -> right == node.val 50 | # For right subtree (RIGHT node is now ROOT), every node should be greater than root val -> left == node.val 51 | return (self.isValid(root.left, left, root.val) and self.isValid(root.right, root.val, right)) 52 | -------------------------------------------------------------------------------- /tries/add_search_word.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/design-add-and-search-words-data-structure/ 3 | """ 4 | 5 | # Define a TrieNode to store information about each node in the Trie 6 | class TrieNode: 7 | """ 8 | Define a class to store each node 9 | i.e. character's information (`children` and `endOfWord` flag) 10 | """ 11 | def __init__(self): 12 | self.children = {} 13 | self.endOfWord = False 14 | 15 | 16 | class WordDictionary: 17 | 18 | def __init__(self): 19 | # Init root of the WordDictionary Trie as an empty TrieNode 20 | self.root = TrieNode() 21 | # Add word length so that we don't search for words longer than the current longest word 22 | self.max_word_len = 0 23 | 24 | def addWord(self, word: str) -> None: 25 | # Similar to adding word in https://leetcode.com/problems/implement-trie-prefix-tree/ 26 | cur = self.root 27 | 28 | for c in word: 29 | # If the character not a part of the Trie, add it 30 | if c not in cur.children: 31 | cur.children[c] = TrieNode() 32 | cur = cur.children[c] 33 | 34 | # After adding all character, set the flag for end of word on the last character/node of the word 35 | cur.endOfWord = True 36 | 37 | # Also, update the length of the longest word in the WordDictionary 38 | self.max_word_len = max(self.max_word_len, len(word)) 39 | 40 | 41 | def search(self, word: str) -> bool: 42 | # Perform normal search as in https://leetcode.com/problems/implement-trie-prefix-tree/ for 43 | # characters but for '.' we use DFS to search all the possible values. 44 | 45 | # If we are searching for a word longer than what exists in the Trie, return False 46 | if len(word) > self.max_word_len: 47 | return False 48 | 49 | # DFS search for the word 50 | def dfs(j, root): 51 | cur = root 52 | 53 | # We start searching from index `j` in the word 54 | for i in range(j, len(word)): 55 | c = word[i] 56 | 57 | # Recursive search for when "." is present in the search string 58 | if c == ".": 59 | # Search all children of the current char in the Trie 60 | for child in cur.children.values(): 61 | 62 | # Since "." can be any character, perform a recursive DFS for the remaining characters 63 | if dfs(i+1, child): 64 | # If the rest of the characters in the children, then we have a match 65 | # Since last recursive call with return True and enter this `if` condition 66 | return True 67 | 68 | # If the rest of the children don't have a match, return False 69 | return False 70 | 71 | # If no "." encountered, the Usual search in the Trie for the current character 72 | else: 73 | if c not in cur.children: 74 | return False 75 | 76 | # If `c` is in the Trie, move to the next character (by searching in the children) 77 | cur = cur.children[c] 78 | 79 | # Finally, when you're here, all the characters in the word have been found in the Trie, BUT... 80 | # Return True ONLY IF the word is a complete word in the Trie i.e. has `endOfWord` flag set! 81 | return cur.endOfWord 82 | 83 | ## Call the recursive DFS on the Trie, starting with index 0 84 | return dfs(0, self.root) 85 | 86 | 87 | # Your WordDictionary object will be instantiated and called as such: 88 | # obj = WordDictionary() 89 | # obj.addWord(word) 90 | # param_2 = obj.search(word) -------------------------------------------------------------------------------- /tries/implement_trie_prefix_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/implement-trie-prefix-tree/ 3 | """ 4 | 5 | 6 | class TrieNode: 7 | """ 8 | Define a class to store each node's 9 | i.e. character's information (children and endOfWord flag) 10 | """ 11 | def __init__(self): 12 | self.children = {} 13 | self.endOfWord = False 14 | 15 | 16 | class Trie: 17 | 18 | def __init__(self): 19 | # Init the root of the Trie as an empty TrieNode 20 | self.root = TrieNode() 21 | 22 | def insert(self, word: str) -> None: 23 | # Start with the root of the Trie 24 | cur = self.root 25 | 26 | # Iterate through every character in the word to be inserted 27 | for c in word: 28 | # If the char doesn't exist in the Trie, Add it! 29 | if c not in cur.children: 30 | cur.children[c] = TrieNode() 31 | 32 | # Move 'cur' to the children of the current character 33 | cur = cur.children[c] 34 | 35 | # Finally, once you insert all characters, 36 | # set the `endOfWord` flag for the last character 37 | cur.endOfWord = True 38 | 39 | 40 | def search(self, word: str) -> bool: 41 | # Start the search from the root of the Trie i.e. at 'self.root' 42 | cur = self.root 43 | 44 | # Extract every character from the search word 45 | for c in word: 46 | 47 | # If current character not in the Trie and it's children, return False 48 | if c not in cur.children: 49 | return False 50 | 51 | # Else, keeping moving `cur` to the current character's children 52 | cur = cur.children[c] 53 | 54 | # Finally return True only if the word is 55 | # Fully a part of the Trie i.e. along with `endOfWord` flag 56 | return cur.endOfWord 57 | 58 | def startsWith(self, prefix: str) -> bool: 59 | # Same logic as `search` function but here we don't check `endOfWord` flag. 60 | # Since we are only checking if the prefix exists in the Trie. 61 | cur = self.root 62 | 63 | for c in prefix: 64 | if c not in cur.children: 65 | return False 66 | 67 | # Keep moving the current character to it's children 68 | cur = cur.children[c] 69 | 70 | # Return 'True' directly w/o checking `endOfWord` flag 71 | return True 72 | 73 | 74 | # Your Trie object will be instantiated and called as such: 75 | # obj = Trie() 76 | # obj.insert(word) 77 | # param_2 = obj.search(word) 78 | # param_3 = obj.startsWith(prefix) -------------------------------------------------------------------------------- /tries/word_search_ii.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/word-search-ii/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | class TrieNode: 9 | """ 10 | TrieNode class to store information about each node in the Trie. 11 | For detailed explanation, refer to solution in tries/implment_trie_prefix_tree.py 12 | """ 13 | 14 | def __init__(self): 15 | self.children = {} 16 | self.isWord = False 17 | self.refs = 0 18 | 19 | def addWord(self, word): 20 | cur = self 21 | cur.refs += 1 22 | for c in word: 23 | if c not in cur.children: 24 | cur.children[c] = TrieNode() 25 | cur = cur.children[c] 26 | cur.refs += 1 27 | cur.isWord = True 28 | 29 | def removeWord(self, word): 30 | cur = self 31 | cur.refs -= 1 32 | for c in word: 33 | if c in cur.children: 34 | cur = cur.children[c] 35 | cur.refs -= 1 36 | 37 | 38 | class Solution: 39 | def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: 40 | # Init the root of the Trie 41 | root = TrieNode() 42 | 43 | # Add all given `words` to the Trie 44 | for w in words: 45 | root.addWord(w) 46 | 47 | # Define the number of rows and columns in the board 48 | ROWS, COLS = len(board), len(board[0]) 49 | # Define the sets to store the visited cells and the result words 50 | res, visit = set(), set() 51 | 52 | # Define the DFS function to search for the words in the board 53 | def dfs(r, c, node, word): 54 | 55 | # Base case for stopping the DFS recursion 56 | if ( 57 | r not in range(ROWS) 58 | or c not in range(COLS) 59 | or board[r][c] not in node.children 60 | or node.children[board[r][c]].refs < 1 61 | or (r, c) in visit 62 | ): 63 | return 64 | 65 | # Add the current cell to the visited set (to avoid revisiting the same elements on the board) 66 | visit.add((r, c)) 67 | 68 | # Set `node` to the current node's children 69 | node = node.children[board[r][c]] 70 | 71 | # Add the current character to the word 72 | word += board[r][c] 73 | 74 | # If the current character is a word (if `isWord` == True), remove it from the Trie and add it to the result set 75 | if node.isWord: 76 | node.isWord = False 77 | res.add(word) 78 | root.removeWord(word) 79 | 80 | # Now recursively search for the next characters in the word in all 4 directions 81 | dfs(r + 1, c, node, word) 82 | dfs(r - 1, c, node, word) 83 | dfs(r, c + 1, node, word) 84 | dfs(r, c - 1, node, word) 85 | 86 | # After seacrching all 4 directions, remove the current cell from the visited set 87 | visit.remove((r, c)) 88 | 89 | 90 | ## Driver code to start the DFS search for every cell in the board (from main function) 91 | for r in range(ROWS): 92 | for c in range(COLS): 93 | # Start params for DFS: current cell, root of the Trie, and the current word 94 | dfs(r, c, root, "") 95 | 96 | # Finally, return the result set as a list 97 | return list(res) 98 | -------------------------------------------------------------------------------- /two_pointers/container_with_most_water.py: -------------------------------------------------------------------------------- 1 | """ 2 | Question: https://leetcode.com/problems/container-with-most-water/ 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | def maxArea(self, height: List[int]) -> int: 10 | """ 11 | Use Two Pointers -> `left` and `right`, compare the volume of water held by each container and only 12 | move the pointer with the smaller height to get the most water in the next container. 13 | Also, keep track of the max volume of water held by any container. 14 | 15 | Time Complexity: O(N) 16 | """ 17 | 18 | # Init the two pointers on opposite ends of the list `height` 19 | left, right = 0, len(height)-1 20 | most_water = 0 21 | 22 | # Iterate until thw two pointers cross over 23 | while left < right: 24 | 25 | # The max level of water the container can hold depends on the shortest height of the container 26 | # i.e. min of height[left] or height[right] 27 | container_height = min(height[left], height[right]) 28 | 29 | # Width of current container width will be dist b/w right and left i.e. `right - left` 30 | container_width = right - left 31 | 32 | # Now calculate the water held by current container i.e. calculate the area of container (rectangle) 33 | curr_water = container_height * container_width 34 | 35 | # Update max value of the `most_water` 36 | most_water = max(most_water, curr_water) 37 | 38 | # Update the left and right pointers for the next iteration. 39 | # We remove the side with the smaller height (to get most water in the next container) 40 | # i.e. if 'left' height is less, we move left and vice-versa 41 | if height[left] < height[right]: 42 | left += 1 43 | else: 44 | right -= 1 45 | 46 | return most_water 47 | -------------------------------------------------------------------------------- /two_pointers/three_sum.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/3sum/ 3 | ''' 4 | 5 | class Solution: 6 | def threeSum(self, nums: List[int]) -> List[List[int]]: 7 | ''' 8 | Brute force Approach: Make combinations of 3 elements and check if the sum is zero for each -> doesn't handle duplicates 9 | 10 | Optimal method: Sort the array, iterate and for current element, then implement 11 | same solution as Sorted Two Sum! 12 | 13 | Time Complexity : O(n log n) for sorting + O (n ^ 2) for the 3 Sum algorithm = O(n^2) 14 | Space Complexity : O(n), since we store the result in a new array 15 | ''' 16 | 17 | result = [] 18 | n = len(nums) 19 | 20 | # Sorting taking 'n log n' time. Allows us to skip duplicates! (check line 30) 21 | nums.sort() 22 | 23 | for i, curr_num in enumerate(nums): 24 | 25 | # Skip positive integers as first number (since you want the 3 Sum to be zero) 26 | if curr_num > 0: 27 | break 28 | 29 | # If duplicate value, go to next iteration 30 | if i > 0 and curr_num == nums[i-1]: 31 | # Since duplicate, skip to next iteration of `for` loop 32 | continue 33 | 34 | # After selecting first number, problem reduced to Two sum 35 | # i.e. Now the solution is same as Two Sum II (two sum but for sorted array) 36 | 37 | # Search on only the right side of the current number 38 | # `l` -> left pointer & `r` -> right pointer 39 | l, r = i+1, n-1 40 | 41 | # Search until left and right don't overlap 42 | while l < r: 43 | threeSum = curr_num + nums[l] + nums[r] 44 | 45 | # Sum is too big, move right closer 46 | if threeSum > 0: 47 | r -= 1 48 | 49 | # Sum is too small, move left closer 50 | elif threeSum < 0: 51 | l += 1 52 | 53 | # Match found, store the 3 numbers 54 | else: 55 | result.append([curr_num, nums[l], nums[r]]) 56 | # Remember to move to the next number in the list 57 | l += 1 58 | 59 | # Once you have found a 3 Sum combination, move to the next 60 | # non-duplicate number as the first number. 61 | # This condition avoids using duplicate values as first number. 62 | while nums[l] == nums[l-1] and l < r: 63 | # Keep moving `l` until next non-duplicate number 64 | l += 1 65 | 66 | return result -------------------------------------------------------------------------------- /two_pointers/trapping_rain_water.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/trapping-rain-water/ 3 | ''' 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | def trap(self, height: List[int]) -> int: 10 | ''' 11 | Use two pointers to keep track of the max height on each end. 12 | Keep calculating the rain water at each element's position using - `min(leftMax, rightMax) - height at ith index` 13 | Time Complexity: O(n) 14 | Space Complexity: O(1) 15 | ''' 16 | 17 | # Init the two pointers 18 | l, r = 0, len(height) - 1 19 | # Keeps track of the max height on the left and right of current position 20 | leftMax, rightMax = height[l], height[r] 21 | total_water = 0 22 | 23 | while l < r: 24 | 25 | # If leftMax is smaller, we shift the left pointer. Since we need `min(leftMax, rightMax)` 26 | if leftMax < rightMax: 27 | # Move the left pointer `l` and update `leftMax` 28 | l += 1 29 | leftMax = max(leftMax, height[l]) 30 | # Also, add the water stored in the current element to `total_water` 31 | # using 32 | total_water += leftMax - height[l] 33 | 34 | # Vice-versa if `rightMax < leftMax` 35 | else: 36 | r -= 1 37 | rightMax = max(rightMax, height[r]) 38 | total_water += rightMax - height[r] 39 | 40 | # Return the total rain water trapped 41 | return total_water 42 | -------------------------------------------------------------------------------- /two_pointers/two_sum_ii_input_sorted.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/ 3 | ''' 4 | 5 | from typing import List 6 | 7 | 8 | class Solution: 9 | def twoSum(self, numbers: List[int], target: int) -> List[int]: 10 | ''' 11 | Use two pointers and take advantage of the array being sorted. 12 | i.e. perform a binary_search on the array. 13 | 14 | Time Complexity: O(n) 15 | ''' 16 | 17 | left, right = 0, len(numbers) - 1 18 | 19 | # This condition not needed since we are guaranteed a solution! 20 | while left < right: 21 | # Compute the sum using the current positions of `left` and `right` 22 | currSum = numbers[left] + numbers[right] 23 | 24 | # If the currSum is more than target, we move the `right` pointer to make smaller 25 | if currSum > target: 26 | right -= 1 27 | # Vice-versa of the first condition, we move the `left` pointer to make `currSum` bigger 28 | elif currSum < target: 29 | left += 1 30 | 31 | # If neither of the above is true, we have found the solution 32 | else: 33 | # Remember that answer require 1-indexed solution 34 | return [left+1, right+1] 35 | 36 | return [] 37 | -------------------------------------------------------------------------------- /two_pointers/valid_palindrome.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question: https://leetcode.com/problems/valid-palindrome/ 3 | ''' 4 | 5 | class Solution: 6 | def isPalindrome(self, s: str) -> bool: 7 | ''' 8 | Use two pointers at the start and end of the string. 9 | Keep comparing until the letters are alphanumeric (use ASCII values) and equal. 10 | If they are not, return false! 11 | Time Complexity: O(n) & Space Complexity: O(1) 12 | ''' 13 | # Left pointer is at the start and right pointer is at the end of the string 14 | l, r = 0, len(s) - 1 15 | 16 | # Keep checking until the two pointers don't cross each other 17 | while l < r: 18 | 19 | # If left char is not alpha num, increment 'l' by 1 20 | while l < r and not self.isAlphaNum(s[l]): 21 | l += 1 22 | 23 | # If right char is not alpha num, decrement 'r' by 1 24 | while l < r and not self.isAlphaNum(s[r]): 25 | r -= 1 26 | 27 | # If the characters on two pointers are not equal, not a Palindrome string. 28 | # Remember to convert both chars to lower, since the string is case insensitive 29 | if s[l].lower() != s[r].lower(): 30 | return False 31 | 32 | # If all above conditions satisfied, we update 33 | # both the pointers for next iteration 34 | l, r = l+1, r-1 35 | 36 | # If all goes well, it is a Palindrome string 37 | return True 38 | 39 | def isAlphaNum(self, c): 40 | """ 41 | Method to check if a given character is alphanumeric 42 | by checking ASCII values. 43 | 44 | 'ord' gives the ASCII value of a character. 45 | Check if the current character 'c' lies between 46 | A-Z, a-z, 0-9 i.e. alphanumeric 47 | """ 48 | 49 | return (ord('A') <= ord(c) <= ord('Z') or 50 | ord('a') <= ord(c) <= ord('z') or 51 | ord('0') <= ord(c) <= ord('9')) 52 | -------------------------------------------------------------------------------- /utils/get_time_complexity.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | import random 3 | from typing import List 4 | 5 | 6 | def measure_execution_time(func, input_size): 7 | test_data = [random.randint(0, input_size) for _ in range(input_size)] 8 | start_time = timeit.default_timer() 9 | func(test_data) 10 | end_time = timeit.default_timer() 11 | return end_time - start_time 12 | 13 | 14 | def infer_time_complexity(exec_times, input_sizes): 15 | ratios = [] 16 | for i in range(1, len(exec_times)): 17 | time_increase = exec_times[i] / exec_times[i-1] 18 | size_increase = input_sizes[i] / input_sizes[i-1] 19 | ratio = time_increase / size_increase 20 | ratios.append(ratio) 21 | 22 | # Heuristic to determine time complexity 23 | if all(ratio < 2 for ratio in ratios): 24 | return "O(N) - Linear" 25 | elif all(2 <= ratio < (input_sizes[i] / input_sizes[i - 1]) ** 2 for i, ratio in enumerate(ratios, start=1)): 26 | return "O(N^2) - Quadratic" 27 | elif all(ratio < 1.5 for ratio in ratios): # Assuming very slow increase for logarithmic 28 | return "O(log N) - Logarithmic" 29 | elif all(1 < ratio < 2 for ratio in ratios): # Assuming moderate increase for linearithmic 30 | return "O(N log N) - Linearithmic" 31 | else: 32 | return "Other - Could be exponential or unknown" 33 | 34 | 35 | # Create generic functions to measure time complexity of any function passed as argument 36 | def calc_time_complexity(func, input_sizes: List[int] = (100, 1000, 10000, 100000)): 37 | exec_times = [measure_execution_time(func, size) for size in input_sizes] 38 | complexity = infer_time_complexity(exec_times, input_sizes) 39 | print(f"`{func.__name__}` has `{complexity}` Time Complexity.") 40 | return complexity 41 | --------------------------------------------------------------------------------