2010-10-02 14 views
21

Tengo una lista de números, p. 21,4,7,9,12,22,17,8,2,20,23Detectando la secuencia de al menos 3 números secuenciales de una lista dada

Quiero ser capaz de elegir secuencias de números secuenciales (mínimo 3 elementos de longitud), por lo que a partir del ejemplo anterior sería 7,8,9 y 20,21,22,23.

He jugado con algunas funciones poco comunes, pero me pregunto si hay una buena manera LINQ-ish para hacerlo.

¿Alguna sugerencia?

ACTUALIZACIÓN:

Muchas gracias por todas las respuestas, mucho appriciated. Actualmente estoy teniendo una obra de teatro con todos ellos para ver cuál se integraría mejor en nuestro proyecto.

+0

¿Se permite que la lista tenga números duplicados? –

+0

@Kyralessa No la lista nunca contendrá duplicados – Dve

Respuesta

11

/soluciones de Timwi de Jon Skeet son el camino a seguir.

Para la diversión, aquí está una consulta LINQ que hace el trabajo (muy ineficientemente):

var sequences = input.Distinct() 
        .GroupBy(num => Enumerable.Range(num, int.MaxValue - num + 1) 
               .TakeWhile(input.Contains) 
               .Last()) //use the last member of the consecutive sequence as the key 
        .Where(seq => seq.Count() >= 3) 
        .Select(seq => seq.OrderBy(num => num)); // not necessary unless ordering is desirable inside each sequence. 

el rendimiento de la consulta se puede mejorar ligeramente mediante la carga de la entrada en una HashSet (para mejorar Contains), pero eso aún no producirá una solución que esté cerca de ser eficiente.

El único fallo Estoy en cuenta es la posibilidad de un desbordamiento aritmético si la secuencia contiene los números negativos de gran magnitud (no podemos representar el parámetro de countRange). Esto sería fácil de solucionar con un método de extensión static IEnumerable<int> To(this int start, int end) personalizado. Si alguien puede pensar en alguna otra técnica simple para eludir el desbordamiento, por favor avíseme.

EDITAR: Aquí hay una variante un poco más detallada (pero igualmente ineficiente) sin el problema de desbordamiento.

var sequences = input.GroupBy(num => input.Where(candidate => candidate >= num) 
              .OrderBy(candidate => candidate) 
              .TakeWhile((candidate, index) => candidate == num + index) 
              .Last()) 
        .Where(seq => seq.Count() >= 3) 
        .Select(seq => seq.OrderBy(num => num)); 
+3

Interesante. Creo que lo entiendo ... –

+1

Re. números negativos, incluso '-1' fallará con un desbordamiento, y de hecho también' 0', debido a la llamada 'Enumerable.Range (num, int.MaxValue - num + 1)' – configurator

+1

¿Por qué no usar 'int .MaxValue' allí? – configurator

25

Me parece que lo primero que debe hacer es ordenar la lista. Entonces solo es cuestión de recorrerlo, recordar la duración de la secuencia actual y detectar cuándo ha terminado. Para ser honesto, yo sospecho que un bucle foreach simple va a ser la forma más simple de hacer eso - No puedo pensar de inmediato en ninguna manera maravillosamente limpia de LINQ de hacerlo. Ciertamente podría hacerlo en un bloque de iteradores si realmente quisiera, pero tenga en cuenta que ordenar la lista para comenzar significa que tiene un costo razonablemente "adelantado" de todos modos. Así que mi solución sería algo como esto:

