在一组字符串中找到最长的公共起始子串

IT技术 javascript python ruby haskell longest-prefix
2021-01-20 05:12:18

对于一个相对微不足道的问题,提出最优雅的 JavaScript、Ruby 或其他解决方案是一个挑战。

这个问题是最长公共子串问题的一个更具体的例子我只需要在数组中找到最长的公共起始子串。这大大简化了问题。

例如,最长的子串[interspecies, interstelar, interstate]是“inters”。但是,我不需要在[specifics, terrific].

我已经通过在 JavaScript 中快速编码解决方案解决了这个问题,作为我关于类似 shell 的选项卡完成这里是测试页面)的回答的一部分这是该解决方案,稍作调整:

function common_substring(data) {
  var i, ch, memo, idx = 0
  do {
    memo = null
    for (i=0; i < data.length; i++) {
      ch = data[i].charAt(idx)
      if (!ch) break
      if (!memo) memo = ch
      else if (ch != memo) break
    }
  } while (i == data.length && idx < data.length && ++idx)

  return (data[0] || '').slice(0, idx)
}

代码可在此 Gist 中以及 Ruby 中的类似解决方案中找到。您可以将 gist 克隆为 git repo 以进行尝试:

$ git clone git://gist.github.com/257891.git substring-challenge

我对这些解决方案不太满意。我有一种感觉,它们可能会以更优雅和更少的执行复杂性来解决——这就是我发布这个挑战的原因。

我将接受我认为最优雅或最简洁的解决方案作为答案。例如,这是我想出的一个疯狂的 Ruby hack——&在 String 上定义运算符:

# works with Ruby 1.8.7 and above
class String
  def &(other)
    difference = other.to_str.each_char.with_index.find { |ch, idx|
      self[idx].nil? or ch != self[idx].chr
    }
    difference ? self[0, difference.last] : self
  end
end

class Array
  def common_substring
    self.inject(nil) { |memo, str| memo.nil? ? str : memo & str }.to_s
  end
end

JavaScript 或 Ruby 的解决方案是首选,但你可以展示其他语言的巧妙解决方案,只要你解释发生了什么。请仅使用标准库中的代码。

更新:我最喜欢的解决方案

我选择了JavaScript的分拣解决方案通过肯纳贝克为“答案”,因为它让我觉得既意外和天才。如果我们不考虑实际排序的复杂度(假设它被语言实现无限优化),解决方案的复杂度只是比较两个字符串。

其他很棒的解决方案:

感谢参与!从评论中可以看出,我学到了很多东西(甚至是关于 Ruby 的)。

6个回答

这是一个品味问题,但这是一个简单的 javascript 版本:它对数组进行排序,然后只查看第一项和最后一项。

//数组中最长的公共起始子串

function sharedStart(array){
    var A= array.concat().sort(), 
    a1= A[0], a2= A[A.length-1], L= a1.length, i= 0;
    while(i<L && a1.charAt(i)=== a2.charAt(i)) i++;
    return a1.substring(0, i);
}

演示

sharedStart(['interspecies', 'interstelar', 'interstate'])  //=> 'inters'
sharedStart(['throne', 'throne'])                           //=> 'throne'
sharedStart(['throne', 'dungeon'])                          //=> ''
sharedStart(['cheese'])                                     //=> 'cheese'
sharedStart([])                                             //=> ''
sharedStart(['prefix', 'suffix'])                           //=> ''
请注意,排序是在 O(n·log n) 中完成的。
2021-03-16 05:12:18
您不必对其进行排序。对?只需找到最小的一个和最大的一个并比较两者。
2021-03-24 05:12:18
我有另一个想法,如果你输入一个单元素数组,返回逻辑上应该是整个字符串——它确实匹配自己。就地编辑,将 tem1=A.shift() 替换为 tem1=A[0]。
2021-03-29 05:12:18
@JeffMay 现在可以了。
2021-04-04 05:12:18
如果数组有一项或全部重复,这将不起作用。
2021-04-10 05:12:18

在 Python 中:

>>> from os.path import commonprefix
>>> commonprefix('interspecies interstelar interstate'.split())
'inters'
不敢相信。Python 在 stdlib 中有这个吗?
2021-03-26 05:12:18
干得好,python。感觉像是在作弊。
2021-03-29 05:12:18
是的,但是它隐藏在os.pathmodule中,虽然它是一个有用的字符串操作函数。
2021-04-14 05:12:18

Ruby单线:

l=strings.inject{|l,s| l=l.chop while l!=s[0...l.length];l}
对于不在结果中的每个字符,这将遍历每个字符串一次。效率不高。
2021-04-07 05:12:18
它可能需要比其他解决方案更多的迭代,但我喜欢它。这就是我认为的优雅。
2021-04-12 05:12:18

我的 Haskell 单线:

import Data.List

commonPre :: [String] -> String
commonPre = map head . takeWhile (\(x:xs)-> all (==x) xs) . transpose

编辑:barkmadley 对下面的代码给出了很好的解释。我还要补充一点,haskell 使用惰性求值,因此我们可以懒惰地使用transpose; 它只会在必要时转置我们的列表以找到公共前缀的结尾。

可爱的代码,但不幸的是,当一个字符串是另一个字符串的前缀时,这会失败,例如commonPre ["foo", "foobar", "foobarbaz"] == "foobarbaz".
2021-03-19 05:12:18
这看起来很不错的代码!您能花点时间向我们不懂语言的人解释一下吗?(你不必去很深的地方)
2021-03-22 05:12:18
首先你转置字符串。 ["abc","abd","aed"] -> ["aaa","bbe","cdd"],然后你从这个列表中取出,直到有什么不同["aaa"],然后你取出这个列表中每个列表的第一个元素(字符串只是 Haskell 中的列表),这让你"a". 从右到左阅读更容易,因为这是数据在 Haskell 的点自由风格中传输的方式。
2021-03-30 05:12:18

您只需要遍历所有字符串直到它们不同,然后将子字符串带到这一点。

伪代码:

loop for i upfrom 0
     while all strings[i] are equal
     finally return substring[0..i]

通用 Lisp:

(defun longest-common-starting-substring (&rest strings)
  (loop for i from 0 below (apply #'min (mapcar #'length strings))
     while (apply #'char=
                  (mapcar (lambda (string) (aref string i))
                          strings))
     finally (return (subseq (first strings) 0 i))))
@Denis:1. 没有“更优”。最优的就是最优的。在给定的 Lisp 实现中有两种可能的优化:applyloops 或dos替换两种形式(也消除mapcar),并在发生不匹配时立即中断第二个形式。2. 这里的“可用选项的最小值”是多少?3. 这里没有搜索树。如果正确结果的长度为n,则您需要查看每个输入子字符串中索引n之前的每个字符这就是这个例程所做的,它只线性地查看每个字符一次。
2021-03-21 05:12:18
为了减少搜索树,进行第一遍查找可用选项的最小值不是更理想吗?
2021-04-10 05:12:18