Finden aller Positionen von Teilzeichenfolgen in einer größeren Zeichenfolge in C #

80

Ich habe eine große Zeichenfolge, die ich analysieren muss, und ich muss alle Instanzen von extract"(me,i-have lots. of]punctuationfinden und den Index von jeder in einer Liste speichern.

Angenommen, dieses Stück Zeichenfolge befand sich am Anfang und in der Mitte der größeren Zeichenfolge. Beide würden gefunden und ihre Indizes würden zu der hinzugefügt List. und der Listwürde enthalten 0und der andere Index, was auch immer es sein würde.

Ich habe herumgespielt und das string.IndexOfmacht fast das, wonach ich suche, und ich habe Code geschrieben - aber es funktioniert nicht und ich konnte nicht genau herausfinden, was falsch ist:

List<int> inst = new List<int>();
int index = 0;
while (index < source.LastIndexOf("extract\"(me,i-have lots. of]punctuation", 0) + 39)
{
    int src = source.IndexOf("extract\"(me,i-have lots. of]punctuation", index);
    inst.Add(src);
    index = src + 40;
}
  • inst = Die Liste
  • source = Die große Zeichenfolge

Irgendwelche besseren Ideen?

Caesay
quelle

Antworten:

140

Hier ist ein Beispiel für eine Erweiterungsmethode:

public static List<int> AllIndexesOf(this string str, string value) {
    if (String.IsNullOrEmpty(value))
        throw new ArgumentException("the string to find may not be empty", "value");
    List<int> indexes = new List<int>();
    for (int index = 0;; index += value.Length) {
        index = str.IndexOf(value, index);
        if (index == -1)
            return indexes;
        indexes.Add(index);
    }
}

Wenn Sie dies in eine statische Klasse einfügen und den Namespace mit importieren using, wird es als Methode für eine beliebige Zeichenfolge angezeigt, und Sie können einfach Folgendes tun:

List<int> indexes = "fooStringfooBar".AllIndexesOf("foo");

Weitere Informationen zu Erweiterungsmethoden finden Sie unter http://msdn.microsoft.com/en-us/library/bb383977.aspx

Das gleiche gilt auch für einen Iterator:

public static IEnumerable<int> AllIndexesOf(this string str, string value) {
    if (String.IsNullOrEmpty(value))
        throw new ArgumentException("the string to find may not be empty", "value");
    for (int index = 0;; index += value.Length) {
        index = str.IndexOf(value, index);
        if (index == -1)
            break;
        yield return index;
    }
}
Matti Virkkunen
quelle
8
Warum nicht IEnumerable <int> verwenden und anstelle der Indexliste den Return-Index zurückgeben?
m0sa
2
@ m0sa: Guter Punkt. Eine weitere Version wurde nur zum Spaß hinzugefügt.
Matti Virkkunen
2
@ PedroC88: Mit yieldwird der Code "faul". Es werden nicht alle Indizes in einer speicherinternen Liste innerhalb der Methode gesammelt. Welche praktischen Auswirkungen dies auf die Leistung hat, hängt von vielen Faktoren ab.
Matti Virkkunen
1
@ Paul: "Darf nicht" wie in "darf nicht". Wenn Ihnen der Wortlaut nicht gefällt, können Sie jederzeit eine Änderung vorschlagen, aber ich denke nicht, dass es so schwer zu verstehen ist.
Matti Virkkunen
10
Beachtung! Aufgrund des Hinzufügens können value.LengthSie verschachtelte Übereinstimmungen verpassen! Beispiel: "Dies ist ein NestedNestedNested-Match-Test!" Bei Übereinstimmung mit "NestedNested" wird nur ein Index gefunden, nicht jedoch der verschachtelte. Um dies zu beheben, fügen Sie statt +=1in einfach eine Schleife hinzu +=value.Length.
Christoph Meißner
20

Warum verwenden Sie nicht die integrierte RegEx-Klasse:

public static IEnumerable<int> GetAllIndexes(this string source, string matchString)
{
   matchString = Regex.Escape(matchString);
   foreach (Match match in Regex.Matches(source, matchString))
   {
      yield return match.Index;
   }
}

