Facebook Pixel

2390. Removing Stars From a String

MediumStackStringSimulation
Leetcode Link

Problem Description

You are given a string s that contains regular characters and asterisks (*).

The task is to process the string by removing characters according to a specific rule involving the asterisks. For each asterisk in the string, you need to:

  1. Find the closest non-star character to its left
  2. Remove both that character and the asterisk itself

You need to apply this operation for every asterisk in the string and return the final result after all removals are complete.

For example, if you have the string "abc*de*":

  • The first * removes the c (closest non-star to its left), leaving "abde*"
  • The second * removes the e (closest non-star to its left), leaving "abd"

The problem guarantees that:

  • There will always be enough non-star characters to the left of each asterisk (the operation is always possible)
  • The final result is unique regardless of the order in which you process the asterisks

The solution uses a stack-based approach: traverse the string from left to right, push non-star characters onto a stack, and pop from the stack whenever you encounter an asterisk. This naturally handles the "closest to the left" requirement since the stack's top element is always the most recently added (rightmost) non-star character.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we see this problem, we need to think about how to efficiently track and remove the "closest non-star character to the left" of each asterisk.

The key insight is recognizing that "closest to the left" follows a Last-In-First-Out (LIFO) pattern. As we traverse the string from left to right, the most recently seen non-star character is exactly the one that should be removed when we encounter an asterisk.

This naturally leads us to think of using a stack data structure. Why? Because:

  1. As we move through the string, we're building up a collection of non-star characters that might potentially be removed later
  2. When we see an asterisk, we need to remove the most recent addition to this collection
  3. A stack perfectly models this behavior - we push elements as we see them, and pop the most recent one when needed

Let's trace through an example to see why this works. Consider "leet**cod*e":

  • We push 'l', 'e', 'e', 't' onto the stack: ['l', 'e', 'e', 't']
  • First *: pop 't', stack becomes ['l', 'e', 'e']
  • Second *: pop 'e', stack becomes ['l', 'e']
  • Push 'c', 'o', 'd': stack becomes ['l', 'e', 'c', 'o', 'd']
  • Third *: pop 'd', stack becomes ['l', 'e', 'c', 'o']
  • Push 'e': final stack is ['l', 'e', 'c', 'o', 'e']

The beauty of this approach is that we don't need to look backward or keep track of indices - the stack naturally maintains the order and availability of removable characters. Each operation is O(1), making the overall solution O(n) where n is the length of the string.

Learn more about Stack patterns.

Solution Approach

The implementation uses a stack simulation approach to solve this problem efficiently.

Algorithm Steps:

  1. Initialize an empty list ans that will serve as our stack to store characters.

  2. Traverse the string character by character:

    • If the current character c is an asterisk (*):
      • Pop the top element from the stack using ans.pop()
      • This removes the closest non-star character to the left
    • If the current character is not an asterisk:
      • Push it onto the stack using ans.append(c)
      • This character might be removed later if we encounter an asterisk
  3. Build the result string by joining all remaining characters in the stack using ''.join(ans)

Implementation Details:

class Solution:
    def removeStars(self, s: str) -> str:
        ans = []  # [Stack](/problems/stack_intro) to store characters
        for c in s:
            if c == '*':
                ans.pop()  # Remove the most recent non-star character
            else:
                ans.append(c)  # Add non-star character to stack
        return ''.join(ans)  # Convert stack to final string

Why This Works:

  • The stack (implemented as a list) maintains the order of non-star characters from left to right
  • When we encounter an asterisk, the top of the stack is guaranteed to be the closest non-star character to its left
  • The pop() operation removes this character in O(1) time
  • After processing all characters, the stack contains exactly the characters that weren't removed

Time and Space Complexity:

  • Time Complexity: O(n) where n is the length of the input string, as we traverse the string once
  • Space Complexity: O(n) for the stack in the worst case when there are no asterisks

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with the string "ab*c*d" to illustrate the solution approach.

Initial Setup:

  • Input string: "ab*c*d"
  • Stack (ans): [] (empty)

Step-by-step Processing:

Step 1: Process character 'a'

  • Current character is not an asterisk
  • Push 'a' onto the stack
  • Stack: ['a']

Step 2: Process character 'b'

  • Current character is not an asterisk
  • Push 'b' onto the stack
  • Stack: ['a', 'b']

Step 3: Process character '*'

  • Current character is an asterisk
  • Pop the top element from stack (removes 'b')
  • Stack: ['a']

Step 4: Process character 'c'

  • Current character is not an asterisk
  • Push 'c' onto the stack
  • Stack: ['a', 'c']

Step 5: Process character '*'

  • Current character is an asterisk
  • Pop the top element from stack (removes 'c')
  • Stack: ['a']

Step 6: Process character 'd'

  • Current character is not an asterisk
  • Push 'd' onto the stack
  • Stack: ['a', 'd']

Final Result:

  • Join the remaining characters in the stack: ''.join(['a', 'd']) = "ad"
  • Output: "ad"

This example demonstrates how the stack naturally handles the "closest to the left" requirement. When we encounter the first asterisk at position 2, it removes 'b' (the most recent non-star character). When we encounter the second asterisk at position 4, it removes 'c' (which had become the most recent non-star character after 'b' was removed).

