Find common prefix of strings

Viewed 29662

I am having 4 strings:

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

I want to find the common prefix for those strings, i.e. "h:/a". How to find that?

Usually I'd split the string with delimiter '/' and put it in another list, and so on.
Is there any better way to do it?

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

with

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

Just loop round the characters of the shortest string and compare each character to the character in the same position in the other strings. Whilst they all match keep going. As soon as one doesn't match then the string up to the current position -1 is the answer.

Something like (pseudo code)

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;

This is the longest common substring problem (although it's a slightly specialized case since you appear to only care about the prefix). There's no library implementation of the algorithm in the .NET platform that you can call directly, but the article linked here is chock-full of steps on how you'd do it yourself.

Here I implemented quite efficient method when you must analyse huge amount of strings, I'm caching counts and lengths here as well which improves performance by about 1,5x on my tests comparing to properties access in loops:

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: I also implemented parallel version which uses above method as final step:

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() boosts twice comparing to GetCommonPrefix() on huge strings amount and when strings length is significant. On small arrays with short strings GetCommonPrefix() works a little better. I tested on MacBook Pro Retina 13''.

This is a simple method that finds common string prefix.

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

An improvement on Yegor's answer

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

First we know that the longest common prefix cannot be longer than the shortest element. So take the shortest one and take chars from it while all other strings have the same char at the same position. In the extreme case we take all chars from the shortest element. By iterating over the shortest element the index lookup will not throw any exceptions.

Another (worse but still interesting) way to solve it using LINQ would be this:

samples.Aggregate(samples.Min(), (current, next) => new string(current.TakeWhile((c,i) => next[i] == c).ToArray() ));

This one works by creating a commonPrefix and comparing that to each element one by one. In each comparison the commonPrefix is either maintained or decreased. In the first iteration current is the min element, but each iteration after that it is the best commonPrefix found so far. Think of this as a depth first solution while the first one is a breadth first.

This type of solution might be improved upon by sorting the samples in terms of length so that the shortest elements are compared first.

This type of solution cannot really be better than the first however. In the best case this is as-good as the first solution. But otherwise it will do extra work by finding temporary commonPrefixes that are longer than necessary.

I'm late to the party, but I'll give my 2 cents:

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


Explanation:

  • This works by walking through the chars of str as long as All the other strings have the same char c at index i.

  • The signature split in String and params String[] ensures that at least one string is provided, no runtime checks needed.

  • If called with only one string, the function will return the input (a string is its own prefix).
  • It's cheaper to Count the prefix length and return Substring(0, prefixLength) than to reassemble the enumerated chars by means of String.Join() or Enumerable.Aggregate()

Top answer can be improved to ignore case:

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