String Scala中两个字符串的差异

String Scala中两个字符串的差异,string,scala,collections,diff,String,Scala,Collections,Diff,假设我正在编写diffs1:String,s2:String:List[String]来检查s1==s2并返回错误列表: s1[i]!=s2[i]错误为s1[i]!=s2[i] s1[i]如果i>=s2.length,则错误为s1[i]未定义 s2[i]如果i>=s1.length,则错误为s2[i]缺失 例如: diff("a", "a") // returns Nil diff("abc", "abc") // Nil diff("xyz", "abc") // List("x !=

假设我正在编写diffs1:String,s2:String:List[String]来检查s1==s2并返回错误列表:

s1[i]!=s2[i]错误为s1[i]!=s2[i] s1[i]如果i>=s2.length,则错误为s1[i]未定义 s2[i]如果i>=s1.length,则错误为s2[i]缺失 例如:

diff("a", "a")     // returns Nil
diff("abc", "abc") // Nil
diff("xyz", "abc") // List("x != a", "y != b", "z != c")
diff("abcd", "ab") // List("c is undefined", "d is undefined")
diff("ab", "abcd") // List("c is missing", "d is missing")
diff("", "ab")     // List("a is missing", "b is missing")  
diff("axy", "ab")  // List("x != b", "y is undefined") 
你会怎么写

顺便说一下,我是这样写的:

def compare(pair: (Option[Char], Option[Char])) = pair match { 
  case (Some(x), None)    => Some(s"$x is undefined")
  case (None, Some(y))    => Some(s"$y is missing")
  case (Some(x), Some(y)) => if (x != y) Some(s"$x != $y") else None 
  case _ => None
}

def diff(s1: String, s2: String) = {
  val os1 = s1.map(Option.apply)
  val os2 = s2.map(Option.apply)
  os1.zipAll(os2, None, None).flatMap(compare)
}

[原始答案见下文]

这可以通过递归算法完成:

def diff(a: String, b: String): List[String] = {
  @annotation.tailrec
  def loop(l: List[Char], r: List[Char], res: List[String]): List[String] =
    (l, r) match {
      case (Nil, Nil) =>
        res.reverse
      case (undef, Nil) =>
        res.reverse ++ undef.map(c => s"$c is undefined")
      case (Nil, miss) =>
        res.reverse ++ miss.map(c => s"$c is missing")
      case (lh :: lt, rh :: rt) if lh != rh =>
        loop(lt, rt, s"$lh != $rh" +: res)
      case (_ :: lt, _ :: rt) =>
        loop(lt, rt, res)
    }

  loop(a.toList, b.toList, Nil)
}
就我个人而言,我发现这比使用Option/zipAll/flatMap更明显,但这显然是一个品味问题,也是您碰巧熟悉的问题。我认为这更灵活,因为,例如,可以很容易地修改它,为所有未定义/缺少的字符生成一个错误字符串

如果效率很重要,则此版本使用迭代器避免创建临时列表,并使用嵌套的If/else而不是match:

感谢Brian McCutchon指出了使用字符串而不是列表[Char]的问题,以及Andrey Tyukin鼓励我发布更有效的解决方案

原始答案 递归实现并不可怕:

def diff(a: String, b: String): List[String] = {
  @annotation.tailrec
  def loop(l: String, r: String, res: List[String]) : List[String] = (l, r) match {
    case ("", "") =>
      res
    case (lrem, "") =>
      res ++ lrem.map(c => s"$c is undefined")
    case ("", rrem) =>
      res ++ rrem.map(c => s"$c is missing")
    case _ if l.head != r.head =>
      loop(l.tail, r.tail, res :+ s"${l.head} != ${r.head}")
    case _ =>
      loop(l.tail, r.tail, res)
  }

 loop(a, b, Nil)
}

这应该可以执行,除非有很多错误,在这种情况下,附加到res将变得昂贵。您可以通过在res前面加上前缀,然后根据需要在末尾加上反转来解决这个问题,但这会使代码不那么清晰。

