39

这是为一个相对琐碎的问题想出最优雅的 JavaScript、Ruby 或其他解决方案的挑战。

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

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

我已经通过在 JavaScript 中快速编写一个解决方案来解决这个问题,作为我关于 shell-like tab-completion ( test page here )的答案的一部分。这是该解决方案,稍作调整:

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 中的类似解决方案。您可以将要点克隆为 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 的解决方案是首选,但你可以用其他语言展示聪明的解决方案,只要你解释发生了什么。请仅使用标准库中的代码。

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

我选择了kennebecJavaScript 排序解决方案作为“答案”,因为它让我觉得既意外又天才。如果我们忽略实际排序的复杂性(让我们想象它被语言实现无限优化),解决方案的复杂性只是比较两个字符串。

其他很棒的解决方案:

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

4

31 回答 31

55

这是一个口味问题,但这是一个简单的 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'])                           //=> ''
于 2009-12-16T19:26:12.593 回答
38

在 Python 中:

>>> from os.path import commonprefix
>>> commonprefix('interspecies interstelar interstate'.split())
'inters'
于 2009-12-16T18:22:48.103 回答
9

我的 Haskell 单线:

import Data.List

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

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

于 2009-12-16T23:03:26.210 回答
9

红宝石单线:

l=strings.inject{|l,s| l=l.chop while l!=s[0...l.length];l}
于 2009-12-16T18:00:44.440 回答
9

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

伪代码:

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))))
于 2009-12-16T18:02:20.507 回答
6

还有另一种方法:使用正则表达式贪婪。

words = %w(interspecies interstelar interstate)
j = '='
str = ['', *words].join(j)
re = "[^#{j}]*"