var ordered = list.OrderBy(x => x); 
int count = 0; 
int firstItem = 0; // Irrelevant to start with 
foreach (int x in ordered) 
{ 
    // First value in the ordered list: start of a sequence 
    if (count == 0) 
    { 
     firstItem = x; 
     count = 1; 
    } 
    // Skip duplicate values 
    else if (x == firstItem + count - 1) 
    { 
     // No need to do anything 
    } 
    // New value contributes to sequence 
    else if (x == firstItem + count) 
    { 
     count++; 
    } 
    // End of one sequence, start of another 
    else 
    { 
     if (count >= 3) 
     { 
      Console.WriteLine("Found sequence of length {0} starting at {1}", 
           count, firstItem); 
     } 
     count = 1; 
     firstItem = x; 
    } 
} 
if (count >= 3) 
{ 
    Console.WriteLine("Found sequence of length {0} starting at {1}", 
         count, firstItem); 
} 

EDIT: Bueno, he acaba de ocurrir una manera bastante más LINQ-ish de hacer las cosas. No tengo el tiempo para aplicar plenamente ahora, pero:

  • Orden de la secuencia
  • usar algo como SelectWithPrevious (probablemente mejor llamado SelectConsecutive) para obtener pares consecutivos de elementos
  • Use la sobrecarga de Seleccione cuál incluye el índice para obtener tuplas de (índice, actual, anterior)
  • Filtre cualquier elemento donde (actual = anterior + 1) para llegar a cualquier lugar que cuente como comience de una secuencia (índice de caso especial = 0)
  • Uso SelectWithPrevious en el resultado para obtener el longitud de la secuencia entre dos puntos de partida (restar un índice a partir de la anterior)
  • filtrar cualquier secuencia con longitud inferior a 3

I sospechoso se necesita concat int.MinValue en la secuencia ordenada, para garantizar que el elemento final se utiliza correctamente.

EDITAR: Bien, lo he implementado. Se trata de la manera más LINK que se me ocurre para hacer esto ... Utilicé valores nulos como valores "centinela" para forzar las secuencias de inicio y fin. Consulte los comentarios para obtener más detalles.

En general, no recomendaría esta solución. Es difícil darse vuelta, y aunque estoy razonablemente seguro de que es correcto, me tomó un tiempo pensar en posibles errores uno por uno, etc. Es un viaje interesante hacia lo que puede hacer con con LINQ ... y también lo que probablemente no deberías.

Ah, y tenga en cuenta que he enviado la parte de "longitud mínima de 3" a la persona que llama: cuando tiene una secuencia de tuplas como esta, es más limpio filtrarla por separado, IMO.

using System; 
using System.Collections.Generic; 
using System.Linq; 

static class Extensions 
{ 
    public static IEnumerable<TResult> SelectConsecutive<TSource, TResult> 
     (this IEnumerable<TSource> source, 
     Func<TSource, TSource, TResult> selector) 
    { 
     using (IEnumerator<TSource> iterator = source.GetEnumerator()) 
     { 
      if (!iterator.MoveNext()) 
      { 
       yield break; 
      } 
      TSource prev = iterator.Current; 
      while (iterator.MoveNext()) 
      { 
       TSource current = iterator.Current; 
       yield return selector(prev, current); 
       prev = current; 
      } 
     } 
    } 
} 

class Test 
{ 
    static void Main() 
    { 
     var list = new List<int> { 21,4,7,9,12,22,17,8,2,20,23 }; 

     foreach (var sequence in FindSequences(list).Where(x => x.Item1 >= 3)) 
     { 
      Console.WriteLine("Found sequence of length {0} starting at {1}", 
           sequence.Item1, sequence.Item2); 
     } 
    } 

    private static readonly int?[] End = { null }; 

    // Each tuple in the returned sequence is (length, first element) 
    public static IEnumerable<Tuple<int, int>> FindSequences 
     (IEnumerable<int> input) 
    { 
     // Use null values at the start and end of the ordered sequence 
     // so that the first pair always starts a new sequence starting 
     // with the lowest actual element, and the final pair always 
     // starts a new one starting with null. That "sequence at the end" 
     // is used to compute the length of the *real* final element. 
     return End.Concat(input.OrderBy(x => x) 
           .Select(x => (int?) x)) 
        .Concat(End) 
        // Work out consecutive pairs of items 
        .SelectConsecutive((x, y) => Tuple.Create(x, y)) 
        // Remove duplicates 
        .Where(z => z.Item1 != z.Item2) 
        // Keep the index so we can tell sequence length 
        .Select((z, index) => new { z, index }) 
        // Find sequence starting points 
        .Where(both => both.z.Item2 != both.z.Item1 + 1) 
        .SelectConsecutive((start1, start2) => 
         Tuple.Create(start2.index - start1.index, 
            start1.z.Item2.Value)); 
    } 
} 
+0