[原始答案见下文]

这可以通过递归算法完成:

def diff(a: String, b: String): List[String] = {
  @annotation.tailrec
  def loop(l: List[Char], r: List[Char], res: List[String]): List[String] =
    (l, r) match {
      case (Nil, Nil) =>
        res.reverse
      case (undef, Nil) =>
        res.reverse ++ undef.map(c => s"$c is undefined")
      case (Nil, miss) =>
        res.reverse ++ miss.map(c => s"$c is missing")
      case (lh :: lt, rh :: rt) if lh != rh =>
        loop(lt, rt, s"$lh != $rh" +: res)
      case (_ :: lt, _ :: rt) =>
        loop(lt, rt, res)
    }

  loop(a.toList, b.toList, Nil)
}
就我个人而言,我发现这比使用Option/zipAll/flatMap更明显,但这显然是一个品味问题,也是您碰巧熟悉的问题。我认为这更灵活,因为,例如,可以很容易地修改它,为所有未定义/缺少的字符生成一个错误字符串

如果效率很重要,则此版本使用迭代器避免创建临时列表,并使用嵌套的If/else而不是match:

感谢Brian McCutchon指出了使用字符串而不是列表[Char]的问题,以及Andrey Tyukin鼓励我发布更有效的解决方案

原始答案 递归实现并不可怕:

def diff(a: String, b: String): List[String] = {
  @annotation.tailrec
  def loop(l: String, r: String, res: List[String]) : List[String] = (l, r) match {
    case ("", "") =>
      res
    case (lrem, "") =>
      res ++ lrem.map(c => s"$c is undefined")
    case ("", rrem) =>
      res ++ rrem.map(c => s"$c is missing")
    case _ if l.head != r.head =>
      loop(l.tail, r.tail, res :+ s"${l.head} != ${r.head}")
    case _ =>
      loop(l.tail, r.tail, res)
  }

 loop(a, b, Nil)
}
这应该可以执行,除非有很多错误,在这种情况下,附加到res将变得昂贵。您可以通过在res前面加上前缀,然后根据需要在末尾加上反转来解决这个问题,但这会使代码变得不那么清晰。

更简洁一些 首先,下面是我如何在脑海中实现这个方法:

def diff(s1: String, s2: String): List[String] =
  (s1, s2).zipped.collect {
    case (x, y) if x != y => s"$x != $y"
  }.toList ++
    s1.drop(s2.length).map(x => s"$x is undefined") ++
    s2.drop(s1.length).map(y => s"$y is missing")
它的字符数大约是原始实现的一半,在我看来,它至少和原始实现一样可读。你可能会争辩说,放弃技巧有点太聪明了,你可能是对的,但我认为一旦你得到它,它就会读得很好

效率高一点 像这样的方法是自包含的,并且易于测试,如果有可能在性能非常重要的情况下使用它,那么一个命令式的实现是值得考虑的。下面是我将如何做的简要说明:

def diffFast(s1: String, s2: String): IndexedSeq[String] = {
  val builder = Vector.newBuilder[String]

  def diff(short: String, long: String, status: String) = {
    builder.sizeHint(long.length)
    var i = 0

    while (i < short.length) {
      val x = s1.charAt(i)
      val y = s2.charAt(i)
      if (x != y) builder += s"$x != $y"
      i += 1
    }

    while (i < long.length) {
      val x = long.charAt(i)
      builder += s"$x is $status"
      i += 1
    }
  }

  if (s1.length <= s2.length) diff(s1, s2, "missing")
    else diff(s2, s1, "undefined")

  builder.result
}
我们可以测量原始版本(原样并返回列表)、简明版本、快速版本(返回IndexedSeq和List)以及Tim的递归版本的输入吞吐量:

Benchmark                 Mode  Cnt       Score     Error  Units
DiffBench.checkConcise   thrpt   20   47412.127 ± 550.693  ops/s
DiffBench.checkFast      thrpt   20  108661.093 ± 371.827  ops/s
DiffBench.checkFastList  thrpt   20   91745.269 ± 157.128  ops/s
DiffBench.checkOrig      thrpt   20    8129.848 ±  59.989  ops/s
DiffBench.checkOrigList  thrpt   20    7916.637 ±  15.736  ops/s
DiffBench.checkRec       thrpt   20   62409.682 ± 580.529  ops/s
简言之:就性能而言,您的原始实现非常差。我猜更多的原因是由于所有的分配而不是多次遍历,我的简明实现与可读性较差的递归实现相比具有竞争力,吞吐量大约是原始实现的六倍,而且命令式实现的速度几乎是其他实现的两倍。

更简洁一点 首先,下面是我如何在脑海中实现这个方法:

def diff(s1: String, s2: String): List[String] =
  (s1, s2).zipped.collect {
    case (x, y) if x != y => s"$x != $y"
  }.toList ++
    s1.drop(s2.length).map(x => s"$x is undefined") ++
    s2.drop(s1.length).map(y => s"$y is missing")
它的字符数大约是原始实现的一半,在我看来,它至少和原始实现一样可读。你可能会争辩说,放弃技巧有点太聪明了,你可能是对的,但我认为一旦你得到它,它就会读得很好

效率高一点 像这样的方法是自包含的,并且易于测试,如果有可能在性能非常重要的情况下使用它,那么一个命令式的实现是值得考虑的。下面是我将如何做的简要说明:

def diffFast(s1: String, s2: String): IndexedSeq[String] = {
  val builder = Vector.newBuilder[String]

  def diff(short: String, long: String, status: String) = {
    builder.sizeHint(long.length)
    var i = 0

    while (i < short.length) {
      val x = s1.charAt(i)
      val y = s2.charAt(i)
      if (x != y) builder += s"$x != $y"
      i += 1
    }

    while (i < long.length) {
      val x = long.charAt(i)
      builder += s"$x is $status"
      i += 1
    }
  }

  if (s1.length <= s2.length) diff(s1, s2, "missing")
    else diff(s2, s1, "undefined")

  builder.result
}
我们可以测量原始版本(原样并返回列表)、简明版本、快速版本(返回IndexedSeq和List)以及Tim的递归版本的输入吞吐量:

Benchmark                 Mode  Cnt       Score     Error  Units
DiffBench.checkConcise   thrpt   20   47412.127 ± 550.693  ops/s
DiffBench.checkFast      thrpt   20  108661.093 ± 371.827  ops/s
DiffBench.checkFastList  thrpt   20   91745.269 ± 157.128  ops/s
DiffBench.checkOrig      thrpt   20    8129.848 ±  59.989  ops/s
DiffBench.checkOrigList  thrpt   20    7916.637 ±  15.736  ops/s
DiffBench.checkRec       thrpt   20   62409.682 ± 580.529  ops/s

简言之:就性能而言,您的原始实现非常差。我猜更多的原因是由于所有的分配而不是多次遍历,我的简明实现与可读性较差的递归实现相比具有竞争力,吞吐量大约是原始实现的六倍,而且命令式实现的速度几乎是其他实现的两倍。

您是否优先考虑清晰性和正确性?如果是这样,你的工具

