Encontrar todas las posiciones de la subcadena en una cadena más grande en C #

82

Tengo una cadena grande que necesito analizar, y necesito encontrar todas las instancias extract"(me,i-have lots. of]punctuationy almacenar el índice de cada una en una lista.

Entonces, digamos que este trozo de cadena estaba al principio y en medio de la cadena más grande, ambos se encontrarían y sus índices se agregarían al List. y el Listcontendría 0y el otro índice sea lo que sea.

He estado jugando y string.IndexOfhace casi lo que estoy buscando, y he escrito algo de código, pero no funciona y no he podido averiguar exactamente qué está mal:

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 = La lista
  • source = La cuerda grande

¿Alguna idea mejor?

Caesay
fuente

Respuestas:

141

Aquí hay un ejemplo de método de extensión para ello:

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

Si pones esto en una clase estática e importas el espacio de nombres con using, aparece como un método en cualquier cadena, y puedes hacer lo siguiente:

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

Para obtener más información sobre los métodos de extensión, http://msdn.microsoft.com/en-us/library/bb383977.aspx

También lo mismo usando un iterador:

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
fuente
8
¿Por qué no usar IEnumerable <int> y el índice de retorno de rendimiento en lugar de la lista de índices?
m0sa
2
@ m0sa: Buen punto. Se agregó otra versión solo por el gusto de hacerlo.
Matti Virkkunen
2
@ PedroC88: Usar yieldhará que el código sea "perezoso". No recopilará todos los índices en una lista en memoria dentro del método. El tipo de efecto práctico que tiene sobre el rendimiento depende de muchos factores.
Matti Virkkunen
1
@Paul: "No se puede" como en "no se debe". Si no le gusta la redacción, siempre puede sugerir una edición, pero no creo que sea tan difícil de entender.
Matti Virkkunen
10
¡Atención! ¡Debido a la adición, value.Lengthes posible que se pierda las coincidencias anidadas! Ejemplo: "¡Esta es una prueba de coincidencia NestedNestedNested!" con la coincidencia de "NestedNested" encontrará solo un índice, pero no el anidado. Para solucionar esto, simplemente agregue un +=1bucle en lugar de +=value.Length.
Christoph Meißner
20

¿Por qué no usas la clase RegEx incorporada?

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

Si necesita reutilizar la expresión, compílela y almacénela en algún lugar. Cambie el parámetro matchString por un matchExpression de Regex en otra sobrecarga para el caso de reutilización.

csaam
fuente
Esto no se compila
Anshul
lo que es indexes? No está definido en ninguna parte.
Saggio
Mi mal es un remanente. Elimina esa línea.
csaam
2
Tenga en cuenta que este método tiene el mismo defecto que la respuesta aceptada. Si su cadena de origen es "ccc" y el patrón es "cc", solo devolverá una aparición.
user280498
15

usando LINQ

public static IEnumerable<int> IndexOfAll(this string sourceString, string subString)
{
    return Regex.Matches(sourceString, subString).Cast<Match>().Select(m => m.Index);
}
ehosca
fuente
2
Sin embargo, olvidaste escapar de la subString.
csaam
Ésta es preferible a la solución aceptada debido a su menor complejidad ciclomática.
Denny Jacob
5

Versión pulida + estuche ignorando soporte:

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
fuente
2

Se podría hacer en una complejidad de tiempo eficiente utilizando el algoritmo KMP en O (N + M) donde N es la longitud de texty M es la longitud de pattern.

Esta es la implementación y el uso:

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

y este es un ejemplo de cómo usarlo:

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
fuente
¿Cuál es el rendimiento de esto en comparación con la implementación óptima de IndexOf, donde el índice de inicio de búsqueda se establece al final de la coincidencia anterior en cada iteración?
Caesay
Comparar IndexOf con AllIndicesOf es incorrecto ya que su salida es diferente. El uso del método IndexOf en cada iteración aumenta enormemente la complejidad del tiempo a O (N ^ 2 M), mientras que la complejidad óptima es O (N + M). KMP no funciona de manera similar al enfoque ingenuo, utiliza una matriz precalculada (LPS) para evitar la búsqueda desde el principio. Le recomiendo que lea el algoritmo KMP. Los últimos párrafos de la sección "Antecedentes" de Wikipedia explican cómo funciona en O (N).
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;
}

Llámalo así:

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

Hola buena respuesta de @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--;
    }
}

Pero esto cubre casos de pruebas como AOOAOOA donde subcadena

son AOOA y AOOA

Salida 0 y 3

Pranay Deep
fuente
1

Sin Regex, usando el tipo de comparación de cadenas:

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)

Esto devuelve {3,8,19,22}. El patrón vacío coincidiría con todas las posiciones.

Para múltiples patrones:

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

Esto devuelve {3, 8, 19, 22, 15, 16}

Sean
fuente
1

@csam es correcto en teoría, aunque su código no cumplirá y se puede refractar a

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
fuente
si su código era incorrecto, podría haber editado su publicación para corregirlo
caesay
No me había dado cuenta de eso. Tengo que admitir que soy reacio a hacer eso, por si acaso me equivoco, aunque no creo que lo esté.
arame3333
no es una buena idea usar expresiones regulares para cadenas grandes. El enfoque requiere mucha memoria.
W92
1

Noté que al menos dos soluciones propuestas no manejan resultados de búsqueda superpuestos. No revisé el marcado con la marca de verificación verde. Aquí hay uno que maneja resultados de búsqueda superpuestos:

    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
fuente
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
fuente
-1

Según el código que he usado para encontrar múltiples instancias de una cadena dentro de una cadena más grande, su código se vería así:

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
fuente
Aquí hay dos problemas: Primero, siempre agrega -1 a su lista de resultados, que no es un resultado válido. En segundo lugar, el código no termina debido a que indexOfdevuelve -1 y el index++. Usaría a while (true)con a break;si el resultado de IndexOfes -1.
b-pos465
-1

Encontré este ejemplo y lo incorporé a una función:

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

Devoluciones:

53 encontrado en la posición 2

78 encontrado en la posición 4
78 encontrado en la posición 7

57 no está en 153786

Marcas
fuente
1
Hola Mark, veo que eres nuevo en stackoverflow. Esta respuesta no agrega nada a esta vieja pregunta, ya hay respuestas mucho mejores. Si responde una pregunta como esta en el futuro, intente explicar por qué su respuesta contiene alguna información o valor que no existe en otras respuestas.
Caesay