+1 programación nítida –

+0

@Jon Gracias por su rápida respuesta, lo ideal es que desee devolver una lista de secuencias encontradas en la función. Pero su código es un gran punto de partida ... aplausos – Dve

+0

+1 por buena explicación – TalentTuner

2

Aquí es cómo resolver el problema de una manera "LINQish":

int[] arr = new int[]{ 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 }; 
IOrderedEnumerable<int> sorted = arr.OrderBy(x => x); 
int cnt = sorted.Count(); 
int[] sortedArr = sorted.ToArray(); 
IEnumerable<int> selected = sortedArr.Where((x, idx) => 
    idx <= cnt - 3 && sortedArr[idx + 1] == x + 1 && sortedArr[idx + 2] == x + 2); 
IEnumerable<int> result = selected.SelectMany(x => new int[] { x, x + 1, x + 2 }).Distinct(); 

Console.WriteLine(string.Join(",", result.Select(x=>x.ToString()).ToArray())); 

Debido a la copia matriz y la reconstrucción, esta solución - por supuesto - no es tan eficiente como la solución tradicional con bucles

+0

+1 ¡Yuck! Pero es LINQ-ish. –

+0

Mismo error que el de Jon: dada la entrada '[1, 2, 3, 2]', no encontrará '1, 2, 3'. En su caso, la solución es más simple: simplemente agregue '.Distinct()' después de '.OrderBy()' (y cambie 'IOrderedEnumerable' a' IEnumerable', o mejor aún, cambie todo a 'var'). – Timwi

+0

Hm, otro problema con este enfoque es que solo obtiene una lista única al final, no un conjunto de secuencias delineadas correctamente. – Timwi

3

Creo que mi solución es más elegante y simple, y por lo tanto más fácil de verificar como correctas:

/// <summary>Returns a collection containing all consecutive sequences of 
/// integers in the input collection.</summary> 
/// <param name="input">The collection of integers in which to find 
/// consecutive sequences.</param> 
/// <param name="minLength">Minimum length that a sequence should have 
/// to be returned.</param> 
static IEnumerable<IEnumerable<int>> ConsecutiveSequences(
    IEnumerable<int> input, int minLength = 1) 
{ 
    var results = new List<List<int>>(); 
    foreach (var i in input.OrderBy(x => x)) 
    { 
     var existing = results.FirstOrDefault(lst => lst.Last() + 1 == i); 
     if (existing == null) 
      results.Add(new List<int> { i }); 
     else 
      existing.Add(i); 
    } 
    return minLength <= 1 ? results : 
     results.Where(lst => lst.Count >= minLength); 
} 

Beneficios sobre las otras soluciones:

  • Puede encontrar las secuencias que se superponen.
  • Es correctamente reutilizable y documentado.
  • no he encontrado algún error ;-)
+0

¿Podría dar un ejemplo de lo que quiere decir con "secuencias superpuestas"? ¿Quiere decir dónde se produce un valor más de una vez? –

+0

No estoy de acuerdo con su afirmación de que es más simple. Me resulta bastante difícil de entender en este momento. –

+0

Está bien, lo he entendido ahora, creo ... pero todavía no lo encuentro tan simple como el mío. –

1

No es 100% LINQ pero aquí es una variante genérica:

static IEnumerable<IEnumerable<TItem>> GetSequences<TItem>(
     int minSequenceLength, 
     Func<TItem, TItem, bool> areSequential, 
     IEnumerable<TItem> items) 
    where TItem : IComparable<TItem> 
{ 
    items = items 
     .OrderBy(n => n) 
     .Distinct().ToArray(); 

    var lastSelected = default(TItem); 

    var sequences = 
     from startItem in items 
     where startItem.Equals(items.First()) 
      || startItem.CompareTo(lastSelected) > 0 
     let sequence = 
      from item in items 
      where item.Equals(startItem) || areSequential(lastSelected, item) 
      select (lastSelected = item) 
     where sequence.Count() >= minSequenceLength 
     select sequence; 

    return sequences; 
} 

static void UsageInt() 
{ 
    var sequences = GetSequences(
      3, 
      (a, b) => a + 1 == b, 
      new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 }); 

    foreach (var sequence in sequences) 
     Console.WriteLine(string.Join(", ", sequence.ToArray())); 
} 

static void UsageChar() 
{ 
    var list = new List<char>(
     "abcdefghijklmnopqrstuvwxyz".ToCharArray()); 

    var sequences = GetSequences(
      3, 
      (a, b) => (list.IndexOf(a) + 1 == list.IndexOf(b)), 
      "PleaseBeGentleWithMe".ToLower().ToCharArray()); 

    foreach (var sequence in sequences) 
     Console.WriteLine(string.Join(", ", sequence.ToArray())); 
} 
1

¿Qué hay de clasificación de la matriz a continuación, crear otra matriz que es la diferencia entre cada elemento de la anterior


sortedArray = 8, 9, 10, 21, 22, 23, 24, 27, 30, 31, 32 
diffArray = 1, 1, 11, 1, 1, 1, 3, 3, 1, 1 
Ahora iterar a través de la diferencia formación; si la diferencia es igual a 1, aumenta el recuento de una variable, longitud de secuencia, en 1. Si la diferencia es> 1, comprueba la longitud de la secuencia si es> = 2, entonces tienes una secuencia de al menos 3 elementos consecutivos. Luego restablezca sequenceLenght a 0 y continúe su ciclo en la matriz de diferencias.

0

Aquí hay una solución que noqueé en F #, debería ser bastante fácil traducir esto en una consulta de C# LINQ ya que fold es prácticamente equivalente al operador de agregado LINQ.

#light 

let nums = [21;4;7;9;12;22;17;8;2;20;23] 

