Problem overview
The function count_palindromes takes a String and returns the number of contiguous substrings that read the same in both directions. Substrings with different start or end positions are different, even when their letters are equal. For "abbcbc" the answer is 9: the six single letters, plus "bb", "bcb" and "cbc".
Approach: every palindrome has a center
Testing every substring costs O(n³): about n²/2 substrings, and a check that may walk across most of each one. This lesson removes the repeated work with one observation: a palindrome is determined by its center and its radius. If the characters on both sides of a center match, extending by one more step on each side gives another palindrome. If they differ, no larger palindrome with that center exists, so the search from that center can stop.
There are two kinds of centers:
- An odd-length palindrome has one middle character, so its center is an index
i, and the search starts withleft = right = i. - An even-length palindrome has two middle characters, so its center sits between
iandi + 1, and the search starts withleft = iandright = i + 1.
For each of the n positions, the code runs both searches. Each search counts one palindrome per successful expansion, which is why the count comes straight out of a loop with no extra bookkeeping. Every palindrome has exactly one center of exactly one kind, so none is counted twice.
On "abbcbc": the odd searches give one palindrome for each of the six letters, plus "bcb" at index 3 and "cbc" at index 4, so 8. The even searches find only "bb" between indices 1 and 2. In total 8 + 1 = 9.
Step by step
- Prepare.
bytesisinput.as_bytes(), a byte view for reading by index.nisinput.len(), andresultis a mutable counter,0i32. - Loop over centers with
for i in 0..n. - Odd centers. Call
Self::count_with_center(bytes, i as i32, i as i32)and add its return value toresult. The helper works withi32indices, soiis converted withas i32. - Even centers. Call the helper again with
i as i32andi as i32 + 1. If those two characters differ, it returns 0. - Return
resultafter the loop, with no semicolon. - Declare the helper above
count_palindromes. It takesbytes: &[u8]and twomuti32indices, and returns the count. The indices are signed becauseleftdrops to -1 when the expansion passes the start of the string, and ausizecannot hold that. - Start a counter named
resultat0i32inside the helper. - Write the loop condition. Keep going while
left >= 0,rightis belowbytes.len(), and the two bytes are equal. The checks run left to right and stop at the first false one, so the byte comparison, which indexes the slice, only runs when both indices are in range. - Count and expand. Inside the loop, add 1 to
result, then decrementleftand incrementright. - Return
resultfrom the helper.
The solution in Rust
impl Solution {
fn count_with_center(bytes: &[u8], mut left: i32, mut right: i32) -> i32 {
let mut result = 0i32;
while left >= 0
&& (right as usize) < bytes.len()
&& bytes[left as usize] == bytes[right as usize]
{
result += 1;
left -= 1;
right += 1;
}
result
}
fn count_palindromes(input: String) -> i32 {
let bytes = input.as_bytes();
let n = input.len();
let mut result = 0i32;
for i in 0..n {
result += Self::count_with_center(bytes, i as i32, i as i32);
result += Self::count_with_center(bytes, i as i32, i as i32 + 1);
}
result
}
}
Rust details worth knowing:
- Why the order of the conditions matters. Casting a negative
i32withas usizedoes not fail:-1i32 as usizeis 18446744073709551615. The checkleft >= 0comes first so that a negativeleftends the loop before it is ever cast or used to index. - Parentheses around
right as usize. Without them, the compiler readsas usize < ...as the start of a generic type, likeusize<...>, and reports a syntax error. Wrapping the cast in parentheses avoids that. i as i32 + 1. The cast binds tighter than+, so this is(i as i32) + 1.- Bytes, not characters. The code compares bytes. For ASCII text that is the same as comparing characters. A two-byte letter like
égets treated as two separate bytes, so"éé"comes out as 6 instead of the 3 palindromes it has as text. - Tail expressions.
resultwithout a semicolon is what each function returns.
Complexity
Time: O(n²). There are 2n centers. An expansion can run up to about n/2 steps, as in a string of identical letters, and each step does constant work. In the typical case it stops much earlier.
Space: O(1). The byte view shares the string's memory, and only a few integers are stored. This is a real improvement over the O(n³) time of testing every substring, with the same space.
Edge cases and common mistakes
- Empty string. The loop over centers doesn't run, and the result is 0.
- Only odd centers. Forgetting the even call misses every palindrome with an even length.
"abba"would give 4 instead of 6. - The last index as an even center. For
i = n - 1,rightisn, which is out of range. The check(right as usize) < bytes.len()turns that into a return of 0, instead of a panic. - Using
usizeforleft. Decrementing ausizeat 0 underflows. That panics in a debug build and wraps in an optimized one, which would make the next iteration index far outside the slice. - Checking bounds after indexing. Putting the byte comparison first reads out of range, and Rust panics. The comparison must stay last.
- Repeated letters.
"aaa"returns 6, and"aaaa"returns 10, because every window is a palindrome and each one is counted by its own center.
Testing
"abbcbc"gives 9."aaa"gives 6."abc"gives 3."abba"gives 6.""gives 0.