str =~ /\A
    (?: #{j} ( #{re} ) #{re} )
    (?: #{j}    \1     #{re} )*
\z/x

p $1

还有一行,由 mislav 提供(50 个字符):

p ARGV.join(' ').match(/^(\w*)\w*(?: \1\w*)*$/)[1]
于 2009-12-17T14:27:14.777 回答
4

在 Python 中,除了我在另一个答案中展示的现有函数外,我不会使用任何东西commonprefix,但我忍不住重新发明轮子:P。这是我基于迭代器的方法:

>>> a = 'interspecies interstelar interstate'.split()
>>>
>>> from itertools import takewhile, chain, izip as zip, imap as map
>>> ''.join(chain(*takewhile(lambda s: len(s) == 1, map(set, zip(*a)))))
'inters'

编辑:解释这是如何工作的。

zip生成元素的元组,一次取每一项a

In [6]: list(zip(*a))  # here I use list() to expand the iterator
Out[6]:
[('i', 'i', 'i'),
 ('n', 'n', 'n'),
 ('t', 't', 't'),
 ('e', 'e', 'e'),
 ('r', 'r', 'r'),
 ('s', 's', 's'),
 ('p', 't', 't'),
 ('e', 'e', 'a'),
 ('c', 'l', 't'),
 ('i', 'a', 'e')]

通过映射set这些项目,我得到了一系列独特的字母:

In [7]: list(map(set, _))  # _ means the result of the last statement above
Out[7]:
[set(['i']),
 set(['n']),
 set(['t']),
 set(['e']),
 set(['r']),
 set(['s']),
 set(['p', 't']),
 set(['a', 'e']),
 set(['c', 'l', 't']),
 set(['a', 'e', 'i'])]

takewhile(predicate, items)当谓词为 True 时,从中获取元素;在这种特殊情况下,当sets 有一个元素时,即所有单词在该位置都有相同的字母:

In [8]: list(takewhile(lambda s: len(s) == 1, _))
Out[8]:
[set(['i']),
 set(['n']), 
 set(['t']), 
 set(['e']), 
 set(['r']), 
 set(['s'])]

在这一点上,我们有一个可迭代的集合,每个集合都包含我们正在寻找的前缀的一个字母。为了构造字符串,我们chain将它们放入一个可迭代的对象中,从中我们将字母join放入最终的字符串中。

使用迭代器的神奇之处在于所有项目都是按需生成的,因此当takewhile停止请求项目时,压缩会在该点停止,并且不会进行不必要的工作。我的单行中的每个函数调用都有一个隐式for和一个隐式break.

于 2009-12-16T19:02:15.763 回答
3

结合kennebecFlorian Fjberryman的答案,得出以下 Haskell 单线:

commonPrefix l = map fst . takeWhile (uncurry (==)) $ zip (minimum l) (maximum l)

有了Control.Arrow一个可以得到一个无点形式:

commonPrefix = map fst . takeWhile (uncurry (==)) . uncurry zip . (minimum &&& maximum)
于 2011-12-04T00:01:27.057 回答
3

只是为了好玩,这里有一个用 (SWI-)PROLOG 编写的版本:

common_pre([[C|Cs]|Ss], [C|Res]) :-
  maplist(head_tail(C), [[C|Cs]|Ss], RemSs), !,
  common_pre(RemSs, Res).
common_pre(_, []).

head_tail(H, [H|T], T).

跑步:

?- S=["interspecies", "interstelar", "interstate"], common_pre(S, CP), string_to_list(CPString, CP).

给出:

CP = [105, 110, 116, 101, 114, 115],
CPString = "inters".

解释:

(SWI-)PROLOG 将字符串视为字符代码(数字)列表。谓词common_pre/2所做的所有操作都是递归模式匹配以从所有列表(所有字符串,)的列表中C的第一个列表(字符串,)的头部选择第一个代码(),并将匹配的代码附加到结果中,如果它是所有列表(字符串)的所有(剩余)头部共有,否则它终止。[C|Cs][[C|Cs]|Ss]C

不错,干净,简单,高效... :)

于 2009-12-17T06:13:13.163 回答
3

这可能不是最简洁的解决方案(取决于您是否已经有一个库),但一种优雅的方法是使用 trie。我使用尝试在我的 Scheme 解释器中实现制表符完成:

http://github.com/jcoglan/heist/blob/master/lib/trie.rb

例如:

tree = Trie.new
%w[interspecies interstelar interstate].each { |s| tree[s] = true }
tree.longest_prefix('')
#=> "inters"

我还使用它们来匹配通道名称和 Bayeux 协议的通配符;看到这些:

http://github.com/jcoglan/faye/blob/master/client/channel.js

http://github.com/jcoglan/faye/blob/master/lib/faye/channel.rb

于 2009-12-16T17:52:02.260 回答
3

基于@Svante 算法的 javascript 版本:

function commonSubstring(words){
    var iChar, iWord,
        refWord = words[0],
        lRefWord = refWord.length,
        lWords = words.length;
    for (iChar = 0; iChar < lRefWord; iChar += 1) {
        for (iWord = 1; iWord < lWords; iWord += 1) {
            if (refWord[iChar] !== words[iWord][iChar]) {
                return refWord.substring(0, iChar);
            }
        }
    }
    return refWord;
}
于 2010-09-14T15:51:27.310 回答
2

这与 Roberto Bonvallet 的解决方案非常相似,除了在 r​​uby​​ 中。

chars = %w[interspecies interstelar interstate].map {|w| w.split('') }
chars[0].zip(*chars[1..-1]).map { |c| c.uniq }.take_while { |c| c.size == 1 }.join

第一行用一个字符数组替换每个单词。接下来,我使用zip来创建这个数据结构:

[["i", "i", "i"], ["n", "n", "n"], ["t", "t", "t"], ...

map并将其uniq减少到[["i"],["n"],["t"], ...

take_while将字符从数组中拉出,直到找到大小不为一的字符(意味着并非所有字符都相同)。最后,我join把它们重新组合在一起。

于 2009-12-17T04:37:26.073 回答
2
Python 2.6 (r26:66714, Oct  4 2008, 02:48:43) 

>>> a = ['interspecies', 'interstelar', 'interstate']

>>> print a[0][:max(
        [i for i in range(min(map(len, a))) 
            if len(set(map(lambda e: e[i], a))) == 1]
        ) + 1]

inters
  • i for i in range(min(map(len, a))),最大查找次数不能大于最短字符串的长度;在这个例子中,这将评估为[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  • len(set(map(lambda e: e[i], a)))i-th, 1)为列表中的每个字符串创建一个字符数组;2)制作一套;3)确定集合的大小

  • [i for i in range(min(map(len, a))) if len(set(map(lambda e: e[i], a))) == 1],仅包括字符集的大小为 1 的字符(该位置的所有字符都相同..);在这里它会评估为[0, 1, 2, 3, 4, 5]

  • 最后取max,加一,得到子串...

注意:以上不适用于a = ['intersyate', 'intersxate', 'interstate', 'intersrate'],但这会:

 >>> index = len(
         filter(lambda l: l[0] == l[1], 
             [ x for x in enumerate(
                 [i for i in range(min(map(len, a))) 
                     if len(set(map(lambda e: e[i], a))) == 1]
         )]))
 >>> a[0][:index]
 inters
于 2009-12-16T17:43:18.933 回答
2

如果您不太关心最终性能,这似乎并不复杂:

def common_substring(data)
  data.inject { |m, s| s[0,(0..m.length).find { |i| m[i] != s[i] }.to_i] }
end

注入的一个有用特性是能够在数组的第一个元素上进行预播种。这避免了零备忘检查。

puts common_substring(%w[ interspecies interstelar interstate ]).inspect
# => "inters"
puts common_substring(%w[ feet feel feeble ]).inspect
# => "fee"
puts common_substring(%w[ fine firkin fail ]).inspect
# => "f"
puts common_substring(%w[ alpha bravo charlie ]).inspect
# => ""
puts common_substring(%w[ fork ]).inspect
# => "fork"
puts common_substring(%w[ fork forks ]).inspect
# => "fork"

更新:如果高尔夫是这里的游戏,那么 67 个字符:

def f(d)d.inject{|m,s|s[0,(0..m.size).find{|i|m[i]!=s[i]}.to_i]}end
于 2009-12-16T17:58:22.853 回答
2

接受的解决方案已损坏(例如,它返回a类似 的字符串['a', 'ba'])。修复非常简单,您只需更改 3 个字符(从indexOf(tem1) == -1indexOf(tem1) != 0),该功能就会按预期工作。

不幸的是,当我尝试编辑答案以修复错字时,SO 告诉我“编辑必须至少有 6 个字符”。我可以通过改进命名和可读性来改变这 3 个字符,但这感觉有点太多了。

因此,以下是肯尼贝克解决方案的固定和改进(至少从我的角度来看)版本:

function commonPrefix(words) {
  max_word = words.reduce(function(a, b) { return a > b ? a : b });
  prefix   = words.reduce(function(a, b) { return a > b ? b : a }); // min word

  while(max_word.indexOf(prefix) != 0) {
    prefix = prefix.slice(0, -1);
  }

  return prefix;
}

(在jsFiddle上)

请注意,它使用reduce方法(JavaScript 1.8)来查找字母数字最大值/最小值,而不是对数组进行排序,然后获取它的第一个和最后一个元素。

于 2013-07-23T23:33:23.403 回答
2

在使用所有花哨的函数式编程、排序和正则表达式等阅读这些答案时,我只是想:C 有什么问题?所以这是一个看起来很傻的小程序。

#include <stdio.h>

int main (int argc, char *argv[])
{
  int i = -1, j, c;

  if (argc < 2)
    return 1;

  while (c = argv[1][++i])
    for (j = 2; j < argc; j++)
      if (argv[j][i] != c)
        goto out;

 out:
  printf("Longest common prefix: %.*s\n", i, argv[1]);
}

编译它,用你的字符串列表作为命令行参数运行它,然后支持我使用goto

于 2015-03-30T12:33:23.150 回答
1

有趣的替代 Ruby 解决方案:

def common_prefix(*strings)
  chars  = strings.map(&:chars)
  length = chars.first.zip( *chars[1..-1] ).index{ |a| a.uniq.length>1 }
  strings.first[0,length]
end

p common_prefix( 'foon', 'foost', 'forlorn' ) #=> "fo"
p common_prefix( 'foost', 'foobar', 'foon'  ) #=> "foo"
p common_prefix( 'a','b'  )                   #=> ""

如果你使用它可能有助于加快速度chars = strings.sort_by(&:length).map(&:chars),因为第一个字符串越短,由zip. 但是,如果您关心速度,您可能无论如何都不应该使用此解决方案。:)