let scanFunc (mainSeqLength, mainCounter, lastNum:int, subSequenceCounter:int, subSequence:'a list, foundSequences:'a list list) (num:'a) = 
     (mainSeqLength, mainCounter + 1, 
     num, 
     (if num <> lastNum + 1 then 1 else subSequenceCounter+1), 
     (if num <> lastNum + 1 then [num] else [email protected][num]), 
     if subSequenceCounter >= 3 then 
      if mainSeqLength = mainCounter+1 
       then foundSequences @ [[email protected][num]] 
      elif num <> lastNum + 1 
       then foundSequences @ [subSequence] 
      else foundSequences 
     else foundSequences) 

let subSequences = nums |> Seq.sort |> Seq.fold scanFunc (nums |> Seq.length, 0, 0, 0, [], []) |> fun (_,_,_,_,_,results) -> results 
0

Linq no es la solución para todo, a veces es mejor con un simple bucle. He aquí una solución, con sólo un poco de LINQ para ordenar las secuencias originales y filtrar los resultados

void Main() 
{ 
    var numbers = new[] { 21,4,7,9,12,22,17,8,2,20,23 }; 
    var sequences = 
     GetSequences(numbers, (prev, curr) => curr == prev + 1); 
     .Where(s => s.Count() >= 3); 
    sequences.Dump(); 
} 

public static IEnumerable<IEnumerable<T>> GetSequences<T>(
    IEnumerable<T> source, 
    Func<T, T, bool> areConsecutive) 
{ 
    bool first = true; 
    T prev = default(T); 
    List<T> seq = new List<T>(); 
    foreach (var i in source.OrderBy(i => i)) 
    { 
     if (!first && !areConsecutive(prev, i)) 
     { 
      yield return seq.ToArray(); 
      seq.Clear(); 
     } 
     first = false; 
     seq.Add(i); 
     prev = i; 
    } 
    if (seq.Any()) 
     yield return seq.ToArray(); 
} 
0

pensé lo mismo que Jon: para representar un rango de enteros consecutivos único que necesitas son dos números enteros míseros ! Así que me gustaría empezar por ahí:

struct Range : IEnumerable<int> 
{ 
    readonly int _start; 
    readonly int _count; 

    public Range(int start, int count) 
    { 
     _start = start; 
     _count = count; 
    } 

    public int Start 
    { 
     get { return _start; } 
    } 

    public int Count 
    { 
     get { return _count; } 
    } 

    public int End 
    { 
     get { return _start + _count - 1; } 
    } 

    public IEnumerator<int> GetEnumerator() 
    { 
     for (int i = 0; i < _count; ++i) 
     { 
      yield return _start + i; 
     } 
    } 

    // Heck, why not? 
    public static Range operator +(Range x, int y) 
    { 
     return new Range(x.Start, x.Count + y); 
    } 

    // skipping the explicit IEnumerable.GetEnumerator implementation 
} 

A partir de ahí, se puede escribir un método estático para devolver un montón de estos Range valores correspondientes a los números consecutivos de la secuencia.

static IEnumerable<Range> FindRanges(IEnumerable<int> source, int minCount) 
{ 
    // throw exceptions on invalid arguments, maybe... 

    var ordered = source.OrderBy(x => x); 

    Range r = default(Range); 

    foreach (int value in ordered) 
    { 
     // In "real" code I would've overridden the Equals method 
     // and overloaded the == operator to write something like 
     // if (r == Range.Empty) here... but this works well enough 
     // for now, since the only time r.Count will be 0 is on the 
     // first item. 
     if (r.Count == 0) 
     { 
      r = new Range(value, 1); 
      continue; 
     } 

     if (value == r.End) 
     { 
      // skip duplicates 
      continue; 
     } 
     else if (value == r.End + 1) 
     { 
      // "append" consecutive values to the range 
      r += 1; 
     } 
     else 
     { 
      // return what we've got so far 
      if (r.Count >= minCount) 
      { 
       yield return r; 
      } 

      // start over 
      r = new Range(value, 1); 
     } 
    } 

    // return whatever we ended up with 
    if (r.Count >= minCount) 
    { 
     yield return r; 
    } 
} 

Demostración:

int[] numbers = new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 }; 

foreach (Range r in FindConsecutiveRanges(numbers, 3)) 
{ 
    // Using .NET 3.5 here, don't have the much nicer string.Join overloads. 
    Console.WriteLine(string.Join(", ", r.Select(x => x.ToString()).ToArray())); 
} 

Salida:

 
7, 8, 9 
20, 21, 22, 23 
0

Aquí está mi LINQ-y toma en el problema:

static IEnumerable<IEnumerable<int>> 
    ConsecutiveSequences(this IEnumerable<int> input, int minLength = 3) 
{ 
    int order = 0; 
    var inorder = new SortedSet<int>(input); 
    return from item in new[] { new { order = 0, val = inorder.First() } } 
       .Concat(
       inorder.Zip(inorder.Skip(1), (x, val) => 
         new { order = x + 1 == val ? order : ++order, val })) 
      group item.val by item.order into list 
      where list.Count() >= minLength 
      select list; 
} 
  • utiliza ningún explícita bucles, pero aún debe ser O (n lg n)
  • utiliza SortedSet en lugar de .OrderBy().Distinct()
  • combina elementos consecutivos con list.Zip(list.Skip(1))
0

he aquí una solución usando un diccionario en lugar de una especie ... Añade los elementos a un diccionario, y luego para cada valor se incrementa arriba y abajo para encontrar la secuencia más larga.
no es estrictamente LINQ, a pesar de que hace uso de algunas funciones LINQ, y creo que es más fácil de leer que una solución LINQ pura ..

