在一组字符串中查找最长的公共起始子字符串[关闭]
- 2025-04-10 09:46:00
- admin 原创
- 18
问题描述:
这是一个挑战,需要用最优雅的 JavaScript、Ruby 或其他语言来解决一个相对简单的问题。
这个问题是“最长公共子串问题”的一个更具体的例子。我只需要在数组中找到最长的公共起始子串。这大大简化了问题。
例如, 中最长的子字符串[interspecies, interstelar, interstate]
是“inters”。但是,我不需要在 中找到“ific” [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 技巧 —&
在 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 的解决方案,但您可以展示使用其他语言的巧妙解决方案,只要您能解释清楚发生了什么。请仅使用标准库中的代码。
更新:我最喜欢的解决方案
我选择了Kennebec的JavaScript 排序解决方案作为“答案”,因为它让我既感到意外又觉得很天才。如果我们忽略实际排序的复杂性(假设它被语言实现无限优化),解决方案的复杂性仅仅是比较两个字符串。
其他出色的解决方案:
FM 的“regex greed”需要一两分钟才能理解,但随后它的优雅就会打动你。Yehuda Katz 也提出了一个正则表达式解决方案,但它更复杂
commonprefix
在 Python 中— Roberto Bonvallet 使用处理文件系统路径的功能来解决此问题Haskell 单行代码很短,就像被压缩了一样,而且很漂亮
简单易懂的 Ruby 单行代码
感谢您的参与!正如您从评论中看到的那样,我学到了很多东西(甚至关于 Ruby)。
解决方案 1:
这只是个人喜好问题,但这是一个简单的 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']) //=> ''
解决方案 2:
在 Python 中:
>>> from os.path import commonprefix
>>> commonprefix('interspecies interstelar interstate'.split())
'inters'
解决方案 3:
Ruby 单行代码:
l=strings.inject{|l,s| l=l.chop while l!=s[0...l.length];l}
解决方案 4:
我的 Haskell 单行代码:
import Data.List
commonPre :: [String] -> String
commonPre = map head . takeWhile ((x:xs)-> all (==x) xs) . transpose
编辑:barkmadley 对下面的代码进行了很好的解释。我还想补充一点,haskell 使用惰性求值,因此我们可以懒得使用transpose
;它只会根据需要转置我们的列表以找到公共前缀的结尾。
解决方案 5:
您只需遍历所有字符串,直到它们不同,然后取到该点的子字符串。
伪代码:
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))))
解决方案 6:
还有另一种方法:使用正则表达式贪婪。
words = %w(interspecies interstelar interstate)
j = '='
str = ['', *words].join(j)
re = "[^#{j}]*"
str =~ /A
(?: #{j} ( #{re} ) #{re} )
(?: #{j} #{re} )*
z/x
p $1
以下是 mislav 提供的俏皮话(50 个字符):
p ARGV.join(' ').match(/^(w*)w*(?: w*)*$/)[1]
解决方案 7:
在 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 时,从中获取元素;在这种特殊情况下,当set
s 有一个元素时,即所有单词在该位置都有相同的字母:
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
。
解决方案 8:
这可能不是最简洁的解决方案(取决于你是否已经有了相关库),但一种优雅的方法是使用 trie。我使用 tries 在我的 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
解决方案 9:
只是为了好玩,这里有一个用 (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
漂亮,干净,简单,高效... :)
解决方案 10:
基于@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;
}
解决方案 11:
结合kennebec、Florian F和jberryman的答案,得出以下 Haskell 单行代码:
commonPrefix l = map fst . takeWhile (uncurry (==)) $ zip (minimum l) (maximum l)
可以Control.Arrow
得到一个无点形式:
commonPrefix = map fst . takeWhile (uncurry (==)) . uncurry zip . (minimum &&& maximum)
解决方案 12:
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)))
,1)为列表中的每个字符串创建一个字符数组i-th
;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
解决方案 13:
如果你不太关心最终的性能,那么看起来并不复杂:
def common_substring(data)
data.inject { |m, s| s[0,(0..m.length).find { |i| m[i] != s[i] }.to_i] }
end
注入的一个有用功能是能够预先植入数组中要迭代的第一个元素。这可以避免 nil 备忘录检查。
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
解决方案 14:
这个解决方案与 Roberto Bonvallet 的解决方案非常相似,只是使用了红宝石。
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
将字符从数组中拉出,直到找到一个大小不为 1 的字符(即并非所有字符都相同)。最后,我join
将它们重新组合在一起。
解决方案 15:
已接受的解决方案有问题(例如,它返回a
类似 的字符串['a', 'ba']
)。修复非常简单,您只需更改 3 个字符(从indexOf(tem1) == -1
到indexOf(tem1) != 0
),该函数就会按预期工作。
不幸的是,当我尝试编辑答案以修复拼写错误时,SO 告诉我“编辑必须至少 6 个字符”。我可以通过改进命名和可读性来更改超过这 3 个字符,但这感觉有点太多了。
因此,下面是 Kennebec 解决方案的修复和改进版本(至少从我的角度来看):
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)来查找字母数字最大值/最小值,而不是对数组进行排序,然后获取它的第一个和最后一个元素。
解决方案 16:
在阅读这些关于函数式编程、排序、正则表达式等各种花哨内容的答案时,我只是在想: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
", i, argv[1]);
}
编译它,使用字符串列表作为命令行参数来运行它,然后对我的使用表示赞同goto
!
解决方案 17:
以下是使用 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
解决方案 18:
我会做以下事情:
将数组的第一个字符串作为初始的起始子字符串。
取出数组中的下一个字符串并比较字符,直到到达其中一个字符串的末尾或发现不匹配。如果发现不匹配,则将起始子字符串缩短为发现不匹配的长度。
重复步骤 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;
}
}
}
解决方案 19:
您无需排序,只需获取字符串的最小值和最大值即可。
对我来说,计算机程序的优雅在于速度和简单之间的平衡。它不应该进行不必要的计算,并且应该足够简单,以使其正确性显而易见。
我可以称该排序解决方案为“聪明”,但不能称其为“优雅”。
解决方案 20:
通常,使用成熟的开源库比自己开发更为优雅。然后,如果它不能完全满足您的需求,您可以扩展或修改它以改进它,并让社区决定它是否属于该库。
diff-lcs是一款用于最小公共子串的优秀 Ruby 库。
解决方案 21:
我在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);
}
解决方案 22:
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;
});
解决方案 23:
这是 ruby 中的一个有效解决方案。我的想法是基于高/低猜谜游戏的策略,在这个游戏中,你反复地将注意力集中在最长的前缀上。
如果我错了,请纠正我,但我认为复杂度是 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']
=> ""
解决方案 24:
有趣的替代 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
。但是,如果您关心速度,您可能不应该使用此解决方案。:)
解决方案 25:
AShelly的优秀答案的 Javascript 克隆。
需要Array#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;
});
解决方案 26:
这绝不是优雅的,但如果你想要简洁:
Ruby,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
解决方案 27:
这不是代码高尔夫,但你要求有点优雅,我倾向于认为递归很有趣。Java。
/** 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;
}
解决方案 28:
基于 @Svante 算法的 ruby 版本。运行速度比我的第一个版本快 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
解决方案 29:
我的Javascript解决方案:
在我看来,使用排序太棘手了。我的解决方案是通过循环数组逐个字母进行比较。如果字母不匹配,则返回字符串。
这是我的解决方案:
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);
};
解决方案 30:
意识到这有变成一场代码高尔夫比赛的风险(或者这是故意的?),这是我使用的解决方案sed
,从我对另一个 SO 问题的回答中复制并缩短为 36 个字符(其中 30 个是实际sed
表达式)。它期望字符串(每个在单独的行上)在标准输入中提供,或者在作为附加参数传递的文件中提供。
sed 'N;s/^(.*).*
.*$/
/;D'
在 shebang 行中使用 sed 的脚本重 45 个字符:
#!/bin/sed -f
N;s/^(.*).*
.*$/
/;D
对脚本(名为longestprefix
)进行测试运行,并使用“此处文档”提供的字符串:
$ ./longestprefix <<EOF
> interspecies
> interstelar
> interstate
> EOF
inters
$
扫码咨询,免费领取项目管理大礼包!