于 2014-02-14T16:03:04.317 回答
1

这是一个有效的红宝石解决方案。我基于高/低猜谜游戏的策略理念,在最长前缀上迭代归零。

如果我错了,有人纠正我,但我认为复杂度是 O(n log n),其中 n 是最短字符串的长度,字符串的数量被认为是一个常数。

def common(strings)
  lo = 0
  hi = strings.map(&:length).min - 1
  return '' if hi < lo

  guess, last_guess = lo, hi

  while guess != last_guess
    last_guess = guess
    guess = lo + ((hi - lo) / 2.0).ceil

    if strings.map { |s| s[0..guess] }.uniq.length == 1
      lo = guess
    else
      hi = guess
    end
  end

  strings.map { |s| s[0...guess] }.uniq.length == 1 ? strings.first[0...guess] : ''
end

并检查它是否有效:

>> common %w{ interspecies interstelar interstate }
=> "inters"

>> common %w{ dog dalmation }
=> "d"

>> common %w{ asdf qwerty }
=> ""

>> common ['', 'asdf']
=> ""
于 2013-12-17T21:51:37.570 回答
1

您可以只获取字符串的最小值和最大值,而不是排序。

对我来说,计算机程序的优雅是速度和简单性的平衡。它不应该进行不必要的计算,并且应该足够简单以使其正确性显而易见。

