diff --git a/non-overlapping-intervals/yolophg.py b/non-overlapping-intervals/yolophg.py new file mode 100644 index 000000000..4fc1f784a --- /dev/null +++ b/non-overlapping-intervals/yolophg.py @@ -0,0 +1,25 @@ +# Time Complexity: O(N log N) +# (1) sorting the intervals takes O(N log N), and +# (2) iterating through them takes O(N). +# (3) so the overall complexity is O(N log N). +# Space Complexity: O(1) - only use a few extra variables (end and res), so the space usage is constant. + +class Solution: + def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: + # sort intervals by their ending time + intervals.sort(key=lambda x: x[1]) + + # track the last non-overlapping interval's end + end = float('-inf') + # count the number of intervals we need to remove + res = 0 + + for interval in intervals: + # if it overlaps with the last interval + if interval[0] < end: + # need to remove this one + res += 1 + else: + # otherwise, update the end to the current interval's end + end = interval[1] + return res diff --git a/number-of-connected-components-in-an-undirected-graph/yolophg.py b/number-of-connected-components-in-an-undirected-graph/yolophg.py new file mode 100644 index 000000000..be7350097 --- /dev/null +++ b/number-of-connected-components-in-an-undirected-graph/yolophg.py @@ -0,0 +1,36 @@ +# Time Complexity: O(N + E) - go through all nodes & edges. +# Space Complexity: O(N) - store parent info for each node. + +class Solution: + def count_components(self, n: int, edges: List[List[int]]) -> int: + + # set up the Union-Find structure + parent = [i for i in range(n)] # initially, each node is its own parent + rank = [1] * n # used for optimization in Union operation + + # find function (with path compression) + def find(node): + if parent[node] != node: + parent[node] = find(parent[node]) # path compression + return parent[node] + + # union function (using rank to keep tree flat) + def union(node1, node2): + root1, root2 = find(node1), find(node2) + if root1 != root2: + if rank[root1] > rank[root2]: + parent[root2] = root1 + elif rank[root1] < rank[root2]: + parent[root1] = root2 + else: + parent[root2] = root1 + rank[root1] += 1 + return True # union was successful (i.e., we merged two components) + return False # already in the same component + + # connect nodes using Union-Find + for a, b in edges: + union(a, b) + + # count unique roots (number of connected components) + return len(set(find(i) for i in range(n))) diff --git a/remove-nth-node-from-end-of-list/yolophg.py b/remove-nth-node-from-end-of-list/yolophg.py new file mode 100644 index 000000000..4958c43b9 --- /dev/null +++ b/remove-nth-node-from-end-of-list/yolophg.py @@ -0,0 +1,29 @@ +# Time Complexity: O(N) - go through the list twice (once to count, once to remove). +# Space Complexity: O(1) - only use a few extra variables. + +class Solution: + def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]: + # count the total number of nodes in the list + N = 0 + curr = head + while curr: + N += 1 + curr = curr.next + + # find the position of the node to remove (from the start) + node_to_remove = N - n + + # if need to remove the first node, just return the second node as the new head + if node_to_remove == 0: + return head.next + + # traverse again to find the node right before the one we need to remove + curr = head + for i in range(N - 1): + if (i + 1) == node_to_remove: + # remove the nth node by skipping it + curr.next = curr.next.next + break + curr = curr.next + + return head diff --git a/same-tree/yolophg.py b/same-tree/yolophg.py new file mode 100644 index 000000000..085225351 --- /dev/null +++ b/same-tree/yolophg.py @@ -0,0 +1,19 @@ +# Time Complexity: O(N) - visit each node once. +# Space Complexity: O(N) in the worst case (skewed tree), O(log N) in the best case (balanced tree). + +class Solution: + def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: + # if both trees are empty, they are obviously the same + if p is None and q is None: + return True + + # if one tree is empty but the other is not, they can't be the same + if p is None or q is None: + return False + + # if values of the current nodes are different, trees are not the same + if p.val != q.val: + return False + + # recursively check both left and right subtrees + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) diff --git a/serialize-and-deserialize-binary-tree/yolophg.py b/serialize-and-deserialize-binary-tree/yolophg.py new file mode 100644 index 000000000..fe2e24360 --- /dev/null +++ b/serialize-and-deserialize-binary-tree/yolophg.py @@ -0,0 +1,58 @@ +# Time Complexity: +# (1) serialize(): O(N) because traverse all nodes once. +# (2) deserialize(): O(N) because process each node once. + +# Space Complexity: +# (1) serialize(): O(N) to store the serialized string. +# (2) deserialize(): O(N) for the queue and reconstructed tree. + +class Codec: + def serialize(self, root): + # store the tree as a list of values (BFS style) + ans = [] + if not root: + return "" + + queue = [root] + while queue: + cur = queue.pop() + if not cur: + ans.append("n") # use 'n' to represent null nodes + continue + ans.append(str(cur.val)) # add node value as string + queue.append(cur.right) # right first (for consistency in deserialization) + queue.append(cur.left) # then left + + return ",".join(ans) # convert list to comma-separated string + + def deserialize(self, data): + if not data: + return None + + data = deque(data.split(",")) # convert string back to list + root = TreeNode(int(data.popleft())) # first value is the root + queue = [(root, 0)] # track parent nodes and child positions + + while data: + if not queue: + return None # should never happen unless input is corrupted + + cur = data.popleft() + if cur == "n": + val = None # null node, no need to create a TreeNode + else: + val = TreeNode(int(cur)) # create a new TreeNode + + parent, cnt = queue.pop() # get the parent and which child we’re setting + if cnt == 0: + parent.left = val # assign left child + else: + parent.right = val # assign right child + + cnt += 1 # move to the next child + if cnt < 2: + queue.append((parent, cnt)) # if parent still needs a right child, keep it in the queue + if val: + queue.append((val, 0)) # add new node to process its children later + + return root