Wenn Sie den Ausdruck wiederverwenden müssen, kompilieren Sie ihn und speichern Sie ihn irgendwo zwischen. Ändern Sie den Parameter matchString in einen Regex-MatchExpression in einer anderen Überladung für den Wiederverwendungsfall.

csaam
quelle
Dies wird nicht kompiliert
Anshul
was ist indexes? Es ist nirgendwo definiert.
Saggio
Mein schlechtes, es ist ein Überrest. Löschen Sie diese Zeile.
csaam
2
Beachten Sie, dass diese Methode den gleichen Fehler aufweist wie die akzeptierte Antwort. Wenn Ihre Quellzeichenfolge "ccc" und das Muster "cc" ist, wird nur ein Vorkommen zurückgegeben.
user280498
15

mit LINQ

public static IEnumerable<int> IndexOfAll(this string sourceString, string subString)
{
    return Regex.Matches(sourceString, subString).Cast<Match>().Select(m => m.Index);
}
ehosca
quelle
2
Sie haben jedoch vergessen, dem SubString zu entkommen.
Csaam
Dies ist der akzeptierten Lösung wegen ihrer geringeren zyklomatischen Komplexität vorzuziehen.
Denny Jacob
5

Polierte Version + Gehäuse ohne Unterstützung:

public static int[] AllIndexesOf(string str, string substr, bool ignoreCase = false)
{
    if (string.IsNullOrWhiteSpace(str) ||
        string.IsNullOrWhiteSpace(substr))
    {
        throw new ArgumentException("String or substring is not specified.");
    }

    var indexes = new List<int>();
    int index = 0;

    while ((index = str.IndexOf(substr, index, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) != -1)
    {
        indexes.Add(index++);
    }

    return indexes.ToArray();
}
net_prog
quelle
2

Dies könnte in effizienter Zeitkomplexität unter Verwendung des KMP- Algorithmus in O (N + M) erfolgen, wobei N die Länge von textund M die Länge von ist pattern.

Dies ist die Implementierung und Verwendung:

static class StringExtensions
{
    public static IEnumerable<int> AllIndicesOf(this string text, string pattern)
    {
        if (string.IsNullOrEmpty(pattern))
        {
            throw new ArgumentNullException(nameof(pattern));
        }
        return Kmp(text, pattern);
    }

    private static IEnumerable<int> Kmp(string text, string pattern)
    {
        int M = pattern.Length;
        int N = text.Length;

        int[] lps = LongestPrefixSuffix(pattern);
        int i = 0, j = 0; 

        while (i < N)
        {
            if (pattern[j] == text[i])
            {
                j++;
                i++;
            }
            if (j == M)
            {
                yield return i - j;
                j = lps[j - 1];
            }

            else if (i < N && pattern[j] != text[i])
            {
                if (j != 0)
                {
                    j = lps[j - 1];
                }
                else
                {
                    i++;
                }
            }
        }
    }

    private static int[] LongestPrefixSuffix(string pattern)
    {
        int[] lps = new int[pattern.Length];
        int length = 0;
        int i = 1;

        while (i < pattern.Length)
        {
            if (pattern[i] == pattern[length])
            {
                length++;
                lps[i] = length;
                i++;
            }
            else
            {
                if (length != 0)
                {
                    length = lps[length - 1];
                }
                else
                {
                    lps[i] = length;
                    i++;
                }
            }
        }
        return lps;
    }

und dies ist ein Beispiel für die Verwendung:

static void Main(string[] args)
    {
        string text = "this is a test";
        string pattern = "is";
        foreach (var index in text.AllIndicesOf(pattern))
        {
            Console.WriteLine(index); // 2 5
        }
    }
M. Khooryani
quelle
Was ist die Leistung im Vergleich zur optimalen IndexOf-Implementierung, bei der der Suchstartindex bei jeder Iteration auf das Ende der vorherigen Übereinstimmung gesetzt wird?
Caesay
Der Vergleich von IndexOf mit AllIndicesOf ist falsch, da die Ausgabe unterschiedlich ist. Durch die Verwendung der IndexOf-Methode in jeder Iteration wird die Zeitkomplexität auf O (N ^ 2 M) enorm erhöht, während die optimale Komplexität O (N + M) ist. KMP funktioniert nicht ähnlich wie der naive Ansatz, sondern verwendet ein vorberechnetes Array (LPS), um eine Suche von Anfang an zu vermeiden. Empfehlen Sie, den KMP-Algorithmus zu lesen. Die letzten Absätze des Abschnitts "Hintergrund" in Wikipedia erklären, wie es in O (N) funktioniert.
M. Khooryani
1
public List<int> GetPositions(string source, string searchString)
{
    List<int> ret = new List<int>();
    int len = searchString.Length;
    int start = -len;
    while (true)
    {
        start = source.IndexOf(searchString, start + len);
        if (start == -1)
        {
            break;
        }
        else
        {
            ret.Add(start);
        }
    }
    return ret;
}

Nennen Sie es so:

List<int> list = GetPositions("bob is a chowder head bob bob sldfjl", "bob");
// list will contain 0, 22, 26
MusiGenesis
quelle
1

Hallo nette Antwort von @Matti Virkkunen

public static List<int> AllIndexesOf(this string str, string value) {
    if (String.IsNullOrEmpty(value))
        throw new ArgumentException("the string to find may not be empty", "value");
    List<int> indexes = new List<int>();
    for (int index = 0;; index += value.Length) {
        index = str.IndexOf(value, index);
        if (index == -1)
            return indexes;
        indexes.Add(index);
        index--;
    }
}

Dies umfasst jedoch Testfälle wie AOOAOOA, bei denen Teilzeichenfolgen verwendet werden

sind AOOA und AOOA

Ausgang 0 und 3

Pranay Deep
quelle
1

Verwenden Sie ohne Regex den Zeichenfolgenvergleichstyp:

string search = "123aa456AA789bb9991AACAA";
string pattern = "AA";
Enumerable.Range(0, search.Length)
   .Select(index => { return new { Index = index, Length = (index + pattern.Length) > search.Length ? search.Length - index : pattern.Length }; })
   .Where(searchbit => searchbit.Length == pattern.Length && pattern.Equals(search.Substring(searchbit.Index, searchbit.Length),StringComparison.OrdinalIgnoreCase))
   .Select(searchbit => searchbit.Index)

Dies gibt {3,8,19,22} zurück. Ein leeres Muster würde allen Positionen entsprechen.

Für mehrere Muster:

string search = "123aa456AA789bb9991AACAA";
string[] patterns = new string[] { "aa", "99" };
patterns.SelectMany(pattern => Enumerable.Range(0, search.Length)
   .Select(index => { return new { Index = index, Length = (index + pattern.Length) > search.Length ? search.Length - index : pattern.Length }; })
   .Where(searchbit => searchbit.Length == pattern.Length && pattern.Equals(search.Substring(searchbit.Index, searchbit.Length), StringComparison.OrdinalIgnoreCase))
   .Select(searchbit => searchbit.Index))

Dies gibt {3, 8, 19, 22, 15, 16} zurück.

Sean
quelle
1

@csam ist theoretisch korrekt, obwohl sein Code nicht den Anforderungen entspricht und zu refraktärisiert werden kann

public static IEnumerable<int> IndexOfAll(this string sourceString, string matchString)
{
    matchString = Regex.Escape(matchString);
    return from Match match in Regex.Matches(sourceString, matchString) select match.Index;
}
arame3333
quelle
Wenn sein Code falsch war, hätten Sie seinen Beitrag bearbeiten können, um ihn zu korrigieren
caesay
Das hatte ich nicht bemerkt. Ich muss zugeben, dass ich das nur ungern tue, nur für den Fall, dass ich falsch liege, obwohl ich nicht glaube, dass ich es bin.
Arame3333
Das ist keine gute Idee, Regex für große Zeichenfolgen zu verwenden. Der Ansatz benötigt viel Speicher.
W92
1

Ich habe festgestellt, dass mindestens zwei vorgeschlagene Lösungen keine überlappenden Suchtreffer verarbeiten. Ich habe den mit dem grünen Häkchen gekennzeichneten nicht überprüft. Hier ist eine, die überlappende Suchtreffer behandelt:

    public static List<int> GetPositions(this string source, string searchString)
    {
        List<int> ret = new List<int>();
        int len = searchString.Length;
        int start = -1;
        while (true)
        {
            start = source.IndexOf(searchString, start +1);
            if (start == -1)
            {
                break;
            }
            else
            {
                ret.Add(start);
            }
        }
        return ret;
    }
Kevin Baker
quelle
0
public static Dictionary<string, IEnumerable<int>> GetWordsPositions(this string input, string[] Susbtrings)
{
    Dictionary<string, IEnumerable<int>> WordsPositions = new Dictionary<string, IEnumerable<int>>();
    IEnumerable<int> IndexOfAll = null;
    foreach (string st in Susbtrings)
    {
        IndexOfAll = Regex.Matches(input, st).Cast<Match>().Select(m => m.Index);
        WordsPositions.Add(st, IndexOfAll);

    }
    return WordsPositions;
}
Miguel Quiros Garcia
quelle
-1

Basierend auf dem Code, den ich zum Suchen mehrerer Instanzen einer Zeichenfolge in einer größeren Zeichenfolge verwendet habe, sieht Ihr Code folgendermaßen aus:

List<int> inst = new List<int>();
int index = 0;
while (index >=0)
{
    index = source.IndexOf("extract\"(me,i-have lots. of]punctuation", index);
    inst.Add(index);
    index++;
}
Corin
quelle
Hier gibt es zwei Probleme: Erstens fügen Sie Ihrer Ergebnisliste immer -1 hinzu, was kein gültiges Ergebnis ist. Zweitens wird der Code nicht beendet, da indexOf-1 und der zurückgegeben werden index++. Ich würde a while (true)mit a verwenden, break;wenn das Ergebnis von IndexOf-1 ist.
b-pos465
-1

Ich habe dieses Beispiel gefunden und in eine Funktion integriert:

    public static int solution1(int A, int B)
    {
        // Check if A and B are in [0...999,999,999]
        if ( (A >= 0 && A <= 999999999) && (B >= 0 && B <= 999999999))
        {
            if (A == 0 && B == 0)
            {
                return 0;
            }
            // Make sure A < B
            if (A < B)
            {                    
                // Convert A and B to strings
                string a = A.ToString();
                string b = B.ToString();
                int index = 0;

                // See if A is a substring of B
                if (b.Contains(a))
                {
                    // Find index where A is
                    if (b.IndexOf(a) != -1)
                    {                            
                        while ((index = b.IndexOf(a, index)) != -1)
                        {
                            Console.WriteLine(A + " found at position " + index);
                            index++;
                        }
                        Console.ReadLine();
                        return b.IndexOf(a);
                    }
                    else
                        return -1;
                }
                else
                {
                    Console.WriteLine(A + " is not in " + B + ".");
                    Console.ReadLine();

                    return -1;
                }
            }
            else
            {
                Console.WriteLine(A + " must be less than " + B + ".");
               // Console.ReadLine();

                return -1;
            }                
        }
        else
        {
            Console.WriteLine("A or B is out of range.");
            //Console.ReadLine();

            return -1;
        }
    }

    static void Main(string[] args)
    {
        int A = 53, B = 1953786;
        int C = 78, D = 195378678;
        int E = 57, F = 153786;

        solution1(A, B);
        solution1(C, D);
        solution1(E, F);

        Console.WriteLine();
    }

Kehrt zurück:

53 an Position 2 gefunden

78 an Position 4 gefunden
78 gefunden an Position 7

57 ist nicht in 153786

Mark S.
quelle
1
Hallo Markus, ich sehe, dass du neu im Stackoverflow bist. Diese Antwort fügt dieser alten Frage nichts hinzu, es gibt bereits viel bessere Antworten. Wenn Sie in Zukunft eine solche Frage beantworten, versuchen Sie bitte zu erklären, warum Ihre Antwort Informationen oder Werte enthält, die in anderen Antworten noch nicht vorhanden sind.
Caesay