私は4つの弦を持っています:
"h:/a/b/c"
"h:/a/b/d"
"h:/a/b/e"
"h:/a/c"
これらの文字列に共通のプレフィックス、つまり"h:/a"
を見つけたいと思います。それを見つける方法は?
通常、文字列を区切り文字'/'
で分割し、別のリストに配置します。
それを行うためのより良い方法はありますか?
string[] xs = new[] { "h:/a/b/c", "h:/a/b/d", "h:/a/b/e", "h:/a/c" };
string x = string.Join("/", xs.Select(s => s.Split('/').AsEnumerable())
.Transpose()
.TakeWhile(s => s.All(d => d == s.First()))
.Select(s => s.First()));
と
public static IEnumerable<IEnumerable<T>> Transpose<T>(
this IEnumerable<IEnumerable<T>> source)
{
var enumerators = source.Select(e => e.GetEnumerator()).ToArray();
try
{
while (enumerators.All(e => e.MoveNext()))
{
yield return enumerators.Select(e => e.Current).ToArray();
}
}
finally
{
Array.ForEach(enumerators, e => e.Dispose());
}
}
私の短いLINQyソリューション。
var samples = new[] { "h:/a/b/c", "h:/a/b/d", "h:/a/b/e", "h:/a/e" };
var commonPrefix = new string(
samples.First().Substring(0, samples.Min(s => s.Length))
.TakeWhile((c, i) => samples.All(s => s[i] == c)).ToArray());
最短の文字列の文字をループして、各文字を他の文字列の同じ位置にある文字と比較するだけです。それらがすべて一致している間、続けます。一致しない場合はすぐに、現在の位置-1までの文字列が答えになります。
(擬似コード)のようなもの
int count=0;
foreach(char c in shortestString)
{
foreach(string s in otherStrings)
{
if (s[count]!=c)
{
return shortestString.SubString(0,count-1); //need to check count is not 0
}
}
count+=1;
}
return shortestString;
サムホルダーのソリューションに基づく作業コード(質問で最も長い共通の初期部分文字列としてh:/ aではなくh:/ a /を与えることに注意してください):
using System;
namespace CommonPrefix
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(CommonPrefix(new[] { "h:/a/b/c", "h:/a/b/d", "h:/a/b/e", "h:/a/c" })); // "h:/a/"
Console.WriteLine(CommonPrefix(new[] { "abc", "abc" })); // "abc"
Console.WriteLine(CommonPrefix(new[] { "abc" })); // "abc"
Console.WriteLine(CommonPrefix(new string[] { })); // ""
Console.WriteLine(CommonPrefix(new[] { "a", "abc" })); // "a"
Console.WriteLine(CommonPrefix(new[] { "abc", "a" })); // "a"
Console.ReadKey();
}
private static string CommonPrefix(string[] ss)
{
if (ss.Length == 0)
{
return "";
}
if (ss.Length == 1)
{
return ss[0];
}
int prefixLength = 0;
foreach (char c in ss[0])
{
foreach (string s in ss)
{
if (s.Length <= prefixLength || s[prefixLength] != c)
{
return ss[0].Substring(0, prefixLength);
}
}
prefixLength++;
}
return ss[0]; // all strings identical up to length of ss[0]
}
}
}
これは 最長の一般的な部分文字列 の問題です(ただし、プレフィックスのみを気にしているように見えるため、少し特殊なケースです)。 .NETプラットフォームには、直接呼び出すことができるアルゴリズムのライブラリ実装はありませんが、ここにリンクされている記事には、自分でそれを行う方法の手順がぎっしり詰まっています。
共通の文字列プレフィックスが必要でしたが、任意の文字(/など)を含めたいので、テストで読み取れるだけのパフォーマンスの高い/派手なものは必要ありませんでした。だから私はこれを持っています: https://github.com/fschwiet/DreamNJasmine/commit/ad802611ceacc673f2d03c30f7c8199f552b586f
public class CommonStringPrefix
{
public static string Of(IEnumerable<string> strings)
{
var commonPrefix = strings.FirstOrDefault() ?? "";
foreach(var s in strings)
{
var potentialMatchLength = Math.Min(s.Length, commonPrefix.Length);
if (potentialMatchLength < commonPrefix.Length)
commonPrefix = commonPrefix.Substring(0, potentialMatchLength);
for(var i = 0; i < potentialMatchLength; i++)
{
if (s[i] != commonPrefix[i])
{
commonPrefix = commonPrefix.Substring(0, i);
break;
}
}
}
return commonPrefix;
}
}
これは、c#でのtrieアルゴリズムのカスタム実装です( http://en.wikipedia.org/wiki/Trie )。プレフィックスを介してインデックス付き文字列を実行するために使用されます。このクラスにはO(1)リーフノードの書き込みと読み取りがあります。プレフィックス検索の場合、パフォーマンスはO(log n)ですが、プレフィックスの結果のカウントはO(1)です。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public class StringIndex
{
private Dictionary<char, Item> _rootChars;
public StringIndex()
{
_rootChars = new Dictionary<char, Item>();
}
public void Add(string value, string data)
{
int level = 0;
Dictionary<char, Item> currentChars = _rootChars;
Item currentItem = null;
foreach (char c in value)
{
if (currentChars.ContainsKey(c))
{
currentItem = currentChars[c];
}
else
{
currentItem = new Item() { Level = level, Letter = c };
currentChars.Add(c, currentItem);
}
currentChars = currentItem.Items;
level++;
}
if (!currentItem.Values.Contains(data))
{
currentItem.Values.Add(data);
IncrementCount(value);
}
}
private void IncrementCount(string value)
{
Dictionary<char, Item> currentChars = _rootChars;
Item currentItem = null;
foreach (char c in value)
{
currentItem = currentChars[c];
currentItem.Total++;
currentChars = currentItem.Items;
}
}
public void Remove(string value, string data)
{
Dictionary<char, Item> currentChars = _rootChars;
Dictionary<char, Item> parentChars = null;
Item currentItem = null;
foreach (char c in value)
{
if (currentChars.ContainsKey(c))
{
currentItem = currentChars[c];
parentChars = currentChars;
currentChars = currentItem.Items;
}
else
{
return; // no matches found
}
}
if (currentItem.Values.Contains(data))
{
currentItem.Values.Remove(data);
DecrementCount(value);
if (currentItem.Total == 0)
{
parentChars.Remove(currentItem.Letter);
}
}
}
private void DecrementCount(string value)
{
Dictionary<char, Item> currentChars = _rootChars;
Item currentItem = null;
foreach (char c in value)
{
currentItem = currentChars[c];
currentItem.Total--;
currentChars = currentItem.Items;
}
}
public void Clear()
{
_rootChars.Clear();
}
public int GetValuesByPrefixCount(string prefix)
{
int valuescount = 0;
int level = 0;
Dictionary<char, Item> currentChars = _rootChars;
Item currentItem = null;
foreach (char c in prefix)
{
if (currentChars.ContainsKey(c))
{
currentItem = currentChars[c];
currentChars = currentItem.Items;
}
else
{
return valuescount; // no matches found
}
level++;
}
valuescount = currentItem.Total;
return valuescount;
}
public HashSet<string> GetValuesByPrefixFlattened(string prefix)
{
var results = GetValuesByPrefix(prefix);
return new HashSet<string>(results.SelectMany(x => x));
}
public List<HashSet<string>> GetValuesByPrefix(string prefix)
{
var values = new List<HashSet<string>>();
int level = 0;
Dictionary<char, Item> currentChars = _rootChars;
Item currentItem = null;
foreach (char c in prefix)
{
if (currentChars.ContainsKey(c))
{
currentItem = currentChars[c];
currentChars = currentItem.Items;
}
else
{
return values; // no matches found
}
level++;
}
ExtractValues(values, currentItem);
return values;
}
public void ExtractValues(List<HashSet<string>> values, Item item)
{
foreach (Item subitem in item.Items.Values)
{
ExtractValues(values, subitem);
}
values.Add(item.Values);
}
public class Item
{
public int Level { get; set; }
public char Letter { get; set; }
public int Total { get; set; }
public HashSet<string> Values { get; set; }
public Dictionary<char, Item> Items { get; set; }
public Item()
{
Values = new HashSet<string>();
Items = new Dictionary<char, Item>();
}
}
}
このクラスの使用方法の単体テストとサンプルコードは次のとおりです。
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class StringIndexTest
{
[TestMethod]
public void AddAndSearchValues()
{
var si = new StringIndex();
si.Add("abcdef", "1");
si.Add("abcdeff", "2");
si.Add("abcdeffg", "3");
si.Add("bcdef", "4");
si.Add("bcdefg", "5");
si.Add("cdefg", "6");
si.Add("cdefgh", "7");
var output = si.GetValuesByPrefixFlattened("abc");
Assert.IsTrue(output.Contains("1") && output.Contains("2") && output.Contains("3"));
}
[TestMethod]
public void RemoveAndSearch()
{
var si = new StringIndex();
si.Add("abcdef", "1");
si.Add("abcdeff", "2");
si.Add("abcdeffg", "3");
si.Add("bcdef", "4");
si.Add("bcdefg", "5");
si.Add("cdefg", "6");
si.Add("cdefgh", "7");
si.Remove("abcdef", "1");
var output = si.GetValuesByPrefixFlattened("abc");
Assert.IsTrue(!output.Contains("1") && output.Contains("2") && output.Contains("3"));
}
[TestMethod]
public void Clear()
{
var si = new StringIndex();
si.Add("abcdef", "1");
si.Add("abcdeff", "2");
si.Add("abcdeffg", "3");
si.Add("bcdef", "4");
si.Add("bcdefg", "5");
si.Add("cdefg", "6");
si.Add("cdefgh", "7");
si.Clear();
var output = si.GetValuesByPrefix("abc");
Assert.IsTrue(output.Count == 0);
}
[TestMethod]
public void AddAndSearchValuesCount()
{
var si = new StringIndex();
si.Add("abcdef", "1");
si.Add("abcdeff", "2");
si.Add("abcdeffg", "3");
si.Add("bcdef", "4");
si.Add("bcdefg", "5");
si.Add("cdefg", "6");
si.Add("cdefgh", "7");
si.Remove("cdefgh", "7");
var output1 = si.GetValuesByPrefixCount("abc");
var output2 = si.GetValuesByPrefixCount("b");
var output3 = si.GetValuesByPrefixCount("bc");
var output4 = si.GetValuesByPrefixCount("ca");
Assert.IsTrue(output1 == 3 && output2 == 2 && output3 == 2 && output4 == 0);
}
}
このクラスを改善する方法についての提案は大歓迎です:)
私のアプローチは、最初の文字列を取ることです。他のすべての文字列が同じインデックス位置で同じ文字を取得している間に文字ごとに取得し、一致するものがない場合は停止します。区切り文字の場合は、最後の文字を削除します。
var str_array = new string[]{"h:/a/b/c",
"h:/a/b/d",
"h:/a/b/e",
"h:/a/c"};
var separator = '/';
// get longest common prefix (optinally use ToLowerInvariant)
var ret = str_array.Any()
? str_array.First().TakeWhile((s,i) =>
str_array.All(e =>
Char.ToLowerInvariant(s) == Char.ToLowerInvariant(e.Skip(i).Take(1).SingleOrDefault())))
: String.Empty;
// remove last character if it's a separator (optional)
if (ret.LastOrDefault() == separator)
ret = ret.Take(ret.Count() -1);
string prefix = new String(ret.ToArray());
異なる文字列で最も長い共通のプレフィックスを探す必要がありました。私は思いついた:
private string FindCommonPrefix(List<string> list)
{
List<string> prefixes = null;
for (int len = 1; ; ++len)
{
var x = list.Where(s => s.Length >= len)
.GroupBy(c => c.Substring(0,len))
.Where(grp => grp.Count() > 1)
.Select(grp => grp.Key)
.ToList();
if (!x.Any())
{
break;
}
// Copy last list
prefixes = new List<string>(x);
}
return prefixes == null ? string.Empty : prefixes.First();
}
同じ長さのプレフィックスが複数ある場合は、最初に見つかったプレフィックスを任意に返します。また、大文字と小文字が区別されます。これらの両方の点は、読者が対処することができます!
このICollection拡張機能を作成して、Webアドレスのコレクションから最長の共通ベースURLを見つけました。
スラッシュごとに文字列のコレクションをチェックするだけなので、一般的なプレフィックスルーチンよりも少し速くなります(私の非効率的なアルゴリズムは数えません!)。冗長ですが、簡単に理解できます...私のお気に入りのタイプのコード;-)
'http://'と 'https://'、および大文字小文字を無視します。
/// <summary>
/// Resolves a common base Uri from a list of Uri strings. Ignores case. Includes the last slash
/// </summary>
/// <param name="collectionOfUriStrings"></param>
/// <returns>Common root in lowercase</returns>
public static string GetCommonUri(this ICollection<string> collectionOfUriStrings)
{
//Check that collection contains entries
if (!collectionOfUriStrings.Any())
return string.Empty;
//Check that the first is no zero length
var firstUri = collectionOfUriStrings.FirstOrDefault();
if(string.IsNullOrEmpty(firstUri))
return string.Empty;
//set starting position to be passed '://'
int previousSlashPos = firstUri.IndexOf("://", StringComparison.OrdinalIgnoreCase) + 2;
int minPos = previousSlashPos + 1; //results return must have a slash after this initial position
int nextSlashPos = firstUri.IndexOf("/", previousSlashPos + 1, StringComparison.OrdinalIgnoreCase);
//check if any slashes
if (nextSlashPos == -1)
return string.Empty;
do
{
string common = firstUri.Substring(0, nextSlashPos + 1);
//check against whole collection
foreach (var collectionOfUriString in collectionOfUriStrings)
{
if (!collectionOfUriString.StartsWith(common, StringComparison.OrdinalIgnoreCase))
{
//return as soon as a mismatch is found
return previousSlashPos > minPos ? firstUri.Substring(0, previousSlashPos + 1).ToLower() : string.Empty ;
}
}
previousSlashPos = nextSlashPos;
nextSlashPos = firstUri.IndexOf("/", previousSlashPos + 1, StringComparison.OrdinalIgnoreCase);
} while (nextSlashPos != -1);
return previousSlashPos > minPos ? firstUri.Substring(0, previousSlashPos + 1).ToLower() : string.Empty;
}
イェゴールの答えの改善
var samples = new[] { "h:/a/b/c", "h:/a/b/d", "h:/a/b/e", "h:/a/e" };
var commonPrefix = new string(
samples.Min().TakeWhile((c, i) => samples.All(s => s[i] == c)).ToArray());
まず、最長の共通プレフィックスを最短の要素より長くすることはできないことがわかります。したがって、他のすべての文字列が同じ位置に同じ文字を持っている間に、最も短いものを取り、そこから文字を取ります。極端な場合、最短の要素からすべての文字を取得します。最短の要素を反復処理することにより、インデックスルックアップは例外をスローしません。
LINQを使用してそれを解決する別の(より悪いがまだ興味深い)方法は次のとおりです。
samples.Aggregate(samples.Min(), (current, next) => new string(current.TakeWhile((c,i) => next[i] == c).ToArray() ));
これは、commonPrefixを作成し、それを各要素と1つずつ比較することで機能します。各比較で、commonPrefixは維持または減少されます。最初の反復では、currentはmin要素ですが、その後の各反復は、これまでに見つかった最高のcommonPrefixです。これを深さ優先の解決策と考えてください。最初の解決策は幅優先です。
このタイプのソリューションは、最も短い要素が最初に比較されるようにサンプルを長さでソートすることによって改善される可能性があります。
ただし、このタイプのソリューションは、最初のソリューションよりも優れているとは限りません。最良の場合、これは最初の解決策と同じくらい良いです。ただし、それ以外の場合は、必要以上に長い一時的なcommonPrefixを見つけることで余分な作業を行います。
ここでは、大量の文字列を分析する必要がある場合に非常に効率的な方法を実装しました。ここでもカウントと長さをキャッシュしています。これにより、ループ内のプロパティアクセスと比較して、テストのパフォーマンスが約1.5倍向上します。
using System.Collections.Generic;
using System.Text;
........
public static string GetCommonPrefix ( IList<string> strings )
{
var stringsCount = strings.Count;
if( stringsCount == 0 )
return null;
if( stringsCount == 1 )
return strings[0];
var sb = new StringBuilder( strings[0] );
string str;
int i, j, sbLen, strLen;
for( i = 1; i < stringsCount; i++ )
{
str = strings[i];
sbLen = sb.Length;
strLen = str.Length;
if( sbLen > strLen )
sb.Length = sbLen = strLen;
for( j = 0; j < sbLen; j++ )
{
if( sb[j] != str[j] )
{
sb.Length = j;
break;
}
}
}
return sb.ToString();
}
PD:最後のステップとして上記のメソッドを使用する並列バージョンも実装しました:
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
........
public static string GetCommonPrefixParallel ( IList<string> strings )
{
var stringsCount = strings.Count;
if( stringsCount == 0 )
return null;
if( stringsCount == 1 )
return strings[0];
var firstStr = strings[0];
var finalList = new List<string>();
var finalListLock = new object();
Parallel.For( 1, stringsCount,
() => new StringBuilder( firstStr ),
( i, loop, localSb ) =>
{
var sbLen = localSb.Length;
var str = strings[i];
var strLen = str.Length;
if( sbLen > strLen )
localSb.Length = sbLen = strLen;
for( int j = 0; j < sbLen; j++ )
{
if( localSb[j] != str[j] )
{
localSb.Length = j;
break;
}
}
return localSb;
},
( localSb ) =>
{
lock( finalListLock )
{
finalList.Add( localSb.ToString() );
}
} );
return GetCommonPrefix( finalList );
}
GetCommonPrefixParallel()は、膨大な文字列の量と文字列の長さが重要な場合に、GetCommonPrefix()と比較して2倍ブーストします。短い文字列の小さな配列では、GetCommonPrefix()の動作が少し良くなります。 MacBook Pro Retina 13 ''でテストしました。
トップアンサーは、ケースを無視するように改善できます。
.TakeWhile(s =>
{
var reference = s.First();
return s.All(d => string.Equals(reference, d, StringComparison.OrdinalIgnoreCase));
})
これは、一般的な文字列プレフィックスを見つける簡単な方法です。
public static string GetCommonStartingPath(string[] keys)
{
Array.Sort(keys, StringComparer.InvariantCulture);
string a1 = keys[0], a2 = keys[keys.Length - 1];
int L = a1.Length, i = 0;
while (i < L && a1[i] == a2[i])
{
i++;
}
string result = a1.Substring(0, i);
return result;
}
私はパーティーに遅れていますが、2セントを差し上げます。
_public static String CommonPrefix(String str, params String[] more)
{
var prefixLength = str
.TakeWhile((c, i) => more.All(s => i < s.Length && s[i] == c))
.Count();
return str.Substring(0, prefixLength);
}
_
説明:
これは、str
の他の文字列がインデックスAll
で同じ文字c
を持っている限り、i
の文字をウォークスルーすることで機能します。
String
と_params String[]
_で分割された署名により、少なくとも1つの文字列が提供され、実行時チェックは不要になります。
Substring(0, prefixLength)
またはString.Join()
を使用して列挙された文字を再構成するよりも、プレフィックス長をCount
してEnumerable.Aggregate()
を返す方が安価です。