I've been tasked to solve this exercise lately, and this is what I wrote:
object Main {
def isSubstringPalindrome(inp: String)(start: Int, end: Int): Boolean =
(start.compareTo(end), inp(start)==inp(end)) match {
case (0, _) => true
case (1, _) => true
case (-1, true) => isSubstringPalindrome(inp)(start+1, end-1)
case _ => false
}
def vecToPair[T](vec: IndexedSeq[T]): (T, T) = vec match {
case Vector(a, b) => (a, b)
}
def isContained(slice1: (Int, Int))(slice2: (Int, Int)): Boolean = (slice1, slice2) match{
case ((x, y), (w, z)) => x > w && y < z
}
def palindromes(inp: String): Seq[(Int, Int)] = {
val isPalindrome: ((Int, Int)) => Boolean = (isSubstringPalindrome(inp)_).tupled
val palindromeIndexes = inp.zipWithIndex
.groupBy(_._1).values // group by char
.map(_.map(_._2)).filter(_.length > 1) // retain sequence of indexes
.flatMap(_.combinations(2).map(vecToPair))
.filter(isPalindrome).toSeq
.sortBy{case (start, end)=> -(end+1-start)}
palindromeIndexes.filter(slice => ! palindromeIndexes.exists(isContained(slice)_))
}
def main(args: Array[String]) = {
val inp = if (args.isEmpty) "sqrrqabccbatudefggfedvwhijkllkjihxymnnmzpop" else args(0)
palindromes(inp).take(3)
.map{case (start, end) => (inp.slice(start, end+1), start, end+1-start)}.foreach{
case (str, start, length) => println(s"Text: $str, Index: $start, Length: $length")
}
}
}
I tried to care about conciseness and good functional style, tests/TDD wasn't supposedly a focus for this, and my algorithm should already be better than a naive \$ O \left( n^3 \right) \$ (the worst case of the string composed of the same letter all over is still pretty bad, though). The partially applied isSubstringPalindrome
also avoids to wastefully reallocate substrings, and only needs the indexes to work upon.
If performance is a concern, isSubstringPalindrome
can be trivially memoized, which should trade off memory for faster execution on very large strings.
With this taken care of, I've been told:
the solution should be correct, reliable, maintainable, reusable, portable and efficient.
My solution is obviously not ideal: I could've added some scaladoc to explain the code. Another trivial improvement could've been to factorize out end+1-start
into a length
function... that way I could've written something like .sortBy(length).reverse
or .sortBy(-length(_))
.
Also, it would've been nicer if .compareTo
in Scala would return a data Ordering = LT | EQ | GT
just like in Haskell: that way isSubstringPalindrome
would be more explicitly/guarantee in the fact that it's handling all cases: (string not fully checked for palindromeness yet: start < end
, odd length palindrome: start==end
, even length palindrome: end < start
)... but this is not really a shortcoming of my code.
Anyhow, given the algorithm, I think that the code itself is somewhat readable and clean. Functions of only 2/3 statements each, no temporal coupling, etc.
That said, it wasn't appreciated. My suspicion is that they expected some classes to be defined, and gain some insight on my data modeling thought process. If that's so, I'm a bit disappointed, because a test that asks to solve a principally algorithmic problem wouldn't/shouldn't naturally lead to define/create your own data types (anything already available in the stdlib should be enough)... something more real world might've been more appropriate.
I've been given this task by people using Scala professionally. Conversely, I never used Scala at work, so I'd expect that my code might be unidiomatic.
Do you have any other idea on how to improve, even by rewriting it completely with a different algorithm? One of the goals was efficiency, but performance shouldn't be a concern. What I'd like to see this code improved upon is in its readability, cleanliness and general good design.
aab
and(1,8), (0,4)
forabcba_bcb
\$\endgroup\$findAllPalindromes
is \$ O \left(findLongestPalindromeAtPos\right) \times O \left( n \right) \$ (andfindLongestPalindromeAtPost
has worst case \$ O \left( n \right) \$, but it's actually going to be way less expensive in non-pathological cases) ... brute force would be generating all the possible substrings, and checking for their palindromicity. You're instead generating only the palindrome substrings pivoted around some characters. Calling it brute force would be a bit of stretch imho. \$\endgroup\$