Why is this code 10x faster than mine?

87 points daishi55

My solution to Minimum Remove to Make Valid Parentheses(20ms):

pub fn min_remove_to_make_valid(s: String) -> String {
    let mut stack: Vec<usize> = vec![];
    let mut indices: HashSet<usize> = HashSet::new();

    for (i, c) in s.chars().enumerate() {
        if c == '(' {
            stack.push(i)
        } else if c == ')' && stack.pop().is_none() {
            indices.insert(i);
        }
    }

    indices.extend(stack);

    s.chars()
        .enumerate()
        .filter_map(|(i, c)| (!indices.contains(&i)).then_some(c))
        .collect()
}

2ms solution:

struct Letter {
    char: char,
    pos: usize,
}

pub fn min_remove_to_make_valid(s: String) -> String {
    let mut letters: Vec<char> = Vec::new();
    let mut reg: VecDeque<Letter> = VecDeque::new();

    for l in s.chars() {
        if l == '(' {
            letters.push('#');
            let letter: Letter = Letter {
                char: l,
                pos: letters.len() - 1,
            };
            reg.push_back(letter);
        } else if l == ')' {
            if !reg.is_empty() {
                let letter: Letter = reg.pop_back().unwrap();
                letters[letter.pos] = letter.char;
                letters.push(l);
            }
        } else {
            letters.push(l);
        }
    }

    let mut ans = String::new();

    for &char in letters.iter() {
        if char != '#' {
            ans.push(char);
        }
    }

    ans
}

I know I am doing some extra work building the string at the end - accessing the indices set for every character (I tried only checking if the character is ( or ), made no difference). But the 2ms solution is doing more work in the main loop - pushing something to a vector every loop iteration, and sometimes doing two or three accesses. Other than that, we both use two heap-allocated collections, and then build a new string at the end.

Any thoughts?