我可以称排序解决方案“聪明”,但不能称其为“优雅”。

于 2011-02-21T14:26:26.547 回答
1

通常,使用成熟的开源库而不是自己编写会更优雅。然后,如果它不能完全满足您的需求,您可以对其进行扩展或修改以改进它,并让社区决定它是否属于该库。

diff-lcs对于最不常见的子字符串来说是一个很好的 Ruby gem。

于 2012-01-23T03:31:27.093 回答
1

这是在 Ruby 中使用正则表达式的解决方案:

def build_regex(string)
  arr = []
  arr << string.dup while string.chop!
  Regexp.new("^(#{arr.join("|")})")
end

def substring(first, *strings)
  strings.inject(first) do |accum, string|
    build_regex(accum).match(string)[0]
  end
end
于 2009-12-16T17:47:39.220 回答
1

我会做以下事情:

  1. 将数组的第一个字符串作为初始起始子字符串
  2. 获取数组的下一个字符串并比较字符,直到到达其中一个字符串的末尾或发现不匹配。如果发现不匹配,则将起始子字符串减少到发现不匹配的长度。
  3. 重复第 2 步,直到测试完所有字符串。

这是一个 JavaScript 实现:

var array = ["interspecies", "interstelar", "interstate"],
    prefix = array[0],
    len = prefix.length;
for (i=1; i<array.length; i++) {
    for (j=0, len=Math.min(len,array[j].length); j<len; j++) {
        if (prefix[j] != array[i][j]) {
            len = j;
            prefix = prefix.substr(0, len);
            break;
        }
    }
}
于 2009-12-16T18:18:08.240 回答
1

Golfed JS 解决方案只是为了好玩:

w=["hello", "hell", "helen"];
c=w.reduce(function(p,c){
    for(r="",i=0;p[i]==c[i];r+=p[i],i++){}
    return r;
});
于 2013-08-08T22:36:28.407 回答
1

我在 Java 中的解决方案:

public static String compute(Collection<String> strings) {
    if(strings.isEmpty()) return "";
    Set<Character> v = new HashSet<Character>();
    int i = 0;
    try {
        while(true) {
            for(String s : strings) v.add(s.charAt(i));
            if(v.size() > 1) break;
            v.clear();
            i++;
        }
    } catch(StringIndexOutOfBoundsException ex) {}
    return strings.iterator().next().substring(0, i);
}
于 2013-06-06T21:25:17.550 回答
0

我的Javascript解决方案:

IMOP,使用排序太棘手了。我的解决方案是通过循环数组逐字比较。如果字母没有被匹配,则返回字符串。

这是我的解决方案:

