Problem overview
The function countPalindromes takes a String named input and returns the number of its contiguous substrings that are palindromes. Substrings with different start or end indices are different, even if they spell the same text.
For "abbcbc" the answer is 9: the six single letters, plus "bb", "bcb" and "cbc".
Approach: grow palindromes from their centers
Checking every substring from scratch costs O(n³): about n² substrings, each checked in linear time. The faster idea uses a property of palindromes: a palindrome is a smaller palindrome with the same letter added on both sides. Start from a center, and if the letters on both sides match, you have found a palindrome. Widen by one on each side and test again. The first mismatch ends the search, because any wider range around the same center would contain that mismatched pair.
Every palindrome has exactly one center:
- an odd-length palindrome has a single middle letter, so the center is an index
iand the expansion starts at(i, i); - an even-length palindrome has two middle letters, so the center sits between
iandi + 1, and the expansion starts at(i, i + 1).
Trying both kinds of center at every index finds each palindrome exactly once. A string of length n has 2n - 1 centers, and each expansion costs at most O(n), which gives O(n²) overall.
On "abbcbc", the center at index 3 (c) finds "c" and then "bcb", and then stops, because the next pair, b and c, differs. It counts 2. The even center between indices 1 and 2 ("bb") finds "bb", and then stops at a versus c. The even center between indices 0 and 1 finds nothing, because a differs from b.
Over the whole example, the calls for each i give these counts:
| i | Letter | Odd center (i, i) | Even center (i, i + 1) |
|---|---|---|---|
| 0 | a | 1 | 0 |
| 1 | b | 1 | 1 |
| 2 | b | 1 | 0 |
| 3 | c | 2 | 0 |
| 4 | b | 2 | 0 |
| 5 | c | 1 | 0 |
The counts add up to 1 + 2 + 1 + 2 + 2 + 1 = 9. The odd center at index 4 finds "b" and "cbc", which uses the letters at indices 3 to 5.
Step by step
- Declare
resultas avarset to 0 to hold the total. - Loop
iover0 until input.length. Each index is a possible center. - Add the odd centers. Call
countPalindromesWithCenter(input, i, i)and add what it returns toresult. - Add the even centers. Call it again with
(i, i + 1). If those two letters differ, ori + 1is past the end, the helper returns 0. - Return
resultafter the loop. - Declare the helper above
countPalindromes. It takes the string and the starting indicesleftandright, and returns anInt: how many palindromes surround that center. - Make mutable copies. Parameters can't be reassigned, so
landrarevarcopies, and a secondvar,result, starts at 0. - Write the loop condition:
lis at least 0,ris less thaninput.length, andinput[l] == input[r]. Each pass through the loop means one more palindrome. - Inside the loop, add 1 to
result, then widen withl--andr++. - Return
resultafter the loop.
The solution in Kotlin
class Solution {
fun countPalindromesWithCenter(input: String, left: Int, right: Int): Int {
var l = left
var r = right
var result = 0
while (l >= 0 && r < input.length && input[l] == input[r]) {
result++
l--
r++
}
return result
}
fun countPalindromes(input: String): Int {
var result = 0
for (i in 0 until input.length) {
result += countPalindromesWithCenter(input, i, i)
result += countPalindromesWithCenter(input, i, i + 1)
}
return result
}
}
Kotlin details worth knowing:
- The order of the conditions matters.
&&stops at the first condition that is false. The index checksl >= 0andr < input.lengthcome beforeinput[l] == input[r], so the string is never indexed outside its bounds, which would throw aStringIndexOutOfBoundsException. ==compares contents. On twoCharvalues it tests whether they are the same character.- Two
resultvariables. Each function has its own localresult. They don't interfere, because a local variable exists only inside the function that declares it. +=with a function call.result += countPalindromesWithCenter(...)evaluates the call first and adds the returned count.- Parameters are read-only. That's why the helper copies
leftandrightbefore moving them.
The expansion loop is the usual two-pointer palindrome check run in reverse. Instead of starting at the ends and moving in, it starts at the middle and moves out, and it counts every step that succeeds. Because a successful step at width w implies success at every smaller width, no range is ever tested twice.
Complexity
Time: O(n²). There are 2n - 1 centers, and each expansion moves outwards at most about n / 2 steps. The worst case is a string like "aaaa…", where almost every expansion reaches the edge. For a typical string with few repeats, most expansions end after one or two steps.
Space: O(1). Only a few integers are used.
Edge cases and common mistakes
- Empty string. The loop doesn't run and the result is 0.
- One letter.
"a"gives 1. The odd center counts it, and the even center at(0, 1)fails ther < input.lengthtest. - All the same letter.
"aaa"gives 6, the largest possible count for length 3. - All different letters.
"abc"gives 3: just the single letters. - Forgetting the even centers. Without the
(i, i + 1)call,"bb"is missed. - Reordering the loop condition. Putting
input[l] == input[r]first makes the code throw at the edges, for example withl = -1.
Testing
"abbcbc"gives9."aaa"gives6."abc"gives3."a"gives1.""gives0.