diff --git a/Problem1.py b/Problem1.py new file mode 100644 index 00000000..6ba825c5 --- /dev/null +++ b/Problem1.py @@ -0,0 +1,64 @@ +# https://leetcode.com/problems/validate-binary-search-tree/ +# 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 +class Solution: + def isValidBST(self, root: Optional[TreeNode]) -> bool: + # prev = None + # isValid = True + # def helper(root): + # nonlocal prev, isValid + # if root == None: + # return + + # helper(root.left) + # if prev and prev.val >= root.val: + # isValid = False + # prev = root + # helper(root.right) + + # helper(root) + # return isValid + + + + """ + Time Complexity: O(N) + - N is the total number of nodes in the binary tree. + - We visit every single node exactly once to check its value against the boundaries. + + Space Complexity: O(N) in the worst case, O(log N) in the best/average case + - The space is consumed by the recursion call stack. + - Worst case: O(N) if the tree is completely unbalanced (like a linked list). + - Best/Average case: O(log N) if the tree is balanced, where the height of + the tree dictates the maximum depth of the recursive stack. + """ + + # Helper function that passes the valid boundary limits down the tree + def helper(node, minVal, maxVal): + # Base Case: Reaching the end of a branch (an empty node) means + # this path is valid so far. + if node == None: + return True + + # Logic: The current node must be strictly greater than the minimum boundary. + # (minVal is updated when we move to the right child of an ancestor). + if minVal is not None and node.val <= minVal: + return False + + # Logic: The current node must be strictly less than the maximum boundary. + # (maxVal is updated when we move to the left child of an ancestor). + if maxVal is not None and node.val >= maxVal: + return False + + # Recursive Step: + # 1. Check the left subtree: The current node's value becomes the new strict upper limit (maxVal). + # 2. Check the right subtree: The current node's value becomes the new strict lower limit (minVal). + # Both sides must return True for the entire tree to be valid. + return helper(node.left, minVal, node.val) and helper(node.right, node.val, maxVal) + + # Start at the root. Initially, there are no upper or lower boundaries. + return helper(root, None, None) \ No newline at end of file diff --git a/Problem2.py b/Problem2.py new file mode 100644 index 00000000..feac8e15 --- /dev/null +++ b/Problem2.py @@ -0,0 +1,63 @@ +# https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ +# 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 + +class Solution: + def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]: + """ + Logic: + 1. The first element in the `preorder` array is always the root of the current tree/subtree. + 2. We locate this root value in the `inorder` array. + 3. In an inorder traversal, everything to the left of the root belongs to the left subtree, + and everything to the right belongs to the right subtree. + 4. We use the size of the left subtree (from `inorder`) to figure out which elements in + `preorder` belong to the left vs. right subtrees. + 5. We recursively repeat this process for the left and right subtrees. + + Time Complexity: O(N^2) in the worst case (where N is the number of nodes). + For each node, we are searching for its index in `inorder` which takes O(N) time. + Additionally, list slicing takes O(N) time. In a skewed tree, the recursion depth + is N, resulting in N * O(N) = O(N^2) time. + + Space Complexity: O(N^2) in the worst case. At each recursive call, we are creating new arrays + via list slicing (`inLeft`, `inRight`, etc.). For a skewed tree with depth N, + creating these slices repeatedly consumes O(N^2) memory. The recursion stack + also takes O(N) space. + """ + + # Base case: If there are no elements left, the subtree is empty + if len(preorder) == 0: + return None + + # The root of the current subtree is always the first element in preorder + rootVal = preorder[0] + + # Find the index of this root in the inorder list + inorderRootIdx = -1 + for i in range(len(inorder)): + if inorder[i] == rootVal: + inorderRootIdx = i + break + + # Split the inorder list into left and right subtrees + inLeft = inorder[0:inorderRootIdx] + inRight = inorder[(inorderRootIdx + 1):len(inorder)] + + # Split the preorder list into left and right subtrees + # The number of elements in the left subtree is len(inLeft). + # We take that many elements from preorder (skipping the first element, which is the root). + preLeft = preorder[1:(1 + len(inLeft))] + preRight = preorder[(1 + len(inLeft)):len(preorder)] + + # Construct the root node + root = TreeNode(rootVal) + + # Recursively construct the left and right subtrees using the sliced arrays + root.left = self.buildTree(preLeft, inLeft) + root.right = self.buildTree(preRight, inRight) + + return root \ No newline at end of file