查找字符串的公共前缀

时间:2010-01-15 08:45:59

标签: c# string pattern-matching

我有4个字符串:

"h:/a/b/c"
"h:/a/b/d"
"h:/a/b/e"
"h:/a/c"

我想找到这些字符串的公共前缀,即"h:/a"。 如何找到?

通常我会用分隔符'/'拆分字符串并将其放在另一个列表中,依此类推。
有没有更好的方法呢?

15 个答案:

答案 0 :(得分:35)

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());
    }
}

答案 1 :(得分:16)

我的一个简短的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());

答案 2 :(得分:14)

只需循环最短字符串的字符,并将每个字符与其他字符串中相同位置的字符进行比较。虽然他们都匹配继续前进。只要一个不匹配,那么直到当前位置-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;

答案 3 :(得分:6)

基于Sam Holder解决方案的工作代码(注意它给出了h:/ a / not 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]
        }
    }
}

答案 4 :(得分:5)

这是longest common substring问题(虽然这是一个稍微专业化的案例,因为您似乎只关心前缀)。在.NET平台上没有可以直接调用的算法的库实现,但是这里链接的文章充满了你自己如何做的步骤。

答案 5 :(得分:2)

我想要一个常见的字符串前缀,除了我想要包含任何字符(比如/),我不想要一些高性能/花哨的东西,我只能通过测试阅读。所以我有这个: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;
    }
}

答案 6 :(得分:2)

以下是c#(http://en.wikipedia.org/wiki/Trie)中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>();
        }
    }
}

这是单元测试&amp;如何使用此类的示例代码。

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);
        }
    }

欢迎任何有关如何改进此课程的建议:)

答案 7 :(得分:1)

我的方法是,取第一个字符串。 在所有其他字符串在同一索引位置上获得相同的字母时逐字逐句获取,如果没有匹配则停止。 如果它是分隔符,请删除最后一个字符。

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());

答案 8 :(得分:1)

我需要在不同的字符串中查找最长的公共前缀。我提出了:

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();
}

如果有多个前缀具有相同的长度,则它会随意返回找到的第一个前缀。它也区分大小写。这两点都可以由读者解决!

答案 9 :(得分:1)

我编写了这个ICollection扩展,以便从一组网址中找到最长的公共基础Uri。

因为它只检查每个斜杠的字符串集合,它将比通用前缀例程(不计算我的低效算法!)快一点。它很冗长,但很容易遵循......我最喜欢的代码类型; - )

忽略'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;
    } 

答案 10 :(得分:0)

可以改进最佳答案以忽略大小写:

.TakeWhile(s =>
  {
      var reference = s.First();
      return s.All(d => string.Equals(reference, d, StringComparison.OrdinalIgnoreCase));
  })

答案 11 :(得分:0)

这里,当您必须分析大量的字符串时,我实现了一种非常有效的方法,我也在此处缓存计数和长度,与循环中的属性访问相比,该方法将测试的性能提高了约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();
}

UPD: 我还实现了并行版本,将上述方法用作最后一步:

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 );
}

与GetCommonPrefix()相比,GetCommonPrefixParallel()在大量字符串和字符串长度很长的情况下提高了两倍。在带有短字符串的小型数组上,GetCommonPrefix()的效果更好。我在MacBook Pro Retina 13英寸上进行了测试。

答案 12 :(得分:0)

这是一种简单的方法,可以找到通用的字符串前缀。

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;
        }

答案 13 :(得分:0)

对Yegor答案的改进

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并将其与每个元素一一进行比较,可以实现这一目的。在每个比较中,commonPrefix均得以维持或减少。在第一个迭代中,当前是min元素,但在此之后的每个迭代中,这是迄今为止找到的最佳commonPrefix。认为这是深度优先的解决方案,而第一个是广度优先。

可以通过按长度对样本进行排序来改进这种解决方案,以便首先比较最短的元素。

但是,这种解决方案实际上并不能比第一种更好。在最佳情况下,这与第一个解决方案一样好。但是否则,它将通过找到比所需时间更长的临时commonPrefixes来做额外的工作。

答案 14 :(得分:0)

我参加聚会很晚,但我会给我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的字符。

  • Stringparams String[]中分割的签名可确保至少提供一个字符串,无需运行时检查。

  • 如果仅使用一个字符串调用,该函数将返回输入(字符串是其自己的前缀)。
  • Count前缀长度并返回Substring(0, prefixLength)比通过String.Join()Enumerable.Aggregate()重组枚举的字符便宜。