Обнаружение последовательности по меньшей мере из 3 последовательных номеров из данного списка
У меня есть список чисел, например. 21,4,7,9,12,22,17,8,2,20,23
Я хочу иметь возможность выбирать последовательности последовательных чисел (минимум 3 элемента по длине), поэтому из приведенного выше примера будет 7,8,9 и 20,21,22,23.
Я играл с несколькими уродливыми растягивающимися функциями, но мне интересно, есть ли опрятный способ LINQ-ish.
Любые предложения?
UPDATE:
Большое спасибо за все ответы, много из них. В настоящее время у меня есть игра с ними, чтобы посмотреть, что лучше всего интегрировать в наш проект.
Ответы
Ответ 1
Решения Jon Skeet/Timwi - это путь.
Для удовольствия здесь запрос LINQ, выполняющий задание (очень неэффективно):
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.
Производительность запроса может быть немного улучшена путем загрузки ввода в HashSet
(для улучшения Contains
), но это все равно не приведет к решению, которое находится где-то близко к эффективному.
Единственная ошибка, о которой я знаю, - это возможность арифметического переполнения, если последовательность содержит отрицательные числа большой величины (мы не можем представить параметр count
для Range
). Это было бы легко исправить с помощью специального метода расширения static IEnumerable<int> To(this int start, int end)
. Если кто-нибудь может подумать о любой другой простой технике уклонения от переполнения, сообщите мне.
EDIT:
Здесь немного более подробный (но не менее эффективный) вариант без проблемы переполнения.
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));
Ответ 2
Мне кажется, что первое, что вам нужно сделать, это заказать список. Тогда это просто вопрос, проходящий через него, помня длину вашей текущей последовательности и обнаружив, когда она закончится. Честно говоря, я подозреваю, что простой цикл foreach - это самый простой способ сделать это - я не могу сразу думать о каких-то чудесно опрятных LINQ-подобных способах этого. Конечно, вы могли бы сделать это в блоке итератора, если хотите, но имейте в виду, что заказ списка начинается с того, что у вас есть разумная "авансовая" стоимость в любом случае. Поэтому мое решение будет выглядеть примерно так:
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: Хорошо, я только что подумал о более чем LINQ-ish способе делать вещи. У меня нет времени полностью реализовать его, но:
- Порядок последовательности
- Используйте что-то вроде
SelectWithPrevious
(вероятно, лучше с именем SelectConsecutive
), чтобы получить последовательные пары элементов
- Используйте перегрузку Select, которая включает индекс для получения кортежей (индекс, текущий, предыдущий)
- Отфильтруйте любые элементы, где (текущий = предыдущий + 1), чтобы получить место, которое считается началом последовательности (специальный регистр index = 0)
- Используйте
SelectWithPrevious
для результата, чтобы получить длину последовательности между двумя начальными точками (вычтите один индекс из предыдущего)
- Отфильтруйте любую последовательность длиной менее 3
Я подозреваю, что вам нужно выполнить int.MinValue
в упорядоченной последовательности, чтобы гарантировать, что последний элемент используется правильно.
EDIT: Хорошо, я реализовал это. Это о LINQ наиболее эффективном способе, который я могу придумать для этого... Я использовал нулевые значения как значения "дозорного", чтобы заставить начальную и конечную последовательности - см. Комментарии для более подробной информации.
В целом я бы не рекомендовал это решение. Трудно отвести голову, и хотя я уверен, что это правильно, мне потребовалось некоторое время подумать о возможных ошибках "один за другим" и т.д. Это интересное путешествие в то, что вы можете сделать с LINQ... а также что вы, вероятно, не должны.
О, и обратите внимание, что я подтолкнул "минимальную длину 3" к абоненту - когда у вас есть последовательность кортежей, подобная этому, очистите ее отдельно, 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));
}
}
Ответ 3
Я думаю, что мое решение более изящно и просто, и поэтому легче проверить правильность:
/// <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);
}
Преимущества для других решений:
- Он может найти последовательности, которые перекрываются.
- Его правильно повторное использование и документирование.
- Я не нашел никаких ошибок; -)
Ответ 4
Вот как решить проблему в способе "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()));
Из-за копирования и реконструкции массива это решение, конечно же, не так эффективно, как традиционное решение с циклами.
Ответ 5
Не 100% Linq, но здесь общий вариант:
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()));
}
Ответ 6
Как насчет сортировки массива, тогда создайте другой массив, который является разницей между каждым элементом предыдущего
sortedArray = 8, 9, 10, 21, 22, 23, 24, 27, 30, 31, 32
diffArray = 1, 1, 11, 1, 1, 1, 3, 3, 1, 1
Теперь итерации через разностный массив; если разностные выражения 1, увеличьте количество переменных, sequenceLength, на 1. Если разница равнa > 1, проверьте последовательность Length, если она равнa >= 2, тогда у вас есть последовательность по крайней мере из трех последовательных элементов. Затем reset sequenceLenght to 0 и продолжить цикл в разностном массиве.
Ответ 7
Здесь мой снимок:
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()));
}
}
}
Это возвращает конкретные элементы, которые образуют подпоследовательности и разрешает любой тип элемента и любое определение критериев, если оно может быть определено путем сравнения соседних элементов.
Ответ 8
Вот решение, с которым я столкнулся в F #, довольно легко перевести его в запрос С# LINQ, поскольку сфотография в значительной степени эквивалентна оператору агрегации 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
Ответ 9
Linq не является решением для всего, иногда лучше с простым циклом. Здесь решение, всего лишь немного Linq, чтобы упорядочить исходные последовательности и фильтровать результаты
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();
}
Ответ 10
Я подумал о том же, что Jon: для представления целого ряда целых чисел все, что вам действительно нужно, - это два целых числа! Поэтому я бы начал там:
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
}
Оттуда вы можете написать статический метод, чтобы вернуть кучу этих значений Range
, соответствующих последовательным номерам вашей последовательности.
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;
}
}
Демо:
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()));
}
Вывод:
7, 8, 9
20, 21, 22, 23
Ответ 11
Здесь мой LINQ-y принимает проблему:
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;
}
- не использует явные циклы, но все равно должен быть O (n lg n)
- использует
SortedSet
вместо .OrderBy().Distinct()
- объединяет последовательный элемент с
list.Zip(list.Skip(1))
Ответ 12
Здесь решение, использующее словарь вместо сортировки...
Он добавляет элементы в словарь, а затем для каждого значения увеличивается выше и ниже, чтобы найти самую длинную последовательность.
Это не строго LINQ, хотя он использует некоторые функции LINQ, и я думаю, что это более читаемо, чем чистое решение LINQ.
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;
}