static void Main(string[] args) 
    { 
     var items = new[] { -1, 0, 1, 21, -2, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 }; 
     IEnumerable<IEnumerable<int>> sequences = FindSequences(items, 3); 

     foreach (var sequence in sequences) 
     { //print results to consol 
      Console.Out.WriteLine(sequence.Select(num => num.ToString()).Aggregate((a, b) => a + "," + b)); 
     } 
     Console.ReadLine(); 
    } 

    private static IEnumerable<IEnumerable<int>> FindSequences(IEnumerable<int> items, int minSequenceLength) 
    { 
     //Convert item list to dictionary 
     var itemDict = new Dictionary<int, int>(); 
     foreach (int val in items) 
     { 
      itemDict[val] = val; 
     } 
     var allSequences = new List<List<int>>(); 
     //for each val in items, find longest sequence including that value 
     foreach (var item in items) 
     { 
      var sequence = FindLongestSequenceIncludingValue(itemDict, item); 
      allSequences.Add(sequence); 
      //remove items from dict to prevent duplicate sequences 
      sequence.ForEach(i => itemDict.Remove(i)); 
     } 
     //return only sequences longer than 3 
     return allSequences.Where(sequence => sequence.Count >= minSequenceLength).ToList(); 
    } 

    //Find sequence around start param value 
    private static List<int> FindLongestSequenceIncludingValue(Dictionary<int, int> itemDict, int value) 
    { 
     var result = new List<int>(); 
     //check if num exists in dictionary 
     if (!itemDict.ContainsKey(value)) 
      return result; 

     //initialize sequence list 
     result.Add(value); 

     //find values greater than starting value 
     //and add to end of sequence 
     var indexUp = value + 1; 
     while (itemDict.ContainsKey(indexUp)) 
     { 
      result.Add(itemDict[indexUp]); 
      indexUp++; 
     } 

     //find values lower than starting value 
     //and add to start of sequence 
     var indexDown = value - 1; 
     while (itemDict.ContainsKey(indexDown)) 
     { 
      result.Insert(0, itemDict[indexDown]); 
      indexDown--; 
     } 
     return result; 
    } 
1

Aquí es mi tiro en él:

public static class SequenceDetector 
{ 
    public static IEnumerable<IEnumerable<T>> DetectSequenceWhere<T>(this IEnumerable<T> sequence, Func<T, T, bool> inSequenceSelector) 
    { 
     List<T> subsequence = null; 
     // We can only have a sequence with 2 or more items 
     T last = sequence.FirstOrDefault(); 
     foreach (var item in sequence.Skip(1)) 
     { 
      if (inSequenceSelector(last, item)) 
      { 
       // These form part of a sequence 
       if (subsequence == null) 
       { 
        subsequence = new List<T>(); 
        subsequence.Add(last); 
       } 
       subsequence.Add(item); 
      } 
      else if (subsequence != null) 
      { 
       // We have a previous seq to return 
       yield return subsequence; 
       subsequence = null; 
      } 
      last = item; 
     } 
     if (subsequence != null) 
     { 
      // Return any trailing seq 
      yield return subsequence; 
     } 
    } 
} 

public class test 
{ 
    public static void run() 
    { 
     var list = new List<int> { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 }; 
     foreach (var subsequence in list 
      .OrderBy(i => i) 
      .Distinct() 
      .DetectSequenceWhere((first, second) => first + 1 == second) 
      .Where(seq => seq.Count() >= 3)) 
     { 
      Console.WriteLine("Found subsequence {0}", 
       string.Join(", ", subsequence.Select(i => i.ToString()).ToArray())); 
     } 
    } 
} 

Esto devuelve los elementos específicos que forman las subsecuencias y permite cualquier tipo de elemento y cualquier definición de criterio, siempre que se pueda determinar comparando elementos adyacentes.

Cuestiones relacionadas