以下是imperative
解决方案:
def longestCommonSubstring(a: String, b: String) : String = {
def loop(m: Map[(Int, Int), Int], bestIndices: List[Int], i: Int, j: Int) : String = {
if (i > a.length) {
b.substring(bestIndices(1) - m((bestIndices(0),bestIndices(1))), bestIndices(1))
} else if (i == 0 || j == 0) {
loop(m + ((i,j) -> 0), bestIndices, if(j == b.length) i + 1 else i, if(j == b.length) 0 else j + 1)
} else if (a(i-1) == b(j-1) && math.max(m((bestIndices(0),bestIndices(1))), m((i-1,j-1)) + 1) == (m((i-1,j-1)) + 1)) {
loop(
m + ((i,j) -> (m((i-1,j-1)) + 1)),
List(i, j),
if(j == b.length) i + 1 else i,
if(j == b.length) 0 else j + 1
)
} else {
loop(m + ((i,j) -> 0), bestIndices, if(j == b.length) i + 1 else i, if(j == b.length) 0 else j + 1)
}
}
loop(Map[(Int, Int), Int](), List(0, 0), 0, 0)
}
我正在寻找更紧凑的functional way
来找到最长公共子串。
答案 0 :(得分:5)
def getAllSubstrings(str: String): Set[String] = {
str.inits.flatMap(_.tails).toSet
}
def longestCommonSubstring(str1: String, str2: String): String = {
val str1Substrings = getAllSubstrings(str1)
val str2Substrings = getAllSubstrings(str2)
str1Substrings.intersect(str2Substrings).maxBy(_.length)
}
首先获取两个字符串集合中的所有可能的子字符串(取自here)(以删除重复项),然后将这些字符串相交并找到最长的公共子字符串。
答案 1 :(得分:4)
您拥有的代码已经具备功能,而不是那么复杂。与目前发布的其他解决方案相比,它的时间效率也略有提升。
我只是简化它,清理一下并修复错误:
def longestCommonSubstring(a: String, b: String) = {
def loop(bestLengths: Map[(Int, Int), Int], bestIndices: (Int, Int), i: Int, j: Int): String = {
if (i > a.length) {
val bestJ = bestIndices._2
b.substring(bestJ - bestLengths(bestIndices), bestJ)
} else {
val currentLength = if (a(i-1) == b(j-1)) bestLengths(i-1, j-1) + 1 else 0
loop(
bestLengths + ((i, j) -> currentLength),
if (currentLength > bestLengths(bestIndices)) (i, j) else bestIndices,
if (j == b.length) i + 1 else i,
if (j == b.length) 1 else j + 1)
}
}
loop(Map.empty[(Int, Int), Int].withDefaultValue(0), (0, 0), 1, 1)
}
答案 2 :(得分:2)
解决方案可能如下:
def substrings(a:String, len:Int): Stream[String] =
if(len==0)
Stream.empty
else
a.tails.toStream.takeWhile(_.size>=len).map(_.take(len)) #:: substrings(a, len-1)
def longestCommonSubstring(a:String, b:String) =
substrings(a, a.length).dropWhile(sub => !b.contanis(sub)).headOption
这里子串方法返回Stream,产生原始字符串的递减长度子串,例如“test”产生“test”,“tes”,“est”,“te”,“es”, ...
方法 longestCommonSubstring 获取从 a 生成的第一个子字符串,该字符串包含在字符串 b
中答案 3 :(得分:2)
更新:发布此答案后,并感谢@Kolmar的反馈,我发现Char
索引策略的速度明显更快(至少是一个数量级)。现在,我添加了additional answer(根据StackOverflow的政策),该方法涵盖了实质上更快的Scala功能样式解决方案。
我应该更加关注OP专门提供的实现。不幸的是,我使用面向效率低的String
进行比较而对所有其他答案分散了注意力,并为提供能够使用Stream
和LazyList
的那些高兴的人提供了自己的优化版本
除了OP的请求之外,我还具有一些其他要求,以组成一个解决方案,以找到两个String实例之间最长的公共子字符串(LCS)。
String
实例之间的 first LCS String
实例来最大程度地减少CPU工作量String
实例来最大程度地减少GC工作第一个目标是捕获常规搜索策略。该过程从left
String
实例开始,生成从最长的字符串(原始String
实例本身)到最短的(单个字符)有序的子字符串列表。例如,如果left
String
实例包含“ ABCDEF”,则应按以下顺序生成String
实例的结果列表:
[
ABCDEF,
ABCDE, BCDEF,
ABCD, BCDE, CDEF,
ABC, BCD, CDE, DEF,
AB,BC,CD,DE,EF,
A,B,C,D,E,F
]
接下来,将通过此left
子字符串实例列表开始迭代,一旦在left
{{1}内的任何索引处找到了特定的right
子字符串实例,就立即停止。 }实例。找到String
子字符串实例后,将返回它。否则,将返回没有找到匹配项的指示。
关于满足解决方案要求#1的“渴望”方法,有两点需要注意:
left
子字符串实例可以出现在left
right
实例中的多个索引处。这意味着使用String
从left
right
实例的开头搜索String
子字符串实例可能会导致与从{{ 1}} indexOf
实例使用right
。String
子字符串实例,该实例也出现在lastIndexOf
left
实例中。此实现忽略了这种可能性。 right
从2.13版开始被弃用。
String
Stream
def longestCommonSubstring(left: String, right: String): Option[String] =
if (left.nonEmpty && right.nonEmpty) {
def substrings(string: String): LazyList[String] = {
def recursive(size: Int = string.length): LazyList[String] = {
if (size > 0) {
def ofSameLength: LazyList[String] =
(0 to (string.length - size))
.iterator.to(LazyList)
.map(offset => string.substring(offset, offset + size))
ofSameLength #::: recursive(size - 1)
}
else
LazyList.empty
}
recursive()
}
val (shorter, longer) =
if (left.length <= right.length)
(left, right)
else
(right, left)
substrings(shorter).find(longer.contains)
}
else
None
作为输入),请使用def longestCommonSubstring(left: String, right: String): Option[String] =
if (left.nonEmpty && right.nonEmpty) {
def substrings(string: String): Stream[String] = {
def recursive(size: Int = string.length): Stream[String] = {
if (size > 0) {
def ofSameLength: Stream[String] =
(0 to (string.length - size))
.toStream
.map(offset => string.substring(offset, offset + size))
ofSameLength #::: recursive(size - 1)
}
else
Stream.empty
}
recursive()
}
val (shorter, longer) =
if (left.length <= right.length)
(left, right)
else
(right, left)
substrings(shorter).find(longer.contains)
}
else
None
作为函数的返回类型。String
实例中的较短者设置为Option[String]
,以减少实例化和与String
长于{{1 }} shorter
实例。String
子字符串实例的生成以相同的大小进行子批处理,并删除了重复项(通过longer
)。然后将每个子批处理添加到String
(或shorter
)中。这样可以确保仅将实际需要的distinct
子字符串实例的实例化提供给LazyList
函数。答案 4 :(得分:1)
我认为 for 理解的代码看起来非常清晰和实用。
def getSubstrings(s:String) =
for {
start <- 0 until s.size
end <- start to s.size
} yield s.substring(start, end)
def getLongest(one: String, two: String): Seq[String] =
getSubstrings(one).intersect(getSubstrings(two))
.groupBy(_.length).maxBy(_._1)._2
最终函数返回一个Seq [String],只要结果可能包含几个具有相同最大长度的子字符串
答案 5 :(得分:1)
旁注::这是我对这个问题的第二个答案,因为StackOverflow策略不允许从根本上替换先前答案的内容。还要感谢@Kolmar的反馈,这个新答案比my prior answer的性能要好得多。
LCS(最长公共子字符串)问题空间已投入大量时间来寻找最佳解决方案策略。要观察更一般的计算机科学问题和最佳策略,请查看this Wikipedia article。这篇Wikipedia文章的更下方是一些pseudocode describing an implementation strategy。
基于Wikipedia文章的伪代码,我将介绍几种不同的解决方案。目的是允许人们无需进行大量重构即可复制/粘贴所需的特定解决方案:
LCSubstr
:翻译成the Wikipedia article pseudocode的Scala,它使用命令式可变样式 LCSubstrFp
:将LCSubstr
重构为惯用的 Scala功能不变样式 longestCommonSubstrings
:重构LCSubstrFp
以使用描述性名称(例如left
和right
而不是s
和t
),并使用跳过在Map
longestCommonSubstringsFast
:重构longestCommonSubstrings
以便针对CPU和内存进行深度优化longestCommonSubstringsWithIndexes
:重构longestCommonSubstringsFast
,以通过将每个条目扩展为(String, (Int, Int))
的元组来增强返回值,该元组既包含找到的子字符串,又包含每个输入{{1}中的索引},在其中找到子字符串(注意:如果同一对String
出现多次,则将创建索引对的组合扩展)String
:firstLongestCommonSubstring
的以效率为中心的版本,当仅关心第一个LCS并希望忽略其他相同大小的LCS时,它提供了提前终止的机会。longestCommonSubstringsFast
:重构longestCommonSubstringsUltimate
以增加内部实现的可变性,同时在外部保留函数的引用透明性。对OP的请求的更直接答案将落在longestCommonSubstringsFast
和LCSubstrFp
之间。 longestCommonSubstringsFast
是最直接的方法,但是效率很低。使用LCSubstrFp
的效率大大提高,因为最终使用的CPU和GC少得多。而且,如果在函数实现中包含和约束的内部可变性是可以接受的,那么longestCommonSubstringsFast
是迄今为止CPU负担和内存占用最小的版本。
翻译成the Wikipedia article pseudocode的Scala,它使用了命令式可变样式。
目的是尽可能地与Scala尽可能地重现一对一的实现。例如,Scala假定String的基于零的索引,而伪代码显然使用基于一的索引,这需要进行一些调整。
longestCommonSubstringsUltimate
将def LCSubstr(s: String, t: String): scala.collection.mutable.Set[String] =
if (s.nonEmpty && t.nonEmpty) {
val l: scala.collection.mutable.Map[(Int, Int), Int] = scala.collection.mutable.Map.empty
var z: Int = 0
var ret: scala.collection.mutable.Set[String] = scala.collection.mutable.Set.empty
(0 until s.length).foreach {
i =>
(0 until t.length).foreach {
j =>
if (s(i) == t(j)) {
if ((i == 0) || (j == 0))
l += ((i, j) -> 1)
else
l += ((i, j) -> (l((i - 1, j - 1)) + 1))
if (l((i, j)) > z) {
z = l((i, j))
ret = scala.collection.mutable.Set(s.substring(i - z + 1, i + 1))
}
else
if (l((i, j)) == z)
ret += s.substring(i - z + 1, i + 1)
} else
l += ((i, j) -> 0)
}
}
ret
}
else scala.collection.mutable.Set.empty
重构为惯用的 Scala函数不变样式。
所有命令式和突变式代码均已替换为功能和不可变的对应物。两个LCSubstr
循环已被递归替换。
for
重构def LCSubstrFp(s: String, t: String): Set[String] =
if (s.nonEmpty && t.nonEmpty) {
@scala.annotation.tailrec
def recursive(
i: Int = 0,
j: Int = 0,
z: Int = 0,
l: Map[(Int, Int), Int] = Map.empty,
ret: Set[String] = Set.empty
): Set[String] =
if (i < s.length) {
val (newI, newJ) =
if (j < t.length - 1) (i, j + 1)
else (i + 1, 0)
val lij =
if (s(i) != t(j)) 0
else
if ((i == 0) || (j == 0)) 1
else l((i - 1, j - 1)) + 1
recursive(
newI,
newJ,
if (lij > z) lij
else z,
l + ((i, j) -> lij),
if (lij > z) Set(s.substring(i - lij + 1, i + 1))
else
if ((lij == z) && (z > 0)) ret + s.substring(i - lij + 1, i + 1)
else ret
)
}
else ret
recursive()
}
else Set.empty
以使用描述性名称(例如LCSubstrFp
和left
而不是right
和s
),并跳过将零长度存储在t
。
除了增强可读性,这种重构省去了在Map
中存储零长度值,从而大大减少了“内存搅动”的数量。再次对功能样式进行了调整,通过将lengthByIndexLongerAndIndexShorter
包装在Set
中,返回值也得到了增强,从不返回空的Set
。如果返回的值为Option
,则包含的Some
将始终包含至少一项。
Set
重构def longestCommonSubstrings(left: String, right: String): Option[Set[String]] =
if (left.nonEmpty && right.nonEmpty) {
val (shorter, longer) =
if (left.length < right.length) (left, right)
else (right, left)
@scala.annotation.tailrec
def recursive(
indexLonger: Int = 0,
indexShorter: Int = 0,
currentLongestLength: Int = 0,
lengthByIndexLongerAndIndexShorter: Map[(Int, Int), Int] = Map.empty,
accumulator: List[Int] = Nil
): (Int, List[Int]) =
if (indexLonger < longer.length) {
val length =
if (longer(indexLonger) != shorter(indexShorter)) 0
else
if ((indexShorter == 0) || (indexLonger == 0)) 1
else lengthByIndexLongerAndIndexShorter.getOrElse((indexLonger - 1, indexShorter - 1), 0) + 1
val newCurrentLongestLength =
if (length > currentLongestLength) length
else currentLongestLength
val newLengthByIndexLongerAndIndexShorter =
if (length > 0) lengthByIndexLongerAndIndexShorter + ((indexLonger, indexShorter) -> length)
else lengthByIndexLongerAndIndexShorter
val newAccumulator =
if ((length < currentLongestLength) || (length == 0)) accumulator
else {
val entry = indexShorter - length + 1
if (length > currentLongestLength) List(entry)
else entry :: accumulator
}
if (indexShorter < shorter.length - 1)
recursive(
indexLonger,
indexShorter + 1,
newCurrentLongestLength,
newLengthByIndexLongerAndIndexShorter,
newAccumulator
)
else
recursive(
indexLonger + 1,
0,
newCurrentLongestLength,
newLengthByIndexLongerAndIndexShorter,
newAccumulator
)
}
else (currentLongestLength, accumulator)
val (length, indexShorters) = recursive()
if (indexShorters.nonEmpty)
Some(
indexShorters
.map {
indexShorter =>
shorter.substring(indexShorter, indexShorter + length)
}
.toSet
)
else None
}
else None
可以对CPU和内存进行深度优化。
在保持功能和不变的同时消除了所有的低效率,执行速度比longestCommonSubstrings
提高了几倍。通过使用一对仅跟踪当前行和先前行的longestCommonSubstrings
替换整个矩阵的Map
,可以实现大部分成本降低。
要轻松查看与List
,please view this visual diff的区别。
longestCommonSubstrings
通过重构def longestCommonSubstringsFast(left: String, right: String): Option[Set[String]] =
if (left.nonEmpty && right.nonEmpty) {
val (shorter, longer) =
if (left.length < right.length) (left, right)
else (right, left)
@scala.annotation.tailrec
def recursive(
indexLonger: Int = 0,
indexShorter: Int = 0,
currentLongestLength: Int = 0,
lengthsPrior: List[Int] = List.fill(shorter.length)(0),
lengths: List[Int] = Nil,
accumulator: List[Int] = Nil
): (Int, List[Int]) =
if (indexLonger < longer.length) {
val length =
if (longer(indexLonger) != shorter(indexShorter)) 0
else lengthsPrior.head + 1
val newCurrentLongestLength =
if (length > currentLongestLength) length
else currentLongestLength
val newAccumulator =
if ((length < currentLongestLength) || (length == 0)) accumulator
else {
val entry = indexShorter - length + 1
if (length > currentLongestLength) List(entry)
else entry :: accumulator
}
if (indexShorter < shorter.length - 1)
recursive(
indexLonger,
indexShorter + 1,
newCurrentLongestLength,
lengthsPrior.tail,
length :: lengths,
newAccumulator
)
else
recursive(
indexLonger + 1,
0,
newCurrentLongestLength,
0 :: lengths.reverse,
Nil,
newAccumulator
)
}
else (currentLongestLength, accumulator)
val (length, indexShorters) = recursive()
if (indexShorters.nonEmpty)
Some(
indexShorters
.map {
indexShorter =>
shorter.substring(indexShorter, indexShorter + length)
}
.toSet
)
else None
}
else None
来增强返回值,方法是将每个条目扩展为longestCommonSubstringsFast
的元组,该元组既包括找到的子字符串,又包括每个输入(String, (Int, Int))
内的子字符串所在的索引找到。
注意:如果同一对String
出现多次,则会创建索引对的组合扩展。
再次对功能样式进行了调整,通过将String
包装在List
中,返回值也得到了增强,从不返回空的List
。如果返回的值为Option
,则包含的Some
将始终包含至少一项。
List
注重效率的def longestCommonSubstringsWithIndexes(left: String, right: String): Option[List[(String, (Int, Int))]] =
if (left.nonEmpty && right.nonEmpty) {
val isLeftShorter = left.length < right.length
val (shorter, longer) =
if (isLeftShorter) (left, right)
else (right, left)
@scala.annotation.tailrec
def recursive(
indexLonger: Int = 0,
indexShorter: Int = 0,
currentLongestLength: Int = 0,
lengthsPrior: List[Int] = List.fill(shorter.length)(0),
lengths: List[Int] = Nil,
accumulator: List[(Int, Int)] = Nil
): (Int, List[(Int, Int)]) =
if (indexLonger < longer.length) {
val length =
if (longer(indexLonger) != shorter(indexShorter)) 0
else lengthsPrior.head + 1
val newCurrentLongestLength =
if (length > currentLongestLength) length
else currentLongestLength
val newAccumulator =
if ((length < currentLongestLength) || (length == 0)) accumulator
else {
val entry = (indexLonger - length + 1, indexShorter - length + 1)
if (length > currentLongestLength) List(entry)
else entry :: accumulator
}
if (indexShorter < shorter.length - 1)
recursive(
indexLonger,
indexShorter + 1,
newCurrentLongestLength,
lengthsPrior.tail,
length :: lengths,
newAccumulator
)
else
recursive(
indexLonger + 1,
0,
newCurrentLongestLength,
0 :: lengths.reverse,
Nil,
newAccumulator
)
}
else (currentLongestLength, accumulator)
val (length, indexPairs) = recursive()
if (indexPairs.nonEmpty)
Some(
indexPairs
.reverse
.map {
indexPair =>
(
longer.substring(indexPair._1, indexPair._1 + length),
if (isLeftShorter) indexPair.swap else indexPair
)
}
)
else None
}
else None
版本提供了一个机会,可以在仅关心第一个LCS并希望忽略其他相同大小的LCS时尽早终止。
longestCommonSubstringsFast
奖金:
重构def firstLongestCommonSubstring(left: String, right: String): Option[(String, (Int, Int))] =
if (left.nonEmpty && right.nonEmpty) {
val isLeftShorter = left.length < right.length
val (shorter, longer) =
if (isLeftShorter) (left, right)
else (right, left)
@scala.annotation.tailrec
def recursive(
indexLonger: Int = 0,
indexShorter: Int = 0,
currentLongestLength: Int = 0,
lengthsPrior: List[Int] = List.fill(shorter.length)(0),
lengths: List[Int] = Nil,
accumulator: Option[(Int, Int)] = None
): Option[(Int, (Int, Int))] =
if (indexLonger < longer.length) {
val length =
if (longer(indexLonger) != shorter(indexShorter)) 0
else lengthsPrior.head + 1
val newAccumulator =
if (length > currentLongestLength) Some((indexLonger - length + 1, indexShorter - length + 1))
else accumulator
if (length < shorter.length) {
val newCurrentLongestLength =
if (length > currentLongestLength) length
else currentLongestLength
if (indexShorter < shorter.length - 1)
recursive(
indexLonger,
indexShorter + 1,
newCurrentLongestLength,
lengthsPrior.tail,
length :: lengths,
newAccumulator
)
else
recursive(
indexLonger + 1,
0,
newCurrentLongestLength,
0 :: lengths.reverse,
Nil,
newAccumulator
)
}
else
recursive(longer.length, 0, length, lengthsPrior, lengths, newAccumulator) //early terminate
}
else accumulator.map((currentLongestLength, _))
recursive().map {
case (length, indexPair) =>
(
longer.substring(indexPair._1, indexPair._1 + length),
if (isLeftShorter) indexPair.swap else indexPair
)
}
}
else None
,以增加内部实现的可变性,同时从外部保留函数的引用透明性。
进一步消除所有低效率的问题,同时保持功能性和参照透明性(参与利用实现本身内部的可变性(有些人认为 不能有效运行)),执行速度几乎是{的三倍{1}}。大部分成本削减是通过将一对longestCommonSubstringsFast
替换为一个longestCommonSubstringsFast
。
要轻松查看与List
,please view this visual diff的区别。
Array
答案 6 :(得分:0)
这种方法怎么样:
left.inits.flatMap(_。tails)
.toList.sortBy(_。length).reverse
.find(right.contains(_))。get
全功能:
def lcs(left: String, right: String) = {
left.inits.flatMap(_.tails)
.toList.sortBy(_.length).reverse
.find(right.contains(_)).get
}
注意: get永远不会为空,因为初始字符串排列还包含空字符串,该空字符串将始终匹配某些内容。