Make The String Great
EasyPrompt
Given a string s of lower and upper case English letters.
A good string is a string which doesn't have two adjacent characters s[i] and s[i + 1] where:
0 <= i <= s.length - 2s[i]is a lower-case letter ands[i + 1]is the same letter but in upper-case or vice-versa.
To make the string good, you can choose two adjacent characters that make the string bad and remove them. You can keep doing this until the string becomes good.
Return the string after making it good. The answer is guaranteed to be unique under the given constraints.
Notice that an empty string is also good.
Example 1:
Input: s = "leEeetcode"
Output: "leetcode"
Explanation: In the first step, either you choose i = 1 or i = 2, both will result "leEeetcode" to be reduced to "leetcode".Example 2:
Input: s = "abBAcC"
Output: ""
Explanation: We have many possible scenarios, and all lead to the same answer. For example:
"abBAcC" --> "aAcC" --> "cC" --> ""
"abBAcC" --> "abBA" --> "aA" --> ""Example 3:
Input: s = "s"
Output: "s"
Constraints:
1 <= s.length <= 100scontains only lower and upper case English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. We repeatedly scan the string, and whenever we find a "bad" adjacent pair of characters (e.g., 'a' and 'A'), we remove them. After each removal, we restart the scan from the beginning of the modified string to ensure that new adjacent pairs created by the removal are also handled. This process continues until a full scan of the string reveals no bad pairs.
Algorithm
- Convert the input string
sinto aStringBuilderto allow for efficient modifications. - Use a
whileloop that continues as long as a bad pair is found and removed in a pass. A boolean flag can track this. - In each pass, iterate through the
StringBuilderfrom the beginning to find the first adjacent bad pair. - A pair of characters at indices
iandi+1is considered "bad" if they are the same letter but with different cases. This can be checked by seeing if the absolute difference of their ASCII values is 32. - If a bad pair is found, remove the two characters from the
StringBuilder. - Set the flag to indicate a change was made and
breakthe inner loop to restart the scan from the beginning of the now-modified string. - If a full pass is completed with no removals, the string is "good," and the outer loop terminates.
- Return the final string from the
StringBuilder.
Walkthrough
In this method, we use a StringBuilder for mutable string operations. The core of the logic is a while loop that continues as long as we are making changes to the string. Inside this loop, we iterate through the string with a for loop to find an adjacent pair of characters where Math.abs(char1 - char2) == 32. Once such a pair is found, we delete it from the StringBuilder, set a flag indicating a change was made, and break the inner loop. The outer while loop then causes the scan to restart from the beginning. If the inner for loop completes without finding any bad pairs, the flag remains false, the while loop terminates, and the resulting "good" string is returned.
class Solution { public String makeGood(String s) { StringBuilder sb = new StringBuilder(s); boolean changed = true; while (changed) { changed = false; for (int i = 0; i < sb.length() - 1; i++) { char curr = sb.charAt(i); char next = sb.charAt(i + 1); if (Math.abs(curr - next) == 32) { sb.delete(i, i + 2); changed = true; // Restart scan from the beginning of the modified string break; } } } return sb.toString(); }}Complexity
Time
O(N^2), where N is the length of the string. In the worst-case scenario (e.g., a string like "abBAcC..."), we might remove only one pair per pass. Each pass involves iterating up to N characters, and the `delete` operation on a `StringBuilder` can take O(N) time. Since there can be up to N/2 pairs to remove, the total time complexity is O(N * N).
Space
O(N), where N is the length of the input string. This space is used to store the `StringBuilder`.
Trade-offs
Pros
It's a straightforward simulation of the process described in the problem statement.
The logic is easy to understand and implement.
Cons
The time complexity is quadratic,
O(N^2), which is inefficient for very large strings.Restarting the scan from the beginning after every single removal is redundant work, as changes are localized.
A more efficient and optimal approach uses a stack. The problem involves removing adjacent items, which suggests that the last valid character we've seen is the only one that matters for the next character. This Last-In-First-Out (LIFO) behavior is perfectly modeled by a stack. We can iterate through the string just once, using the stack to build the final "good" string. A StringBuilder can be used as a highly efficient stack for this purpose.
Algorithm
- Initialize an empty
StringBuilderwhich will act as a stack and also build our result string. - Iterate through each character
cof the input strings. - For each character
c, check if theStringBuilderis not empty and if its last character forms a bad pair withc(i.e.,Math.abs(lastChar - c) == 32). - If it's a bad pair, it means the current character
cand the last character in our result cancel each other out. So, we delete the last character from theStringBuilder. - Otherwise (if the
StringBuilderis empty or the characters don't form a bad pair), we append the current charactercto theStringBuilder. - After the loop finishes, the
StringBuildercontains the final "good" string. Return its string representation.
Walkthrough
We process the input string character by character in a single pass. We use a StringBuilder which acts like a character stack. For each character from the input, we compare it with the last character added to our StringBuilder (if any). If they form a bad pair, we pop the last character from the StringBuilder by deleting it. If they don't form a bad pair, or if the StringBuilder is empty, we push the current character onto our stack by appending it. This ensures that at any point, the StringBuilder only contains a "good" prefix. After iterating through the entire input string, the StringBuilder holds the final result.
class Solution { public String makeGood(String s) { StringBuilder resultBuilder = new StringBuilder(); for (char currentChar : s.toCharArray()) { int length = resultBuilder.length(); if (length > 0 && Math.abs(resultBuilder.charAt(length - 1) - currentChar) == 32) { // Found a bad pair, remove the last character from the result resultBuilder.deleteCharAt(length - 1); } else { // No bad pair, add the current character to the result resultBuilder.append(currentChar); } } return resultBuilder.toString(); }}Complexity
Time
O(N), where N is the length of the string. We iterate through the string's characters only once. Each character is appended to the `StringBuilder` at most once, and deleted at most once. Both `append` and `deleteCharAt` at the end of a `StringBuilder` are amortized O(1) operations.
Space
O(N), where N is the length of the string. In the worst case (a string with no bad pairs), the `StringBuilder` will grow to the size of the input string to store the result.
Trade-offs
Pros
Optimal time complexity of O(N) due to a single pass over the string.
Elegant and clean solution that correctly handles the cascading nature of removals.
More efficient than the brute-force approach for larger inputs.
Cons
Requires O(N) auxiliary space for the stack or
StringBuilder. However, this is generally unavoidable as a new string must be constructed and returned.
Solutions
Solution
class Solution {public String makeGood(String s) { StringBuilder sb = new StringBuilder(); for (char c : s.toCharArray()) { if (sb.length() == 0 || Math.abs(sb.charAt(sb.length() - 1) - c) != 32) { sb.append(c); } else { sb.deleteCharAt(sb.length() - 1); } } return sb.toString(); }}class Solution {public: string makeGood(string s) { string stk; for (char c : s) { if (stk.empty() || abs(stk.back() - c) != 32) { stk += c; } else { stk.pop_back(); } } return stk; }};class Solution: def makeGood(self, s: str) -> str: stk = [] for c in s: if not stk or abs(ord(stk[- 1]) - ord(c)) != 32: stk . append(c) else: stk . pop() return "" . join(stk)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.