From 279e397ab7fc7b8c2fceeafe4778e49cccb45922 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:30:12 -0500 Subject: [PATCH 01/20] Update number-of-matching-subsequences.md --- python/number-of-matching-subsequences.md | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/python/number-of-matching-subsequences.md b/python/number-of-matching-subsequences.md index 43f8c1d..52734c0 100644 --- a/python/number-of-matching-subsequences.md +++ b/python/number-of-matching-subsequences.md @@ -140,3 +140,48 @@ def numMatchingSubseq(s, words): - **Time Complexity:** O(n + w * log n), where `n` is the length of `s` and `w` is the total number of words. - **Space Complexity:** O(n), for the character-to-indices mapping of `s`. +# Approach 4 +### **Intuition** + When we first look at this problem, we need to understand what makes a string a subsequence of another. A subsequence is formed by taking characters from the original string while keeping their relative order, but they don't need to be consecutive. For example, "ace" is a subsequence of "abcde" because we can find 'a', 'c', and 'e' in order, even though they're not adjacent. + +The natural first thought is to check each word against the main string by trying to find each character in order. We want to ensure that after finding each character, we only look forward in the main string for the next character, never backward, to maintain the proper sequence. + +### **Approach** +The solution implements an elegant way to check for subsequences using Python's built-in string methods. Here's how it works: + +1. The main function `numMatchingSubseq` takes two parameters: + - `s`: The source string to check against + - `words`: A list of words to check if they are subsequences +2. The nested helper function `isSubsequence` does the heavy lifting: + - It maintains a `current_position` pointer that starts at -1 + - For each character in the word we're checking: + - We use `s.find(char, current_position + 1)` to look for the next occurrence of the character + - The second parameter in `find()` tells it to start searching from `current_position + 1`, ensuring we only move forward + - If we can't find the character (`current_position == -1`), the word is not a subsequence + - If we successfully find all characters in order, the word is a subsequence +3. The main function then: + - Initializes a counter for matching words + - Checks each word using `isSubsequence` + - Increments the counter for each matching word + - Returns the final count +``` +class Solution: + def numMatchingSubseq(self, s: str, words: List[str]) -> int: + def isSubsequence(word: str) -> bool: + current_position = -1 + for char in word: + current_position = s.find(char, current_position + 1) + if current_position == -1: + return False + return True + + matching_words = 0 + for word in words: + if isSubsequence(word): + matching_words += 1 + return matching_words +``` +### **Complexity** + +- Time complexity: $$O(k * n)$$ Where k is the total length of all words combined, and n is the length of the source string s. For each character in each word, we might need to scan through a portion of the source string. +- Space complexity: $$O(1)$$ We only use a constant amount of extra space regardless of input size. The `current_position` variable and loop counters are the only extra space needed, and they don't grow with input size. From 7ff74ae64b5bad9f6182c8e7f697be2814f1cfab Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:36:20 -0500 Subject: [PATCH 02/20] Update number-of-matching-subsequences.md --- python/number-of-matching-subsequences.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/number-of-matching-subsequences.md b/python/number-of-matching-subsequences.md index 52734c0..fb0def6 100644 --- a/python/number-of-matching-subsequences.md +++ b/python/number-of-matching-subsequences.md @@ -4,7 +4,7 @@ 1. [Brute Force With Two Pointers](#approach-1-brute-force-with-two-pointers) 2. [Frequency with Maps](#approach-2-frequency-with-maps) 3. [Indexed Characters with Binary Search (Optimal)](#approach-3-indexed-characters-with-binary-search-optimal) - +4. [Iterative String Pattern Matching](#approach-4-iterative-string-pattern-matching) --- ## Approach 1: Brute Force With Two Pointers @@ -164,7 +164,7 @@ The solution implements an elegant way to check for subsequences using Python's - Checks each word using `isSubsequence` - Increments the counter for each matching word - Returns the final count -``` +```python class Solution: def numMatchingSubseq(self, s: str, words: List[str]) -> int: def isSubsequence(word: str) -> bool: From c4098284f4764a247e7bc4be77b3a56626d2effd Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:49:42 -0500 Subject: [PATCH 03/20] Update number-of-matching-subsequences.md --- python/number-of-matching-subsequences.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/number-of-matching-subsequences.md b/python/number-of-matching-subsequences.md index fb0def6..79ad233 100644 --- a/python/number-of-matching-subsequences.md +++ b/python/number-of-matching-subsequences.md @@ -140,7 +140,7 @@ def numMatchingSubseq(s, words): - **Time Complexity:** O(n + w * log n), where `n` is the length of `s` and `w` is the total number of words. - **Space Complexity:** O(n), for the character-to-indices mapping of `s`. -# Approach 4 +## Approach 4 ### **Intuition** When we first look at this problem, we need to understand what makes a string a subsequence of another. A subsequence is formed by taking characters from the original string while keeping their relative order, but they don't need to be consecutive. For example, "ace" is a subsequence of "abcde" because we can find 'a', 'c', and 'e' in order, even though they're not adjacent. From 12c1b2aa4029da886867f9c416221c7f1f8d9054 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:50:31 -0500 Subject: [PATCH 04/20] Update number-of-matching-subsequences.md --- python/number-of-matching-subsequences.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/number-of-matching-subsequences.md b/python/number-of-matching-subsequences.md index 79ad233..196e354 100644 --- a/python/number-of-matching-subsequences.md +++ b/python/number-of-matching-subsequences.md @@ -140,7 +140,7 @@ def numMatchingSubseq(s, words): - **Time Complexity:** O(n + w * log n), where `n` is the length of `s` and `w` is the total number of words. - **Space Complexity:** O(n), for the character-to-indices mapping of `s`. -## Approach 4 +## Approach 4: Iterative String Pattern Matching ### **Intuition** When we first look at this problem, we need to understand what makes a string a subsequence of another. A subsequence is formed by taking characters from the original string while keeping their relative order, but they don't need to be consecutive. For example, "ace" is a subsequence of "abcde" because we can find 'a', 'c', and 'e' in order, even though they're not adjacent. From 381933d1ef4a0cb34cd507c1e4b1e7d47663e336 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:13:16 -0500 Subject: [PATCH 05/20] Update longest-common-prefix.md --- python/longest-common-prefix.md | 44 ++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/python/longest-common-prefix.md b/python/longest-common-prefix.md index bff055c..ce8d9e8 100644 --- a/python/longest-common-prefix.md +++ b/python/longest-common-prefix.md @@ -5,7 +5,8 @@ 2. [Vertical Scanning](#vertical-scanning) 3. [Divie and Conquer](#divide-and-conquer) 4. [Binary Search](#binary-search) - +5. [Simplified Horizontal Scanning] (#simplified-horizontal-scanning) + ### Approach 1: Horizontal Scanning The idea behind horizontal scanning is to look at the prefix common to the first two strings and then continue with this "common prefix" and the next string, and so on. @@ -150,3 +151,44 @@ def longestCommonPrefix_binary_search(strs): - **Time Complexity**: O(S * log(minLen)), where S is the sum of all characters in all strings and minLen is the length of the shortest string in strs. - **Space Complexity**: O(1) as no additional space is used. +## Approach 5: Simplified Horizontal Scanning + +### Intuition + +Think about how you find a common prefix between words manually - you'd take one word and check if its beginning matches other words. As you find differences, you shorten what you're looking for. Just like comparing the start of words "flower" and "flight" - you'd quickly realize only "fl" matches at the beginning. + +### Approach + +We treat the first string as our initial guess for the common prefix. Then, we check this prefix against each remaining string, shortening it when needed until it matches at the start of the current string. If we ever run out of prefix to match, we know there's no common part at the start. + +In plain English: + +1. Take the first string as your starting prefix +2. For each remaining string: + - While this string doesn't start with our current prefix + - Keep removing the last letter of the prefix + - If we remove everything, there's no common prefix +3. Whatever prefix remains at the end is our answer + +```python +class Solution: + def longestCommonPrefix(self, strs: List[str]) -> str: + if not strs: + return "" + + prefix = strs[0] + + for string in range(1, len(strs)): + while strs[string].find(prefix) != 0: + prefix = prefix[:-1] + if not prefix: + return "" + + return prefix +``` + + +### Complexity + +- Time complexity: `O(S)` where S is the sum of all characters in all strings +- Space complexity: `O(1)` since we only modify the prefix string From f6edb8174bea8cf3330628cfa2607551cc8cae00 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:23:52 -0500 Subject: [PATCH 06/20] Update longest-common-prefix.md --- python/longest-common-prefix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/longest-common-prefix.md b/python/longest-common-prefix.md index ce8d9e8..90cc0e9 100644 --- a/python/longest-common-prefix.md +++ b/python/longest-common-prefix.md @@ -5,7 +5,7 @@ 2. [Vertical Scanning](#vertical-scanning) 3. [Divie and Conquer](#divide-and-conquer) 4. [Binary Search](#binary-search) -5. [Simplified Horizontal Scanning] (#simplified-horizontal-scanning) +5. [Simplified Horizontal Scanning](#simplified-horizontal-scanning) ### Approach 1: Horizontal Scanning From 04931683253377b6d89944c6ce0f902be0053401 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:24:34 -0500 Subject: [PATCH 07/20] Update longest-common-prefix.md --- python/longest-common-prefix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/longest-common-prefix.md b/python/longest-common-prefix.md index 90cc0e9..7847b57 100644 --- a/python/longest-common-prefix.md +++ b/python/longest-common-prefix.md @@ -3,7 +3,7 @@ ## Approaches: 1. [Horizontal Scanning](#horizontal-scanning) 2. [Vertical Scanning](#vertical-scanning) -3. [Divie and Conquer](#divide-and-conquer) +3. [Divide and Conquer](#divide-and-conquer) 4. [Binary Search](#binary-search) 5. [Simplified Horizontal Scanning](#simplified-horizontal-scanning) From 60149090fc7c0e683be09d5607f029be4bbdb005 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:13:43 -0500 Subject: [PATCH 08/20] Update guess-the-word.md --- python/guess-the-word.md | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/python/guess-the-word.md b/python/guess-the-word.md index fe61814..91c86e7 100644 --- a/python/guess-the-word.md +++ b/python/guess-the-word.md @@ -3,6 +3,7 @@ ## Approaches: - [Basic Approach: Random Selection](#random-selection) - [Optimal Approach: Minimax with Minimax-Solution](#minimax) +- [Simple Approach: Similarity-Based Elimination](#elimination) ## Basic Approach: Random Selection @@ -88,3 +89,59 @@ def optimal_guess(wordlist, guess): This approach ensures we minimize our chances of making an incorrect guess and quickly narrows down the list of potential secret words. +## Simple Approach: Similarity-Based Elimination + +### Intuition + +Imagine playing a game like Wordle where you need to guess a secret word, but instead of getting letter-by-letter feedback, you only get told how many letters are correct in their exact positions. The key insight is that if we make a guess and get told "2 letters are correct," then the secret word MUST share exactly 2 letters in the same positions with our guess – no more, no less. We can use this to narrow down possibilities. + +Another crucial insight is that in a large list of words, when we make a random guess, we're more likely to get 0 matches than any other number. So, we want to make guesses that are "typical" or "representative" of the word list, which will help us eliminate more words when we get 0 matches. + +### Approach + +Our strategy works like a game of 20 questions, but played very strategically: + +1. First, we analyze how common each letter is at each position across all words. For example, if many words have 'a' as their first letter, we consider 'a' "heavy" in the first position. We do this using a Counter for each position (0-5) in the words. +2. For each word, we calculate its "similarity score" to the rest of the list by adding up the weights of its letters in each position. A higher score means this word shares more common patterns with other words. +3. We sort words by this similarity score, putting the most "typical" words at the end of our list. We'll use these typical words as our guesses because: + - If they get 0 matches (common case), we can eliminate many similar words + - If they get some matches (rare case), we have a good reference point for finding the answer +4. For each guess: + - Take the most typical remaining word + - Get the number of matching positions from master.guess() + - Keep only words that have exactly that many matches with our guess + - Repeat until we find the answer + +```python +# """ +# This is Master's API interface. +# You should not implement it, or speculate about its implementation +# """ +# class Master: +# def guess(self, word: str) -> int: + +class Solution: + def findSecretWord(self, words: List[str], master: 'Master') -> None: + weights = [Counter(word[i] for word in words) for i in range(6)] + + words.sort(key=lambda word: sum(weights[i][c] for i, c in enumerate(word))) + + while words: + word = words.pop() + matches = master.guess(word) + + words = [ + other + for other in words + if matches == sum(w == o for w, o in zip(word, other)) + ] +``` + +### Complexity + +- Time complexity: `O(n²)` where n is the number of words + - For each guess (up to n times), we might need to check every remaining word + - Each word comparison is O(1) since words are fixed length (6) +- Space complexity: `O(1)` + - The weights list is constant size (6 positions) + - We modify the input words list in-place for filtering From 63e7d5a1f899a6782fdf36bf01e8990ab56bf65c Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:24:07 -0500 Subject: [PATCH 09/20] Update guess-the-word.md --- python/guess-the-word.md | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/python/guess-the-word.md b/python/guess-the-word.md index 91c86e7..2ee0a66 100644 --- a/python/guess-the-word.md +++ b/python/guess-the-word.md @@ -122,18 +122,32 @@ Our strategy works like a game of 20 questions, but played very strategically: class Solution: def findSecretWord(self, words: List[str], master: 'Master') -> None: - weights = [Counter(word[i] for word in words) for i in range(6)] - - words.sort(key=lambda word: sum(weights[i][c] for i, c in enumerate(word))) - + # Calculate letter frequencies for each position in the word + letter_frequencies = [Counter(word[position] for word in words) + for position in range(6)] + + # Sort words by their similarity score (sum of letter frequencies) + words.sort(key=lambda current_word: sum( + letter_frequencies[position][letter] + for position, letter in enumerate(current_word)) + ) + + # Continue guessing until we find the secret word or run out of words while words: - word = words.pop() - matches = master.guess(word) - + # Pick the most representative word from our remaining list + candidate_word = words.pop() + + # Get number of matching positions from the Master API + matching_positions = master.guess(candidate_word) + + # Filter word list to only keep words with exact same number of matches words = [ - other - for other in words - if matches == sum(w == o for w, o in zip(word, other)) + remaining_word + for remaining_word in words + if matching_positions == sum( + word_char == other_char + for word_char, other_char in zip(candidate_word, remaining_word) + ) ] ``` From a5e3d81297ea8f313e1b339dcd6af048f6cf8625 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:39:45 -0500 Subject: [PATCH 10/20] Update guess-the-word.md --- python/guess-the-word.md | 74 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/python/guess-the-word.md b/python/guess-the-word.md index 2ee0a66..8c50106 100644 --- a/python/guess-the-word.md +++ b/python/guess-the-word.md @@ -2,9 +2,9 @@ ## Approaches: - [Basic Approach: Random Selection](#random-selection) -- [Optimal Approach: Minimax with Minimax-Solution](#minimax) +- [Minimax with Minimax-Solution](#minimax) - [Simple Approach: Similarity-Based Elimination](#elimination) - +- [Optimal Approach: Random Sampling with Match-Based Filtering](#filtering) ## Basic Approach: Random Selection ### Intuition @@ -159,3 +159,73 @@ class Solution: - Space complexity: `O(1)` - The weights list is constant size (6 positions) - We modify the input words list in-place for filtering + +## Optimal Approach: Random Sampling with Match-Based Filtering + +### Intuition + +Think about playing a word guessing game where your friend only tells you the number of letters that match exactly with their secret word. If you guess "PLEASE" and they say "1 match", that's powerful information – you know their word must have exactly one letter in the same position as "PLEASE". Each guess lets you narrow down possibilities based on this matching pattern, similar to how each question in "20 Questions" helps eliminate options. + +### Approach + +The algorithm works through progressive elimination using random sampling and match information. Here's how it works in plain English: + +1. Start with a complete list of possible secret words. +2. Until we find the secret word (get 6 matches): + - Pick one word randomly from our remaining list of possibilities + - Ask how many letters match exactly with the secret word + - If it's not the secret word (less than 6 matches): + - Look at every remaining word in our list + - Keep only words that match our guess in exactly the same number of positions + - Replace our old list with this filtered list + - If it is the secret word (6 matches), we're done! + +For example, let's say we have words like "PLEASE", "PYTHON", "PALACE", and "PENCIL": + +- We randomly pick "PLEASE" and get told "1 match" +- We check each remaining word against "PLEASE": + - "PYTHON" has 1 match ('P'), so we keep it + - "PALACE" has 3 matches ('P', 'L', 'E'), so we remove it + - "PENCIL" has 1 match ('P'), so we keep it +- Our new list only contains "PYTHON" and "PENCIL" +- We continue this process until we find the word + +```python +class Solution: + def compare(self, word1: str, word2: str) -> int: + """Count matching characters at same positions between two words.""" + return sum(int(word1[i] == word2[i]) for i in range(6)) + + def findSecretWord(self, words: List[str], master: 'Master') -> None: + # Track how many positions match with secret word + matching_positions = 0 + filtered_words = [] + current_guess = "" + + while matching_positions < 6: + # Make a random guess from remaining words + current_guess = random.choice(words) + matching_positions = master.guess(current_guess) + + if matching_positions < 6: + # Keep only words with same number of matches + for word in words: + if self.compare(word, current_guess) == matching_positions: + filtered_words.append(word) + + # Update our word list with filtered results + words.clear() + words, filtered_words = filtered_words, words + + return current_guess +``` + +### Complexity + +- Time complexity: `O(n²)` where n is the number of words + - We might need up to n guesses in the worst case + - For each guess, we compare against up to n remaining words + - Each comparison takes constant time (6 characters) +- Space complexity: `O(n)` + - We need space for our filtered list of words + - This list never grows larger than our initial word list From 8154540c04379801ac86bdbde14dd994eb06d252 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:21:42 -0500 Subject: [PATCH 11/20] Update valid-palindrome.md --- python/valid-palindrome.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/python/valid-palindrome.md b/python/valid-palindrome.md index fdda437..dc65115 100644 --- a/python/valid-palindrome.md +++ b/python/valid-palindrome.md @@ -3,6 +3,7 @@ ## Approaches 1. [Two-Pointer Approach with String Builder](#approach-1) 2. [Optimized Two-Pointer Approach (Without Extra Space)](#approach-2) +3. [Single Pass String Cleaning](#approach-3) ## Approach 1: Two-Pointer Approach with String Builder @@ -94,3 +95,29 @@ def isPalindrome(s: str) -> bool: ### Space Complexity - O(1): No additional space is used besides a few variables. +## Single-Pass String Cleaning + +### Intuition + +When thinking about palindromes, we need to focus only on the actual letters and numbers in the string, ignoring spaces, punctuation, and case sensitivity. My first thought was that we need to "clean" the string before checking if it reads the same forwards and backwards. Think of how "A man, a plan, a canal: Panama" is a palindrome - we need to transform it to "amanaplanacanalpanama" before we can properly check. + +### Approach + +Let's break this down into a clear step-by-step process: + +1. First, we convert the entire string to lowercase using `s.lower()`. This ensures that cases don't affect our comparison - for example, "A" and "a" should be considered the same character. +2. Next, we filter out all non-alphanumeric characters using Python's built-in `filter()` function with `str.isalnum`. This removes spaces, punctuation, and any other special characters. The `isalnum()` method returns True only for letters and numbers. +3. We join the filtered characters back into a single string using `"".join()`. At this point, our string contains only lowercase letters and numbers. +4. Finally, we compare this cleaned string with its reverse. In Python, `s[::-1]` creates a reversed copy of the string using slice notation. If the string equals its reverse, it's a palindrome. + +```python +class Solution: + def isPalindrome(self, s: str) -> bool: + s = "".join(filter(str.isalnum, s.lower())) + return s == s[::-1] +``` + +### Complexity + +- Time complexity: $$O(n)$$ The solution needs to process each character in the input string once during the lowercase conversion, once during filtering, and once during the reverse comparison. While we perform multiple passes, this still results in linear time complexity. +- Space complexity: $$O(n)$$ We create a new string containing the filtered characters. In the worst case (when all characters are alphanumeric), this will be the same length as the input string. The reversed string also takes up additional space, though Python's slice operations are optimized to minimize memory usage. From bc1f2e2ee53bfee8eb379ba3952c972cd939b890 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sun, 12 Jan 2025 08:26:35 -0500 Subject: [PATCH 12/20] Update number-of-good-ways-to-split-a-string.md --- .../number-of-good-ways-to-split-a-string.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/python/number-of-good-ways-to-split-a-string.md b/python/number-of-good-ways-to-split-a-string.md index e9d979d..e5d8352 100644 --- a/python/number-of-good-ways-to-split-a-string.md +++ b/python/number-of-good-ways-to-split-a-string.md @@ -3,6 +3,7 @@ ## Approaches 1. [Brute Force Approach](#brute-force-approach) 2. [Optimized Prefix and Suffix Frequency Arrays](#optimized-prefix-and-suffix-frequency-arrays) +3. [Simplest Approach: Character Boundary Indexing Approach](#character-boundary-index) --- @@ -86,3 +87,69 @@ def numSplits(s: str) -> int: Each block of code in this solution highlights the approach we've used; from computing unique character counts using two traversals of the input array to using these precomputed values to make quick checks for valid string partitions. This represents an optimal balancing of the problem's time and space complexity constraints. +## Character Boundary Indexing Approach + +### Intuition + +The problem of finding good string splits can be reimagined as finding positions where the count of distinct characters on both sides is equal. Instead of checking every possible position, we can focus on where characters first appear and last appear in the string. These boundary positions give us crucial information about where valid splits might occur. + +### Approach + +To solve this problem, we follow these logical steps: + +First, we handle two special cases that don't require complex processing: + +1. If the string length is 1, return 0 since we can't split it. +2. If the string length is 2, return 1 since there's only one way to split it. + +For all other cases, we process the string in this way: + +1. We scan through the string and record two important pieces of information for each character: + - The first position where we see each character + - The last position where we see each character +2. These positions represent boundaries where characters enter (first occurrence) and exit (last occurrence) our potential partitions. +3. We collect all these boundary positions and sort them in ascending order. +4. The valid split points must occur between consecutive positions at the middle of this sorted list. Why? Because: + - The first half of positions represents where characters become available to the left partition + - The second half represents where characters become unavailable to the right partition + - For equal numbers of distinct characters, our split point must lie between these middle positions +5. We calculate the number of valid split points by finding the difference between the middle position and the position before it in our sorted list. + +### Code +```python +class Solution: + def numSplits(self, s: str) -> int: + length_of_string = len(s) + + if length_of_string == 1: + return 0 + + if length_of_string == 2: + return 1 + + first_occurrences = {} + last_occurrences = {} + + for position, character in enumerate(s): + if character not in first_occurrences: + first_occurrences[character] = position + last_occurrences[character] = position + + all_indices = list(first_occurrences.values()) + list(last_occurrences.values()) + all_indices.sort() + + middle_index = len(all_indices) // 2 + + return all_indices[middle_index] - all_indices[middle_index - 1] +``` + +### Complexity + +- Time complexity: **O(n log n)** + - We must scan the entire string once to find first and last occurrences + - The sorting of boundary positions takes O(k log k) where k is the number of unique characters + - Since k is bounded by the alphabet size (26 for lowercase letters), this effectively becomes O(n) +- Space complexity: **O(k)** + - We store two positions (first and last) for each unique character + - The space needed is proportional to the number of unique characters + - Since k is bounded by the alphabet size, this is effectively O(1) From e07e701c9c94b264a44514b6d9e24c2111d875d1 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:42:45 -0500 Subject: [PATCH 13/20] Update rotate-array.md --- python/rotate-array.md | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/python/rotate-array.md b/python/rotate-array.md index b68bdbc..5532802 100644 --- a/python/rotate-array.md +++ b/python/rotate-array.md @@ -4,6 +4,7 @@ - [Approach 1: Brute Force](#approach-1-brute-force) - [Approach 2: Using Extra Array](#approach-2-using-extra-array) - [Approach 3: Reverse Array](#approach-3-reverse-array) +- [Approach 4: Two-Point Slice Swap](#approach-4-slice-swap) ## Approach 1: Brute Force @@ -95,3 +96,45 @@ def rotate(nums, k): Each approach improves efficiency from the one before, with the third approach being optimal for both time and space. +## Approach 4: Two-Point Python Slice Swap + +### Intuition + +When rotating an array by k positions to the right, we're essentially splitting the array into two parts and swapping their positions. The last k elements move to the front, while the remaining elements shift to the end. + +### Approach + +1. First, we handle edge cases: + - If k is larger than array length, we only need to rotate by k % len(nums) + - If array has 0-1 elements or k = 0, no rotation needed +2. The main logic uses Python's slice assignment feature: + - `nums[-k:]` takes the last k elements + - `nums[:-k]` takes all elements except the last k + - We assign these slices to `nums[:k]` (first k positions) and `nums[k:]` (remaining positions) respectively + +For example, with nums = [1,2,3,4,5] and k = 2: + +- `nums[-k:]` is [4,5] +- `nums[:-k]` is [1,2,3] +- After assignment: [4,5,1,2,3] + +```python +class Solution: + def rotate(self, nums: List[int], k: int) -> None: + """ + Do not return anything, modify nums in-place instead. + """ + k = k % len(nums) + + if len(nums) <= 1 or k == 0: + return + + nums[:k], nums[k:] = nums[-k:], nums[:-k] +``` + +### Complexity + +- Time complexity: $$O(n)$$ + - Creating slices and copying elements requires traversing the array once +- Space complexity: $$O(1)$$ + - While Python's slice assignment creates temporary lists internally, the problem considers this as constant space since we're using language features rather than explicitly creating additional data structures From c1968e4cf5b29eb50b9da9497a826eb93fe9034f Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:28:42 -0500 Subject: [PATCH 14/20] Update find-all-anagrams-in-a-string.md --- python/find-all-anagrams-in-a-string.md | 62 ++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/python/find-all-anagrams-in-a-string.md b/python/find-all-anagrams-in-a-string.md index 58de3b1..fe7d65b 100644 --- a/python/find-all-anagrams-in-a-string.md +++ b/python/find-all-anagrams-in-a-string.md @@ -5,7 +5,7 @@ - [Approach 1: Brute-force Checking](#approach-1-brute-force-checking) - [Approach 2: Sorting and HashMap](#approach-2-sorting-and-hashmap) - [Approach 3: Efficient Sliding Window with HashMap](#approach-3-efficient-sliding-window-with-hashmap) - +- [Approach 4: Simplified Sliding Window with Counter](#approach-4-sliding-window-with-counter) --- ## Approach 1: Brute-force Checking @@ -129,3 +129,63 @@ findAnagrams("cbaebabacd", "abc") ### Space Complexity - O(1), as the space used by the Counter is constant relative to the fixed character set (assuming only lowercase letters). +## Approach 4: Sliding Window with Counter +### Intuition + +When looking for anagrams in a text, we need to compare character frequencies. Since we're looking for all possible positions, using a sliding window approach that maintains and updates character counts feels natural - this way we don't have to recount characters for each possible position. + +### Approach + +1. First validate the inputs - if either string is empty or the text is shorter than pattern, return empty list +2. Create a frequency counter for the pattern we're looking for +3. Create initial window of pattern length at start of text and count its characters +4. Compare first window with pattern - if they match, we found our first anagram +5. For rest of text, slide window one character at a time: + - Add new character on right to window count + - Remove leftmost character from window count + - If character count becomes zero, remove it completely + - Compare current window with pattern + - If frequencies match, add starting position to results +6. Return all positions where anagrams were found + +### Code +```python +from collections import Counter + +def find_anagrams(text: str, pattern: str) -> list: + """Find starting indices of pattern's anagrams in text.""" + if not text or not pattern or len(text) < len(pattern): + return [] + + result = [] + pattern_count = Counter(pattern) + window_count = Counter(text[:len(pattern)]) + + # Check first window + if window_count == pattern_count: + result.append(0) + + # Slide window: remove left char, add right char, check if anagram + for i in range(len(pattern), len(text)): + window_count[text[i]] += 1 + window_count[text[i - len(pattern)]] -= 1 + + # Clean up zero counts + if window_count[text[i - len(pattern)]] == 0: + del window_count[text[i - len(pattern)]] + + if window_count == pattern_count: + result.append(i - len(pattern) + 1) + + return result +``` + +### Complexity + +- Time complexity: $$O(n)$$ where n is length of input text + - We scan text once with constant time operations at each step + - Counter comparisons are O(k) where k is alphabet size (constant) +- Space complexity: $$O(k)$$ where k is size of alphabet + - We store two frequency counters + - Each counter has at most k entries (one per unique character) + - k is limited by alphabet size, so effectively O(1) From 69715dbf913ba6715a67242253f0eb9d9cae1df9 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:50:49 -0500 Subject: [PATCH 15/20] Update permutation-in-string.md --- python/permutation-in-string.md | 66 ++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/python/permutation-in-string.md b/python/permutation-in-string.md index eda74c5..c81854f 100644 --- a/python/permutation-in-string.md +++ b/python/permutation-in-string.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Approach 1: Generate All Permutations (Brute Force)](#approach-1-generate-all-permutations-brute-force) 2. [Approach 2: Sliding Window with Character Count Comparison](#approach-2-sliding-window-with-character-count-comparison) - +3. [Approach 3: Simplified Sliding Window with Counter](#approach-3-simplified-sliding-window-with-counter) --- ## Approach 1: Generate All Permutations (Brute Force) @@ -95,3 +95,67 @@ print(checkInclusion(s1, s2)) # Output: True - **Time Complexity**: \(O(l + m)\), where \(l\) is the length of `s1`, and \(m\) is the length of `s2`. Iterating once through `s2` and performing constant-time operations for each character. - **Space Complexity**: \(O(1)\), the space needed for the frequency arrays is constant. +## Approach 3: Simplified Sliding Window with Counter +### Intuition + +This problem asks to find if a string contains a permutation of another string. The key insight is that permutations have identical character frequencies, and we can use a sliding window to efficiently check each possible substring without recounting characters each time. + +### Approach + +1. First check if s1 is longer than s2 - if so, no permutation can exist +2. Create character frequency maps: + - Count characters in pattern string (s1) + - Count characters in first window of s2 +3. Check if first window matches pattern +4. Slide window through remaining text: + - Add new character on right + - Remove leftmost character + - Remove character from count if frequency becomes zero + - Compare window with pattern +5. Return true if any window matches, false if none match + +### Code +```python +from collections import Counter + +def check_inclusion(s1: str, s2: str) -> bool: + """ + Check if s2 contains a permutation of s1. + """ + # Edge case: s1 longer than s2 + if len(s1) > len(s2): + return False + + # Initialize frequency counters + pattern_count = Counter(s1) + window_count = Counter(s2[:len(s1)]) + + # Check first window + if pattern_count == window_count: + return True + + # Slide window through remaining string + for i in range(len(s1), len(s2)): + # Update window counts + window_count[s2[i]] += 1 + window_count[s2[i - len(s1)]] -= 1 + + # Remove zero counts + if window_count[s2[i - len(s1)]] == 0: + del window_count[s2[i - len(s1)]] + + # Check for permutation + if pattern_count == window_count: + return True + + return False +``` + +### Complexity + +- Time complexity: $$O(n)$$ where n is length of s2 + - Single pass through s2 + - Counter comparisons are O(1) since limited by alphabet size +- Space complexity: $$O(1)$$ + - Two Counter objects storing at most 26 characters each (lowercase English letters) + - Fixed size regardless of input length From 8de9beabc853a99173599fde36fd8ab30951d5b9 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sun, 19 Jan 2025 10:23:58 -0500 Subject: [PATCH 16/20] Update minimum-window-substring.md --- python/minimum-window-substring.md | 86 +++++++++++++++++------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/python/minimum-window-substring.md b/python/minimum-window-substring.md index a9cf341..3575ffc 100644 --- a/python/minimum-window-substring.md +++ b/python/minimum-window-substring.md @@ -67,45 +67,55 @@ The sliding window approach is a more optimal solution. The idea is to use two p ### Code ```python -from collections import Counter - -def minWindow(s: str, t: str) -> str: - if not t or not s: - return "" - - t_count = Counter(t) - current_count = {} - required = len(t_count) - formed = 0 - - left, right = 0, 0 - min_length = float('inf') - min_window = (0, 0) - - while right < len(s): - char = s[right] - current_count[char] = current_count.get(char, 0) + 1 - - if char in t_count and current_count[char] == t_count[char]: - formed += 1 - - while left <= right and formed == required: - if right - left + 1 < min_length: - min_length = right - left + 1 - min_window = (left, right) +class Solution: + def minWindow(self, s: str, t: str) -> str: + # Handle edge cases + if not s or not t: + return "" + + # Create variables for counting + t_count = Counter(t) + current_count = {} + required = len(t_count) + formed = 0 + + # Create variables for sliding window + left = right = 0 + minimum_length = float('inf') + minimum_window = (0,0) + + # Main loop for sliding window + while right < len(s): + # 1. Extract the char and update current_count + char = s[right] + current_count[char] = current_count.get(char, 0) + 1 + + # Update if valid is formed + if char in t_count and current_count[char] == t_count[char]: + formed += 1 - # Try to contract the window - char = s[left] - current_count[char] -= 1 - if char in t_count and current_count[char] < t_count[char]: - formed -= 1 - - left += 1 - - right += 1 - - l, r = min_window - return s[l:r+1] if min_length != float('inf') else "" + # 2. Reduce the size of window if still valid + while left <= right and formed == required: + # Update minimum_length and minimum_window + current_length = right - left + 1 + if current_length < minimum_length: + minimum_length = current_length + minimum_window = (left,right) + + # Extract the leftmost char from current_count + char = s[left] + current_count[char] -= 1 + + # Check if removing char invalidates window + if char in t_count and current_count[char] < t_count[char]: + formed -= 1 + + left += 1 + right += 1 + + # Extract the minimum window substring + start, end = minimum_window + return s[start:end + 1] if minimum_length != float('inf') else "" ``` ### Complexity From 3c8a691b3cfd1d43a5e0f41978357c6ad9b22d9b Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:28:56 -0500 Subject: [PATCH 17/20] Update rotate-list.md --- python/rotate-list.md | 78 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/python/rotate-list.md b/python/rotate-list.md index 4d0ec7e..35f768b 100644 --- a/python/rotate-list.md +++ b/python/rotate-list.md @@ -3,7 +3,7 @@ ## Approaches - [Approach 1: Simulation](#approach-1-simulation) - [Approach 2: Making the List Circular](#approach-2-making-the-list-circular) - +- [Approach 3: Two Pointer with Dummy Node Rotation](#approach-3-two-pointer-with-dummy) ## Approach 1: Simulation **Intuition**: @@ -107,3 +107,79 @@ def rotateRight(head: ListNode, k: int) -> ListNode: **Time Complexity**: O(N), where N is the length of the list. **Space Complexity**: O(1), as we manipulate the list in-place without extra data structures. +## Two Pointer with Dummy Node Rotation Solution + +### Intuition + +When approaching a linked list rotation problem, we can visualize it as reconnecting the last k nodes to the beginning of the list while maintaining the relative order of elements. The dummy node pattern helps handle edge cases elegantly, while two pointers allow us to track the necessary positions for rotation. + +### Approach + +The solution employs a systematic strategy: + +1. First, we handle base cases for empty lists and single-node lists, as these require no rotation. +2. We then calculate the list size for two important reasons: + - To handle cases where k is larger than the list length + - To normalize k through modulo operation, avoiding unnecessary rotations +3. The main rotation mechanism uses two pointers: + - The first pointer advances to the end of the list + - The second pointer stops at the node before where we need to break the list + - We maintain a dummy node to simplify head modifications +4. For each rotation: + - We connect the last node (first pointer) to the original head + - Update the dummy's next pointer to the new head + - Set the new end node's next pointer to null + - Reset pointers for the next rotation + +```python +def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]: + # Base cases + if not head or not head.next: + return head + + # Calculate size and normalize k + dummy = ListNode(0, head) + current = dummy.next + size = 0 + while current: + current = current.next + size += 1 + + k %= size + + # Perform rotations + first, second = dummy.next, dummy + rotations = 0 + + while rotations < k: + # Position pointers + while first.next: + first = first.next + second = second.next + + # Execute rotation + first.next = dummy.next + dummy.next = first + second.next = None + + # Reset for next rotation + second = dummy + first = dummy.next + rotations += 1 + + return dummy.next +``` +## Complexity + +Time complexity: O(k × n) + +- We traverse the list once to calculate size: O(n) +- For each rotation, we traverse the list once: O(n) +- We perform k rotations (after normalization) +- Therefore, total complexity is O(n + k × n) = O(k × n) + +Space complexity: O(1) + +- We only use a constant amount of extra space regardless of input size +- The dummy node and pointers are the only additional space required +- No recursive calls or data structures that grow with input size From 82cb524019af865bb5b34d6bf4bb30b95753a158 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sat, 8 Feb 2025 09:30:52 -0500 Subject: [PATCH 18/20] Update median-of-two-sorted-arrays.md --- python/median-of-two-sorted-arrays.md | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/python/median-of-two-sorted-arrays.md b/python/median-of-two-sorted-arrays.md index b975347..75dd8b3 100644 --- a/python/median-of-two-sorted-arrays.md +++ b/python/median-of-two-sorted-arrays.md @@ -94,3 +94,48 @@ def findMedianSortedArrays(nums1, nums2): This optimal solution efficiently finds the median without merging, using the properties of arrays and partitions. +### Optimal Binary Search +```python +class Solution: + def find_median_sorted_arrays(self, nums1, nums2): + """ + Find the median of two sorted arrays using binary search. + + Args: + nums1 (List[int]): First sorted array + nums2 (List[int]): Second sorted array + + Returns: + float: Median value of the combined sorted arrays + """ + # Make nums1 the smaller array for efficiency + if len(nums1) > len(nums2): + return self.find_median_sorted_arrays(nums2, nums1) + + size1, size2 = len(nums1), len(nums2) + total_size = size1 + size2 + partition_size = (total_size + 1) // 2 + + left, right = 0, size1 + + while left <= right: + partition1 = (left + right) // 2 + partition2 = partition_size - partition1 + + # Get partition elements, using sentinel values for edge cases + left1 = nums1[partition1 - 1] if partition1 > 0 else float('-inf') + left2 = nums2[partition2 - 1] if partition2 > 0 else float('-inf') + right1 = nums1[partition1] if partition1 < size1 else float('inf') + right2 = nums2[partition2] if partition2 < size2 else float('inf') + + if left1 <= right2 and left2 <= right1: + # Found the correct partition + if total_size % 2: + return max(left1, left2) + return (max(left1, left2) + min(right1, right2)) / 2 + + if left1 > right2: + right = partition1 - 1 # Move partition1 leftward + else: + left = partition1 + 1 # Move partition1 rightward +``` From 7ae55d3913d14eb0147ef112d70ed2859b72b86b Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sat, 8 Feb 2025 10:28:27 -0500 Subject: [PATCH 19/20] Update rotate-image.md --- python/rotate-image.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/python/rotate-image.md b/python/rotate-image.md index 8d8a465..61eac60 100644 --- a/python/rotate-image.md +++ b/python/rotate-image.md @@ -62,30 +62,32 @@ Time Complexity: O(N^2), where N is the number of rows/columns in the matrix. Space Complexity: O(1), as the rotations are done in-place. ```python -def rotate(matrix): - n = len(matrix) +```python +def rotate_matrix_90_degrees(matrix: list) -> None: + matrix_size = len(matrix) - # Rotate layer by layer - for layer in range(n // 2): - first = layer - last = n - 1 - layer - for i in range(first, last): - offset = i - first + # Rotate layer by layer from outer to inner + for current_layer in range(matrix_size // 2): + layer_start = current_layer + layer_end = matrix_size - 1 - current_layer + + for element_idx in range(layer_start, layer_end): + position_offset = element_idx - layer_start - # Save the top element - top = matrix[first][i] + # Store the top element before overwriting + temp_top = matrix[layer_start][element_idx] - # Left -> Top - matrix[first][i] = matrix[last - offset][first] + # Move left element to top + matrix[layer_start][element_idx] = matrix[layer_end - position_offset][layer_start] - # Bottom -> Left - matrix[last - offset][first] = matrix[last][last - offset] + # Move bottom element to left + matrix[layer_end - position_offset][layer_start] = matrix[layer_end][layer_end - position_offset] - # Right -> Bottom - matrix[last][last - offset] = matrix[i][last] + # Move right element to bottom + matrix[layer_end][layer_end - position_offset] = matrix[element_idx][layer_end] - # Top -> Right - matrix[i][last] = top + # Move stored top element to right + matrix[element_idx][layer_end] = temp_top # Example usage: # matrix = [ From f46ff0aa35a113faf03ce978769071b04a79d156 Mon Sep 17 00:00:00 2001 From: Ian Wilson <67807910+ianwilson97@users.noreply.github.com> Date: Sat, 8 Feb 2025 10:29:01 -0500 Subject: [PATCH 20/20] Update rotate-image.md --- python/rotate-image.md | 1 - 1 file changed, 1 deletion(-) diff --git a/python/rotate-image.md b/python/rotate-image.md index 61eac60..4f4600a 100644 --- a/python/rotate-image.md +++ b/python/rotate-image.md @@ -61,7 +61,6 @@ This approach involves rotating the matrix layer by layer or cycle by cycle, per Time Complexity: O(N^2), where N is the number of rows/columns in the matrix. Space Complexity: O(1), as the rotations are done in-place. -```python ```python def rotate_matrix_90_degrees(matrix: list) -> None: matrix_size = len(matrix)