C'est un défi de trouver le JavaScript le plus élégant, Ruby ou une autre solution à un problème relativement trivial.
Ce problème est un cas plus spécifique du problème de sous-chaîne commun le plus long . J'ai seulement besoin de trouver la plus longue sous-chaîne commune de départ dans un tableau. Cela simplifie considérablement le problème.
Par exemple, la sous-chaîne la plus longue dans [interspecies, interstelar, interstate]
est "inters". Cependant, je n'ai pas besoin de trouver "ific" dans [specifics, terrific]
.
J'ai résolu le problème en codant rapidement une solution en JavaScript dans le cadre de mon réponse sur la complétion de tabulation de type Shell ( page de test ici ). Voici cette solution, légèrement modifiée:
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)
}
Ce le code est disponible dans ce Gist avec une solution similaire dans Ruby. Vous pouvez cloner le Gist comme un dépôt git pour l'essayer:
$ git clone git://Gist.github.com/257891.git substring-challenge
Je ne suis pas très satisfait de ces solutions. J'ai le sentiment qu'ils pourraient être résolus avec plus d'élégance et moins de complexité d'exécution - c'est pourquoi je poste ce défi.
Je vais accepter comme réponse la solution que je trouve la plus élégante ou concise. Voici, par exemple, un fou Ruby hack que je propose — définissant le &
opérateur sur 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
Les solutions en JavaScript ou Ruby sont préférées, mais vous pouvez montrer une solution intelligente dans d'autres langues tant que vous expliquez ce qui se passe. Seul le code de la bibliothèque standard s'il vous plaît.
J'ai choisi la solution de tri JavaScript par kennebec comme "réponse" car elle m'a paru à la fois inattendue et géniale. Si nous ne tenons pas compte de la complexité du tri réel (imaginons qu'il soit optimisé à l'infini par l'implémentation du langage), la complexité de la solution consiste simplement à comparer deux chaînes.
Autres excellentes solutions:
commonprefix
en Python - Roberto Bonvallet a utilisé une fonctionnalité conçue pour gérer les chemins des systèmes de fichiers pour résoudre ce problèmeMerci d'avoir participé! Comme vous pouvez le voir dans les commentaires, j'ai beaucoup appris (même sur Ruby).
C'est une question de goût, mais il s'agit d'une simple version javascript: il trie le tableau, puis ne regarde que les premier et dernier éléments.
// plus longue sous-chaîne de départ commune dans un tableau
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);
}
DÉMOS
sharedStart(['interspecies', 'interstelar', 'interstate']) //=> 'inters'
sharedStart(['throne', 'throne']) //=> 'throne'
sharedStart(['throne', 'dungeon']) //=> ''
sharedStart(['cheese']) //=> 'cheese'
sharedStart([]) //=> ''
sharedStart(['prefix', 'suffix']) //=> ''
En Python:
>>> from os.path import commonprefix
>>> commonprefix('interspecies interstelar interstate'.split())
'inters'
Ruby one-liner:
l=strings.inject{|l,s| l=l.chop while l!=s[0...l.length];l}
Il vous suffit de parcourir toutes les chaînes jusqu'à ce qu'elles diffèrent, puis de prendre la sous-chaîne jusqu'à ce point.
Pseudocode:
loop for i upfrom 0
while all strings[i] are equal
finally return substring[0..i]
LISP commun:
(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))))
Mon doublure Haskell:
import Data.List
commonPre :: [String] -> String
commonPre = map head . takeWhile (\(x:xs)-> all (==x) xs) . transpose
EDIT: barkmadley a donné une bonne explication du code ci-dessous. J'ajouterais également que haskell utilise l'évaluation paresseuse, afin que nous puissions être paresseux quant à notre utilisation de transpose
; il ne transposera nos listes que dans la mesure nécessaire pour retrouver la fin du préfixe commun.
Encore une autre façon de le faire: utilisez la cupidité regex.
words = %w(interspecies interstelar interstate)
j = '='
str = ['', *words].join(j)
re = "[^#{j}]*"
str =~ /\A
(?: #{j} ( #{re} ) #{re} )
(?: #{j} \1 #{re} )*
\z/x
p $1
Et le one-liner, gracieuseté de mislav (50 caractères):
p ARGV.join(' ').match(/^(\w*)\w*(?: \1\w*)*$/)[1]
En Python je n'utiliserais rien d'autre que la fonction commonprefix
existante que j'ai montrée dans une autre réponse, mais je n'ai pas pu m'empêcher de réinventer la roue :P
. Voici mon approche basée sur les itérateurs:
>>> 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'
Edit: Explication de la façon dont cela fonctionne.
Zip
génère des tuples d'éléments en prenant un de chaque élément de a
à la fois:
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')]
En mappant set
sur ces éléments, j'obtiens une série de lettres uniques:
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)
en prend des éléments alors que le prédicat est True; dans ce cas particulier, lorsque les set
s ont un élément, c'est-à-dire que tous les mots ont la même lettre à cette position:
In [8]: list(takewhile(lambda s: len(s) == 1, _))
Out[8]:
[set(['i']),
set(['n']),
set(['t']),
set(['e']),
set(['r']),
set(['s'])]
À ce stade, nous avons un ensemble d'itérations, contenant chacun une lettre du préfixe que nous recherchions. Pour construire la chaîne, nous chain
les en un seul itérable, à partir duquel nous obtenons les lettres de join
dans la chaîne finale.
La magie de l'utilisation des itérateurs est que tous les éléments sont générés à la demande, donc lorsque takewhile
cesse de demander des éléments, le zipping s'arrête à ce point et aucun travail inutile n'est effectué. Chaque appel de fonction dans mon one-liner a un for
implicite et un break
implicite.
Ce n'est probablement pas la solution la plus concise (cela dépend si vous avez déjà une bibliothèque pour cela), mais une méthode élégante consiste à utiliser un trie. J'utilise des essais pour implémenter la complétion d'onglets dans mon interpréteur de schéma:
http://github.com/jcoglan/heist/blob/master/lib/trie.rb
Par exemple:
tree = Trie.new
%w[interspecies interstelar interstate].each { |s| tree[s] = true }
tree.longest_prefix('')
#=> "inters"
Je les utilise également pour faire correspondre les noms de canaux avec des caractères génériques pour le protocole de Bayeux; voir ces:
http://github.com/jcoglan/faye/blob/master/client/channel.js
http://github.com/jcoglan/faye/blob/master/lib/faye/channel.rb
Juste pour le plaisir, voici une version écrite en (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).
Fonctionnement:
?- S=["interspecies", "interstelar", "interstate"], common_pre(S, CP), string_to_list(CPString, CP).
Donne:
CP = [105, 110, 116, 101, 114, 115],
CPString = "inters".
Explication:
(SWI-) PROLOG traite les chaînes comme des listes de codes de caractères (nombres). Tous les prédicats common_pre/2
does est une correspondance de modèle récursive pour sélectionner le premier code (C
) dans la tête de la première liste (chaîne, [C|Cs]
) dans la liste de toutes les listes (toutes les chaînes, [[C|Cs]|Ss]
), et ajoute le code correspondant C
au résultat iff il est commun à toutes les têtes (restantes) de toutes les listes (chaînes), sinon il se termine.
Agréable, propre, simple et efficace ... :)
Une version javascript basée sur algorithme de @ Svante :
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;
}
La combinaison des réponses par kennebec, Florian F et jberryman donne le one-liner Haskell suivant:
commonPrefix l = map fst . takeWhile (uncurry (==)) $ Zip (minimum l) (maximum l)
Avec Control.Arrow
on peut obtenir un formulaire sans point:
commonPrefix = map fst . takeWhile (uncurry (==)) . uncurry Zip . (minimum &&& maximum)
Celui-ci est très similaire à la solution de Roberto Bonvallet, sauf dans Ruby.
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
La première ligne remplace chaque mot par un tableau de caractères. Ensuite, j'utilise Zip
pour créer cette structure de données:
[["i", "i", "i"], ["n", "n", "n"], ["t", "t", "t"], ...
map
et uniq
réduisez cela à [["i"],["n"],["t"], ...
take_while
extrait les caractères du tableau jusqu'à ce qu'il en trouve un dont la taille n'est pas un (ce qui signifie que tous les caractères n'étaient pas les mêmes). Enfin, je join
les rassemble.
solution acceptée est cassé (par exemple, il renvoie a
pour des chaînes comme ['a', 'ba']
). Le correctif est très simple, vous devez littéralement changer seulement 3 caractères (de indexOf(tem1) == -1
à indexOf(tem1) != 0
) et la fonction fonctionnerait comme prévu.
Malheureusement, lorsque j'ai essayé de modifier la réponse pour corriger la faute de frappe, SO m'a dit que "les modifications doivent comporter au moins 6 caractères". Je pouvais changez plus que ces 3 caractères, en améliorant le nommage et la lisibilité, mais cela semble un peu trop.
Voici donc une version fixe et améliorée (du moins de mon point de vue) de la solution de 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;
}
(le jsFiddle )
Notez qu'il utilise la méthode réduire (JavaScript 1.8) afin de trouver le max/min alphanumérique au lieu de trier le tableau puis de récupérer le premier et le dernier élément de celui-ci.
Cela ne semble pas si compliqué si vous n'êtes pas trop préoccupé par les performances ultimes:
def common_substring(data)
data.inject { |m, s| s[0,(0..m.length).find { |i| m[i] != s[i] }.to_i] }
end
L'une des caractéristiques utiles de l'injection est la capacité de pré-amorçage avec le premier élément du réseau interagi. Cela évite le contrôle de mémo nul.
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"
Mise à jour: Si le golf est le jeu ici, alors 67 caractères:
def f(d)d.inject{|m,s|s[0,(0..m.size).find{|i|m[i]!=s[i]}.to_i]}end
En lisant ces réponses avec toute la programmation fonctionnelle de fantaisie, le tri et les expressions rationnelles et ainsi de suite, je me suis juste dit: qu'est-ce qui ne va pas un petit C? Voici donc un petit programme loufoque.
#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]);
}
Compilez-le, exécutez-le avec votre liste de chaînes comme arguments de ligne de commande, puis votez pour l'utilisation de goto
!
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)))
, le nombre de recherches maximum ne peut pas être supérieur à la longueur de la chaîne la plus courte; dans cet exemple, cela équivaudrait à [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
len(set(map(lambda e: e[i], a)))
, 1) créez un tableau du caractère i-th
pour chaque chaîne de la liste; 2) en faire un ensemble; 3) déterminer la taille de l'ensemble
[i for i in range(min(map(len, a))) if len(set(map(lambda e: e[i], a))) == 1]
, n'inclut que les caractères pour lesquels la taille de l'ensemble est 1 (tous les caractères à cette position étaient les mêmes ..); ici, il serait évalué à [0, 1, 2, 3, 4, 5]
enfin prenez le max
, ajoutez-en un, et obtenez la sous-chaîne ...
Remarque: ce qui précède ne fonctionne pas pour a = ['intersyate', 'intersxate', 'interstate', 'intersrate']
, Mais cela:
>>> 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
Solution JS golfée juste pour le plaisir:
w=["hello", "hell", "helen"];
c=w.reduce(function(p,c){
for(r="",i=0;p[i]==c[i];r+=p[i],i++){}
return r;
});
Voici une solution efficace dans Ruby. J'ai basé l'idée de la stratégie d'un jeu de devinettes hi/lo où vous vous concentrez itérativement sur le préfixe le plus long.
Quelqu'un me corrige si je me trompe, mais je pense que la complexité est O (n log n), où n est la longueur de la chaîne la plus courte et le nombre de chaînes est considéré comme une constante.
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
Et quelques vérifications que cela fonctionne:
>> common %w{ interspecies interstelar interstate }
=> "inters"
>> common %w{ dog dalmation }
=> "d"
>> common %w{ asdf qwerty }
=> ""
>> common ['', 'asdf']
=> ""
Au lieu de trier, vous pouvez simplement obtenir le min et le max des chaînes.
Pour moi, l'élégance dans un programme informatique est un équilibre entre rapidité et simplicité. Il ne doit pas faire de calcul inutile et il doit être suffisamment simple pour rendre son exactitude évidente.
Je pourrais appeler la solution de tri "intelligente", mais pas "élégante".
Alternative amusante Ruby solution:
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' ) #=> ""
Cela peut aider à accélérer si vous utilisez chars = strings.sort_by(&:length).map(&:chars)
, car plus la première chaîne est courte, plus les tableaux créés par Zip
sont courts. Cependant, si vous vous souciez de la vitesse, vous ne devriez probablement pas utiliser cette solution de toute façon. :)
Ma solution en 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);
}
Je ferais ce qui suit:
Voici une implémentation 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;
}
}
}
Voici une solution utilisant des expressions régulières dans 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
Souvent, il est plus élégant d'utiliser une bibliothèque open source mature au lieu de rouler la vôtre. Ensuite, s'il ne répond pas complètement à vos besoins, vous pouvez l'étendre ou le modifier pour l'améliorer, et laisser la communauté décider si cela appartient à la bibliothèque.
diff-lcs est une bonne gemme Ruby pour la sous-chaîne la moins courante).
Ma solution Javascript:
IMOP, l'utilisation du tri est trop délicate. Ma solution consiste à comparer lettre par lettre en bouclant le tableau. Renvoie une chaîne si la lettre n'est pas remplacée.
Voici ma solution:
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);
};
Rubis
require 'abbrev'
ar = ["interspecies", "interstelar", "interstate"]
ar.abbrev.keys.min_by(&:size).chop # => "inters"
Étant donné un ensemble de chaînes, abbrev
calcule l'ensemble des abréviations non ambiguës pour ces chaînes et renvoie un hachage où les clés sont toutes les abréviations possibles (et les valeurs sont les chaînes complètes). La clé la plus courte moins la dernière char sera le préfixe commun.
Ce n'est en aucun cas élégant, mais si vous voulez être concis:
def f(a)b=a[0];b[0,(0..b.size).find{|n|a.any?{|i|i[0,n]!=b[0,n]}}-1]end
Si vous voulez que cela se déroule, cela ressemble à ceci:
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
Réalisant le risque que cela se transforme en une correspondance de code golf (ou est-ce l'intention?), Voici ma solution en utilisant sed
, copiée de ma réponse à un autre SO question et raccourci à 36 caractères (dont 30 sont l'expression réelle sed
). Il s'attend à ce que les chaînes (chacune sur une ligne distincte) soient fournies en standard entrée ou dans des fichiers passés comme arguments supplémentaires.
sed 'N;s/^\(.*\).*\n\1.*$/\1\n\1/;D'
Un script avec sed dans la ligne Shebang pèse 45 caractères:
#!/bin/sed -f
N;s/^\(.*\).*\n\1.*$/\1\n\1/;D
Une exécution de test du script (nommé longestprefix
), avec des chaînes fournies en tant que "document ici":
$ ./longestprefix <<EOF
> interspecies
> interstelar
> interstate
> EOF
inters
$
Ce n'est pas du golf de code, mais vous avez demandé quelque chose d'élégant, et j'ai tendance à penser que la récursivité est amusante. 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;
}
A Ruby version basée sur l'algorithme de @ Svante. Fonctionne ~ 3 fois plus vite que mon premier.
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
Javascript clone de AShelly excellente réponse.
A besoin Array#reduce
qui n'est pris en charge que dans 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;
});