Solution Implementation

1class Solution:
2    def removeStars(self, s: str) -> str:
3        """
4        Remove stars and their preceding characters from a string.
5      
6        Each '*' removes the closest non-star character to its left.
7      
8        Args:
9            s: Input string containing letters and stars
10          
11        Returns:
12            String after removing all stars and their corresponding characters
13        """
14        # Use a stack to track characters
15        result_stack = []
16      
17        # Process each character in the string
18        for char in s:
19            if char == '*':
20                # Remove the most recent non-star character
21                result_stack.pop()
22            else:
23                # Add non-star character to the stack
24                result_stack.append(char)
25      
26        # Convert the stack to a string and return
27        return ''.join(result_stack)
28
1class Solution {
2    public String removeStars(String s) {
3        // Use StringBuilder to efficiently build the result string
4        StringBuilder result = new StringBuilder();
5      
6        // Iterate through each character in the input string
7        for (int i = 0; i < s.length(); i++) {
8            // Check if current character is a star
9            if (s.charAt(i) == '*') {
10                // Remove the last character from result (the star removes the previous character)
11                result.deleteCharAt(result.length() - 1);
12            } else {
13                // Append non-star character to the result
14                result.append(s.charAt(i));
15            }
16        }
17      
18        // Convert StringBuilder to String and return
19        return result.toString();
20    }
21}
22
1class Solution {
2public:
3    string removeStars(string s) {
4        // Initialize result string to store characters after star removal
5        string result;
6      
7        // Iterate through each character in the input string
8        for (char current_char : s) {
9            if (current_char == '*') {
10                // If current character is a star, remove the last character from result
11                result.pop_back();
12            } else {
13                // If current character is not a star, append it to result
14                result.push_back(current_char);
15            }
16        }
17      
18        // Return the final string after all star operations
19        return result;
20    }
21};
22
1/**
2 * Removes stars and their preceding characters from a string
3 * Each star removes the closest non-star character to its left
4 * @param s - Input string containing letters and stars
5 * @returns Modified string after processing all stars
6 */
7function removeStars(s: string): string {
8    // Stack to store characters that haven't been removed
9    const resultStack: string[] = [];
10  
11    // Iterate through each character in the input string
12    for (const character of s) {
13        if (character === '*') {
14            // Remove the most recent character when encountering a star
15            resultStack.pop();
16        } else {
17            // Add non-star characters to the stack
18            resultStack.push(character);
19        }
20    }
21  
22    // Join all remaining characters to form the final string
23    return resultStack.join('');
24}
25

Time and Space Complexity

The time complexity is O(n), where n is the length of the string s. This is because we iterate through each character in the string exactly once, and both the append() and pop() operations on a list in Python have O(1) amortized time complexity.

The space complexity is O(n). While the reference answer mentions O(1) when ignoring the space consumption of the answer string, the actual space complexity must account for the ans list which stores the result. In the worst case where the input string contains no stars, the ans list will store all n characters from the input string, requiring O(n) space.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Edge Cases with Multiple Consecutive Asterisks

A common mistake is assuming the algorithm might fail when multiple asterisks appear consecutively. Developers might worry that consecutive asterisks could cause issues or require special handling.

Why it's not actually a problem: The problem statement guarantees there will always be enough non-star characters to the left of each asterisk. This means if you have n asterisks in a row, there must be at least n non-star characters available to remove before encountering those asterisks.

Example: For string "abcd***e":

  • First * removes d → stack: ['a', 'b', 'c']
  • Second * removes c → stack: ['a', 'b']
  • Third * removes b → stack: ['a']
  • Add e → final result: "ae"

2. Attempting to Process Asterisks in Reverse or Using Complex Logic

Some developers might overthink the problem and try to:

  • Process asterisks from right to left
  • Count asterisks first and then remove characters
  • Use additional data structures or complex indexing

Why the simple approach works: The stack-based approach naturally handles the "closest to the left" requirement. Processing left-to-right with immediate removal is both simpler and more efficient.

3. Using String Concatenation Instead of a List

A performance pitfall is building the result using string concatenation:

# Inefficient approach
result = ""
for c in s:
    if c == '*':
        result = result[:-1]  # O(n) operation each time
    else:
        result += c  # Creates new string object

Problem: Strings are immutable in Python, so each concatenation or slicing operation creates a new string object, leading to O(n²) time complexity.

Solution: Always use a list (as shown in the correct implementation) and join at the end. List operations like append() and pop() are O(1), making the overall algorithm O(n).

4. Forgetting to Handle Empty Stack Scenario (Though Guaranteed Not to Happen)

While the problem guarantees sufficient characters, in real-world scenarios or variations of this problem, you might need to handle the case where there's an asterisk but the stack is empty:

# Defensive programming approach
for char in s:
    if char == '*':
        if result_stack:  # Check if stack is not empty
            result_stack.pop()
    else:
        result_stack.append(char)

However, for this specific problem, this check is unnecessary due to the guarantee, and adding it might confuse the solution's clarity.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More