diff --git a/best-time-to-buy-and-sell-stock/alphaorderly.py b/best-time-to-buy-and-sell-stock/alphaorderly.py new file mode 100644 index 0000000000..092340b657 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/alphaorderly.py @@ -0,0 +1,21 @@ +""" +Time Complexity: O(n) +Space Complexity: O(1) + +- Single Pass approach +- Use a variable to store the least price +- Use a variable to store the maximum profit +- Update the maximum profit whenever the current price is greater than the least price +""" +class Solution: + def maxProfit(self, prices: List[int]) -> int: + N = len(prices) + + least = prices[0] + ans = 0 + + for i in range(1, N): + ans = max(ans, prices[i] - least) + least = min(least, prices[i]) + + return ans diff --git a/encode-and-decode-strings/alphaorderly.py b/encode-and-decode-strings/alphaorderly.py new file mode 100644 index 0000000000..2511b25ff6 --- /dev/null +++ b/encode-and-decode-strings/alphaorderly.py @@ -0,0 +1,58 @@ +""" +Time Complexity: O(n) +Space Complexity: O(n) + +### Wrong Approach ( NEVER USE IT ) +- Uses eval() on potentially untrusted input, which is a major security risk. +- If s or strs can be controlled or modified externally, arbitrary code execution is possible. +- This is for illustration only; never use eval for decoding in production code. + +- Encodes by converting the list of strings to its string representation using str(). +- Decodes by evaluating that string with eval(), reconstructing the original list. +""" +class Codec: + def encode(self, strs: List[str]) -> str: + return str(strs) + + def decode(self, s: str) -> List[str]: + return eval(s) + +""" +Time Complexity: O(n) +Space Complexity: O(n) + +- Encodes the list of strings by prefixing each string with its length and a separator ("_"), then concatenating. +- Decodes by parsing each length, extracting the separated string accordingly. +- Does not use eval, so this approach is safe. +""" +class Codec: + def encode(self, strs: List[str]) -> str: + strs = [str(len(s)) + "_" + s for s in strs] + return "".join(strs) + + def decode(self, s: str) -> List[str]: + ans = [] + N = len(s) + index = 0 + size = 0 + + word_part = False + + while index < N: + ch = s[index] + if not word_part: + if ch == "_": + if size == 0: + ans.append("") + else: + word_part = True + else: + size = (size * 10) + int(ch) + index += 1 + else: + ans.append(s[index : index + size]) + index += size + size = 0 + word_part = False + + return ans diff --git a/group-anagrams/alphaorderly.py b/group-anagrams/alphaorderly.py new file mode 100644 index 0000000000..a09bf3cdec --- /dev/null +++ b/group-anagrams/alphaorderly.py @@ -0,0 +1,44 @@ +""" +Time Complexity: O(n * k log k) +Space Complexity: O(n * k) + +- Iterate through each word in the input list +- Use a dictionary to group anagrams by their sorted tuple of characters as the key +- For each word, sort its characters and use the resulting tuple as the dictionary key +- Append the word to the appropriate list in the dictionary +- Return a list of the grouped anagrams +""" +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + d = defaultdict(list) + + for word in strs: + d[tuple(sorted(word))].append(word) + + return list(d.values()) + +""" +Time Complexity: O(n * k) +Space Complexity: O(n * k) + +- Iterate through each word in the input list +- Use a dictionary to group anagrams; key is a tuple representing character counts +- For each word, build a character frequency tuple (instead of sorting) +- This tuple serves as a unique identifier for anagrams +- Append the word to the appropriate list in the dictionary +- Return a list of the grouped anagrams +""" +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + d = defaultdict(list) + + def hash(s: str) -> Tuple[int, ...]: + count = [0] * 26 + for ch in s: + count[ord(ch) - ord('a')] += 1 + return tuple(count) + + for word in strs: + d[hash(word)].append(word) + + return list(d.values()) diff --git a/implement-trie-prefix-tree/alphaorderly.py b/implement-trie-prefix-tree/alphaorderly.py new file mode 100644 index 0000000000..e77f70ea47 --- /dev/null +++ b/implement-trie-prefix-tree/alphaorderly.py @@ -0,0 +1,38 @@ +""" +Time Complexity: O(L), where L is the length of the input string (word or prefix) per operation. +Space Complexity: O(N), where N is the total number of characters inserted into the trie (all words inserted). + +- Each Trie node uses a dictionary to store children nodes for each character. +- The `end` flag indicates whether the current node represents the end of a complete word. +- The `insert` method adds a word to the trie by sequentially creating/following nodes for each character. +- The `search` method checks for the presence of a full word in the trie by traversing nodes for each character and checking the `end` flag at the last character. +- The `startsWith` method checks if there exists any word in the trie with the given prefix by traversing nodes for each character in the prefix. +""" +class Trie: + + def __init__(self): + self.children = dict() + self.end = False + + def insert(self, word: str) -> None: + node = self + + for ch in word: + if ch not in node.children: + node.children[ch] = Trie() + node = node.children[ch] + + node.end = True + + def search(self, word: str, isPrefix: bool = False) -> bool: + node = self + + for ch in word: + if ch not in node.children: + return False + node = node.children[ch] + + return True if isPrefix else node.end + + def startsWith(self, prefix: str) -> bool: + return self.search(word=prefix, isPrefix=True) diff --git a/word-break/alphaorderly.py b/word-break/alphaorderly.py new file mode 100644 index 0000000000..605ded02fc --- /dev/null +++ b/word-break/alphaorderly.py @@ -0,0 +1,152 @@ +""" +Top-down dynamic programming with memoization. + +Time Complexity: O(k * n * (n + m)), or O(k * n^2) when m <= n + - n = len(s) + - k = len(wordDict) + - m = the maximum word length + + There are at most n memoized states, and each state checks every word. + In this implementation, s[index:] also creates a suffix whose length is + O(n), while startswith may compare up to m characters. + +Space Complexity: O(n) + The memoization table and recursion stack each contain at most n entries. + +dp(index) returns whether the suffix beginning at index can be completely +segmented. Reaching len(s) means that all characters have been consumed. +For every dictionary word that matches the current suffix, the function +recursively checks the remaining suffix and stops at the first valid split. +""" +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + @lru_cache() + def dp(index: int) -> bool: + if index == len(s): + return True + + for word in wordDict: + if len(s) - index < len(word): + continue + + if s[index:].startswith(word) and dp(index + len(word)): + return True + + return False + + return dp(0) + +""" +Bottom-up dynamic programming over prefixes of s. + +Time Complexity: O(k * n * (n + m)), or O(k * n^2) when m <= n + - n = len(s) + - k = len(wordDict) + - m = the maximum word length + + Each of the n + 1 prefix boundaries checks every word. The expression + s[index - W:] creates a suffix of up to O(n) characters, and startswith + may compare up to m characters. + +Space Complexity: O(n) + The dp array stores one value for every prefix boundary. + +dp[index] indicates whether s[:index] can be segmented. dp[0] is true because +the empty prefix needs no words. A word of length W can end at index when the +preceding prefix, dp[index - W], is valid and the suffix beginning there starts +with that word. The final entry represents the entire string. +""" +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + S = len(s) + + dp = [0] * (S + 1) + dp[0] = 1 + + for index in range(S + 1): + for word in wordDict: + W = len(word) + if index < W or dp[index - W] == 0: + continue + if s[index - W :].startswith(word): + dp[index] = dp[index - W] + + return bool(dp[-1]) + +""" +Trie-assisted depth-first search over valid word boundaries. + +Time Complexity: O(L + n^2) + - n = len(s) + - L = the total number of characters in wordDict + + Building the trie takes O(L). Each reachable boundary is processed once, + but a trie lookup may scan the remaining suffix, giving O(n^2) total work + in the worst case. + +Space Complexity: O(L + n) + The trie uses O(L) space. The visited array, DFS stack, and a lookup's list + of matching end positions use O(n) additional space. + +startsWith(target, start) follows the trie from target[start:] and returns every +exclusive end index at which a dictionary word finishes. The DFS treats those +indices as the next segmentation boundaries. visited prevents the same boundary +from being added to the stack more than once. +""" +class Trie: + def __init__(self): + self.children = dict() + self.end = False + + def insert(self, target: int): + node = self + + for ch in target: + if ch not in node.children: + node.children[ch] = Trie() + node = node.children[ch] + + node.end = True + + def startsWith(self, target: str, start: int) -> List[int]: + ans = [] + node = self + + for i in range(start, len(target)): + ch = target[i] + + if ch not in node.children: + return ans + + node = node.children[ch] + if node.end: + ans.append(i + 1) + + return ans + + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + S = len(s) + t = Trie() + visited = [False] * (S + 1) + + for word in wordDict: + t.insert(word) + + stack = [0] + + while stack: + start = stack.pop() + + starts_with = t.startsWith(s, start) + + for start_index in starts_with: + if start_index == S: + return True + if visited[start_index]: + continue + visited[start_index] = True + stack.append(start_index) + + return False