diff --git a/coin-change/alphaorderly.py b/coin-change/alphaorderly.py new file mode 100644 index 0000000000..dca5339002 --- /dev/null +++ b/coin-change/alphaorderly.py @@ -0,0 +1,72 @@ +""" +Time Complexity: O(n * amount) +Space Complexity: O(n * amount) + +- DP / Top-Down approach +- Use a cache to store the results of the subproblems +""" +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + LARGE = 10 ** 5 + + @cache + def dp(coin: int, left: int) -> int: + if left == 0: + return 0 + + if left < 0 or coin >= len(coins): + return LARGE + + a = dp(coin, left - coins[coin]) + 1 + b = dp(coin + 1, left) + + return min(a, b) + + ans = dp(0, amount) + + return ans if ans != LARGE else -1 + +""" +Time Complexity: O(n * amount) +Space Complexity: O(n * amount) + +- DP / Bottom-Up approach +- Use a 2D array to store the results of the subproblems +""" +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + N = len(coins) + dp = [[float('inf')] * (amount + 1) for _ in range(N)] + + for c in range(N): + dp[c][0] = 0 + + if c > 0: + for a in range(amount + 1): + dp[c][a] = dp[c - 1][a] + + for a in range(coins[c], amount + 1): + dp[c][a] = min(dp[c][a], dp[c][a - coins[c]] + 1) + + ans = dp[-1][-1] + + return ans if ans != float('inf') else -1 + +""" +Time Complexity: O(n * amount) +Space Complexity: O(amount) + +- DP / Bottom-Up approach [Space Optimized] +- Use a 1D array to store the results of the subproblems +""" +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + N = len(coins) + dp = [float("inf")] * (amount + 1) + dp[0] = 0 + + for c in range(N): + for a in range(coins[c], amount + 1): + dp[a] = min(dp[a], dp[a - coins[c]] + 1) + + return dp[-1] if dp[-1] != float("inf") else -1 diff --git a/find-minimum-in-rotated-sorted-array/alphaorderly.py b/find-minimum-in-rotated-sorted-array/alphaorderly.py new file mode 100644 index 0000000000..c8e4cf760a --- /dev/null +++ b/find-minimum-in-rotated-sorted-array/alphaorderly.py @@ -0,0 +1,40 @@ +""" +Time Complexity: O(log N) +Space Complexity: O(1) + +### Binary Search Solution ### + +1. Initialize two pointers, left and right, to the start and end of the array +2. While left is less than right, calculate the middle index +3. If nums[mid] > nums[right], search the right half (left = mid + 1) + - Why? mid is in the left sorted portion (larger values), so the rotation point (minimum) must be to the right of mid +4. Otherwise, search the left half including mid (right = mid) + - nums[mid] <= nums[right] means mid is in the right sorted portion or the array is not rotated, so the minimum is at mid or to its left +5. When left == right, return nums[right] +""" +class Solution: + def findMin(self, nums: List[int]) -> int: + N = len(nums) + left, right = 0, N - 1 + + while left < right: + mid = (left + right) // 2 + + if nums[mid] > nums[right]: + left = mid + 1 + else: + right = mid + + return nums[right] + +""" +Time Complexity: O(N) +Space Complexity: O(1) + +### Linear Search Solution ### ( NOT RECOMMENDED ) + +1. Return the minimum element in the array +""" +class Solution: + def findMin(self, nums: List[int]) -> int: + return min(nums) diff --git a/maximum-depth-of-binary-tree/alphaorderly.py b/maximum-depth-of-binary-tree/alphaorderly.py new file mode 100644 index 0000000000..d3e57b3354 --- /dev/null +++ b/maximum-depth-of-binary-tree/alphaorderly.py @@ -0,0 +1,122 @@ +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right +""" +Time Complexity: O(N) +Space Complexity: O(N) - Recursive stack space + +### Recursive Solution ( DFS ) ### + +1. If the root is None, return 0 +2. Return the maximum of the depth of the left and right subtrees + 1 +""" +class Solution: + def maxDepth(self, root: Optional[TreeNode]) -> int: + if not root: + return 0 + + return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1 + +""" +Time Complexity: O(N) +Space Complexity: O(N) - Stack space + +### Iterative Solution ( DFS ) ### + +1. If the root is None, return 0 +2. Use a stack to store the nodes and the level of the nodes +3. Pop the nodes from the stack and update the maximum depth +4. If the node has a left child, add the left child and the level + 1 to the stack +5. If the node has a right child, add the right child and the level + 1 to the stack +6. Return the maximum depth +""" +class Solution: + def maxDepth(self, root: Optional[TreeNode]) -> int: + if not root: + return 0 + + s = [(root, 1)] + ans = 1 + + while s: + node, level = s.pop() + ans = max(ans, level) + + if node.left: + s.append((node.left, level + 1)) + + if node.right: + s.append((node.right, level + 1)) + + return ans + +""" +Time Complexity: O(N) +Space Complexity: O(N) - Queue space + +### Iterative Solution ( BFS ) ### + +1. If the root is None, return 0 +2. Use a queue to store the nodes and the level of the nodes +3. Pop the nodes from the queue and update the maximum depth +4. If the node has a left child, add the left child and the level + 1 to the queue +5. If the node has a right child, add the right child and the level + 1 to the queue +6. Return the maximum depth +""" +class Solution: + def maxDepth(self, root: Optional[TreeNode]) -> int: + if not root: + return 0 + + q = deque([(root, 1)]) + ans = 1 + + while q: + node, level = q.popleft() + ans = max(ans, level) + + if node.left: + q.append((node.left, level + 1)) + + if node.right: + q.append((node.right, level + 1)) + + return ans + +""" +Time Complexity: O(N) +Space Complexity: O(N) - Queue space + +### Iterative Solution ( Level Order Traversal ) ### + +1. If the root is None, return 0 +2. Use a queue to store the nodes +3. Pop the nodes from the queue and update the maximum depth +4. If the node has a left child, add the left child to the queue +5. If the node has a right child, add the right child to the queue +6. Return the maximum depth +""" +class Solution: + def maxDepth(self, root: Optional[TreeNode]) -> int: + if not root: + return 0 + + q = deque([root]) + level = 0 + + while q: + level += 1 + N = len(q) + for _ in range(N): + node = q.popleft() + + if node.left: + q.append(node.left) + + if node.right: + q.append(node.right) + + return level diff --git a/merge-two-sorted-lists/alphaorderly.py b/merge-two-sorted-lists/alphaorderly.py new file mode 100644 index 0000000000..0f4655fc63 --- /dev/null +++ b/merge-two-sorted-lists/alphaorderly.py @@ -0,0 +1,57 @@ +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +""" +Time Complexity: O(N) +Space Complexity: O(1) + +1. Create a dummy node to store the head of the merged list ( return_node ) +2. Traverse the two lists until one of the lists is empty +3. Compare the values of the two nodes and add the smaller node to the dummy node +4. Move the pointer of the list with the smaller node to the next node +5. Add the remaining nodes of the non-empty list to the dummy node +6. Return the next node of the dummy node +""" +class Solution: + def mergeTwoLists( + self, list1: Optional[ListNode], list2: Optional[ListNode] + ) -> Optional[ListNode]: + head = ListNode() + node = head + + while list1 and list2: + if list1.val > list2.val: + head.next = list2 + list2 = list2.next + else: + head.next = list1 + list1 = list1.next + + head = head.next + + head.next = list1 or list2 + + return node.next + +""" +Time Complexity: O(N) +Space Complexity: O(N) - Recursive stack space + +1. If one of the lists is empty, return the other list +2. Compare the values of the two nodes and add the smaller node to the merged list +3. Move the pointer of the list with the smaller node to the next node +4. Return the next node of the merged list +""" +class Solution: + def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: + if not list1 or not list2: + return list1 or list2 + + if list1.val > list2.val: + list2.next = self.mergeTwoLists(list1, list2.next) + return list2 + else: + list1.next = self.mergeTwoLists(list1.next, list2) + return list1 diff --git a/word-search/alphaorderly.py b/word-search/alphaorderly.py new file mode 100644 index 0000000000..1b2b60d689 --- /dev/null +++ b/word-search/alphaorderly.py @@ -0,0 +1,147 @@ + +""" +Time Complexity: O(N * 4^L) +Space Complexity: O(L) + +### Classic Backtracking Solution ### + +1. Initialize a set to store the cells that have been visited +2. Define a helper function to check if the cell is within the bounds of the board +3. Define a helper function to perform the backtracking +4. If the current index is equal to the length of the word, return True +5. If the cell is out of bounds or the cell does not match the current character, return False +6. Add the cell to the set ( to prevent revisiting the same cell ) +7. Recursively call the helper function for the four possible directions +8. Remove the cell from the set ( backtracking ) +9. Return False ( if the current path does not lead to the target word ) +""" +class Solution: + def exist(self, board: List[List[str]], word: str) -> bool: + check = set() + ROW, COL = len(board), len(board[0]) + + def bound(r: int, c: int) -> bool: + return 0 <= r < ROW and 0 <= c < COL + + def backtracking(row: int, col: int, n: int) -> bool: + nonlocal check + + if n == len(word): + return True + + if not bound(row, col) or board[row][col] != word[n] or (row, col) in check: + return False + + check.add((row, col)) + if backtracking(row + 1, col, n + 1): + return True + if backtracking(row, col + 1, n + 1): + return True + if backtracking(row - 1, col, n + 1): + return True + if backtracking(row, col - 1, n + 1): + return True + check.remove((row, col)) + + return False + + for row in range(len(board)): + for col in range(len(board[row])): + if backtracking(row, col, 0): + return True + + return False + +""" +Time Complexity: O(N * 4^L) +Space Complexity: O(L) + +### Optimized Backtracking Solution ### + +1. Check if the board is larger than the word size itself +2. Check if the board can assemble the word +3. Make the word to search sparse character first +4. Start the backtracking +5. Check if the cell is within the bounds of the board +6. Check if the cell matches the current character +7. Check if the cell has been visited +8. Mark the cell as visited +9. Recursively call the helper function for the four possible directions +10. Unmark the cell as visited +11. Return False ( if the current path does not lead to the target word ) + + +### Optimizing point ### +1. Check board is larger than word size itself +2. Check board can assemble word +3. Make word to search sparse character first +4. Use check as 2D matrix not set for better performance +""" +class Solution: + def exist(self, board: List[List[str]], word: str) -> bool: + ROW, COL = len(board), len(board[0]) + N = len(word) + + ### 1. Check board is larger than word size itself + if ROW * COL < len(word): + return False + + ### 2. Check board can assemble word + board_counter = Counter(board[r][c] for r in range(ROW) for c in range(COL)) + word_counter = Counter(word) + + for ch, freq in word_counter.items(): + if board_counter[ch] < freq: + return False + + ### 3. Make word to search sparse character first + if word_counter[word[0]] > word_counter[word[-1]]: + word = word[::-1] + + ### -> START BACKTRCKING <- ### + + ### Prevent overlapping + check = [[False] * COL for _ in range(ROW)] + + ### Define the four possible directions + DIR = [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1] + ] + + ### Helper function to check bound + def bound(r: int, c: int) -> bool: + return 0 <= r < ROW and 0 <= c < COL + + def backtracking(r: int, c: int, n: int) -> bool: + ### If the current index is equal to the length of the word, return True + if n == N: + return True + + ### If the cell is out of bounds, return False + if not bound(r, c): + return False + + ### If the cell does not match the current character, return False + if word[n] != board[r][c]: + return False + + ### If the cell has been visited, return False + if check[r][c]: + return False + + check[r][c] = True + for dr, dc in DIR: + if backtracking(r + dr, c + dc, n + 1): + return True + check[r][c] = False + + for r in range(ROW): + for c in range(COL): + if board[r][c] == word[0]: + if backtracking(r, c, 0): + return True + + return False