在我看来,这很不错。如果你将它应用于非常大的字符串,或者你关心性能,但是,这根本不是你想要的。谢谢你的回复!我正在优先考虑清晰性,但现在我慢慢意识到遍历字符串三次显然是次优的。我会考虑如何改进它。如果你想对你的代码进行一般性的改进,可能是一个更好的网站。谢谢,也许我会在那里发布这个问题。你是否优先考虑清晰性和正确性?如果是这样的话,我觉得你的实现相当不错。如果你将它应用于非常大的字符串,或者你关心性能,但是,这根本不是你想要的。谢谢你的回复!我正在优先考虑清晰性,但现在我慢慢意识到遍历字符串三次显然是次优的。我会考虑如何改进它。如果你正在寻找代码的一般改进,可能是一个更好的网站。谢谢,也许我会在那里发布这个问题。谁会在乎这个东西是否有错误的渐近行为?如果你给一个中等大小的源代码文件输入50000个字符,比如说1000到1500行,它会意外地挂起超过一分钟。我想如果你给它一个大约15万个字符的短篇故事,它将永远不会回来。更糟糕的是:它可能在测试过程中看起来还可以,但在生产过程中就消失了,因为没有人会期望对一个明显存在的问题使用On^2算法。即使您修复了添加到列表的问题,即使没有错误,您的代码也处于^2状态,因为您反复调用字符串的尾部。您可以通过首先将字符串转换为列表来解决此问题。不过,总的来说,这个解决方案比OP的代码更不清晰,效率也更低。@BrianMcCutchon谢谢你的评论,我考虑的是列表,没有考虑到字符串尾部的成本。至于哪个更清楚,我想这只是一个意见的问题。由于输出清晰易读,我的印象是,这不会产生大量错误,因此追加的成本并不重要。@Andreytukin我使用反向书写了完整版本,它不仅仅是额外的八个字符,因此,我选择了更简单但效率更低的版本,作为对这个问题提出不同方法的一种方式。@Tim,谢谢你详细而周到的回答。谁会在乎这个东西是否具有错误的渐近行为?如果你给一个中等大小的源代码文件输入50000个字符,比如说1000到1500行,它会意外地挂起超过一分钟。我想如果你给它一个大约15万个字符的短篇故事,它将永远不会回来。更糟糕的是:它可能在测试过程中看起来还可以,但在生产过程中就消失了,因为没有人会期望对一个明显存在的问题使用On^2算法。即使您修复了添加到列表的问题,即使没有错误,您的代码也处于^2状态,因为您反复调用字符串的尾部。您可以通过首先将字符串转换为列表来解决此问题。不过,总的来说,这个解决方案比OP的代码更不清晰,效率也更低。@BrianMcCutchon谢谢你的评论,我考虑的是列表,没有考虑到字符串尾部的成本。至于哪个更清楚,我想这只是一个意见的问题。由于输出清晰易读,我的印象是,这不会产生大量错误,因此追加的成本并不重要。@Andreytukin我使用反向书写了完整版本,它不仅仅是额外的八个字符,因此,我选择了更简单但效率较低的版本作为解决问题的另一种方法。@Tim,谢谢你详细而周到的回答。你的实现与原来的实现不完全相同。在左/右字符串中缺少字符时,存在未定义和缺少的差异。它不会降低优化版本的相关性:非常感谢,特拉维斯!我喜欢简洁的实现和基准。只是找不到如何处理丢失的大小写。@Michael如果要不对称地处理丢失的字符,请分别映射带有删除前缀的两个列表:def diffs1:String,s2:String:List[String]=s1,s2.zipped.collect{case x,y If x!=y=>s$x!=y}.toList++s1.drops2.length.map_+未定义++s2.drops1.length.map_+缺失。在优化的命令式版本中,可以将错误消息作为第三个参数传递给if-else中的diff,这不会显著改变基准测试的结果。@Andreytytukin明白了:谢谢。谢谢你注意到了消息中的问题-我已经更新了答案,现在正在重新运行基准测试。你的实现与原来的实现不完全相同。两者之间有一个差异,即未定义,当字符
左/右字符串中缺少er。它不会降低优化版本的相关性:非常感谢,特拉维斯!我喜欢简洁的实现和基准。只是找不到如何处理丢失的大小写。@Michael如果要不对称地处理丢失的字符,请分别映射带有删除前缀的两个列表:def diffs1:String,s2:String:List[String]=s1,s2.zipped.collect{case x,y If x!=y=>s$x!=y}.toList++s1.drops2.length.map_+未定义++s2.drops1.length.map_+缺失。在优化的命令式版本中,您可以在if-else中将错误消息作为第三个参数传递给diff,这不会显著改变基准测试的结果。@Andreytukin了解到:谢谢。感谢您注意到消息中的问题-我已经更新了答案,现在正在重新运行基准测试。