var longestCommonPrefix = function(strs){
    if(strs.length < 1){
        return '';
    }

    var p = 0, i = 0, c = strs[0][0];

    while(p < strs[i].length && strs[i][p] === c){
        i++;
        if(i === strs.length){
            i = 0;
            p++;
            c = strs[0][p];
        }
    }

    return strs[0].substr(0, p);
};
于 2015-04-01T09:47:26.580 回答
0

基于@Svante 算法的红宝石版本。运行速度大约是我的第一个的 3 倍。

 def common_prefix set 
   i=0
   rest=set[1..-1]
   set[0].each_byte{|c|
     rest.each{|e|return set[0][0...i] if e[i]!=c}
     i+=1
   }
   set
 end
于 2009-12-17T04:19:25.967 回答
0

这不是代码高尔夫,但你要求有点优雅,我倾向于认为递归很有趣。爪哇。

/** Recursively find the common prefix. */
public String findCommonPrefix(String[] strings) {

    int minLength = findMinLength(strings);

    if (isFirstCharacterSame(strings)) {
        return strings[0].charAt(0) + findCommonPrefix(removeFirstCharacter(strings));
    } else {
        return "";
    }
}

/** Get the minimum length of a string in strings[]. */
private int findMinLength(final String[] strings) {
    int length = strings[0].size();
    for (String string : strings) {
        if (string.size() < length) {
            length = string.size();
        }
    }
    return length;
}

/** Compare the first character of all strings. */
private boolean isFirstCharacterSame(String[] strings) {
    char c = string[0].charAt(0);
    for (String string : strings) {
        if (c != string.charAt(0)) return false;
    }

    return true;
}

/** Remove the first character of each string in the array, 
    and return a new array with the results. */
private String[] removeFirstCharacter(String[] source) {
    String[] result = new String[source.length];
    for (int i=0; i<result.length; i++) {
        result[i] = source[i].substring(1); 
    }
    return result;
}
于 2009-12-16T19:31:00.233 回答
0

这绝不是优雅的,但如果你想要简洁:

红宝石,71 个字符

def f(a)b=a[0];b[0,(0..b.size).find{|n|a.any?{|i|i[0,n]!=b[0,n]}}-1]end

如果你想要展开它,它看起来像这样:

def f(words)
  first_word = words[0];
  first_word[0, (0..(first_word.size)).find { |num_chars|
    words.any? { |word| word[0, num_chars] != first_word[0, num_chars] }
  } - 1]
end
于 2009-12-16T17:59:28.203 回答
0

AShelly的优秀答案的Javascript 克隆。

RequiresArray#reduce仅在 Firefox 中受支持。

var strings = ["interspecies", "intermediate", "interrogation"]
var sub = strings.reduce(function(l,r) { 
    while(l!=r.slice(0,l.length)) {  
        l = l.slice(0, -1);
    }
    return l;
});
于 2009-12-16T19:09:30.137 回答
0

意识到这会变成代码高尔夫比赛的风险(或者这是意图吗?),这是我使用的解决方案,从我对另一个 SO 问题的回答sed中复制并缩短为 36 个字符(其中 30 个是实际表达式)。它期望在标准输入或作为附加参数传递的文件中提供字符串(每个在单独的行上)。sed

sed 'N;s/^\(.*\).*\n\1.*$/\1\n\1/;D'

shebang 行中带有 sed 的脚本有 45 个字符:

#!/bin/sed -f
N;s/^\(.*\).*\n\1.*$/\1\n\1/;D

脚本的测试运行(名为longestprefix),字符串作为“此处文档”提供:

$ ./longestprefix <<EOF
> interspecies
> interstelar
> interstate
> EOF
inters
$
于 2015-12-12T18:07:45.797 回答
0

红宝石

require 'abbrev'

ar = ["interspecies", "interstelar", "interstate"]
ar.abbrev.keys.min_by(&:size).chop # => "inters"

给定一组字符串,abbrev计算这些字符串的明确缩写集,并返回一个哈希,其中键是所有可能的缩写(值是完整的字符串)。最短的键减去最后一个字符将是公共前缀.

于 2015-12-12T20:59:38.913 回答