簡體   English   中英

如何使用LINQ查找與一個謂詞匹配的行中的5個元素,但第六個元素不匹配?

[英]How do I use LINQ to find 5 elements in a row that match one predicate, but where the sixth element doesn't?

我正在嘗試學習LINQ,似乎找到一系列與謂詞匹配的'n'元素應該是可能的,但我似乎無法弄清楚如何解決問題。

我的解決方案實際上需要一個第二,不同的謂詞測試序列的“結束”,但發現不過去的一個考驗, 那些通過測試也將是有趣的至少5個元素的序列之后的第一個元素。

這是我天真的非LINQ方法....

 int numPassed = 0;
 for (int i = 0; i < array.Count - 1; i++ )
 {
     if (FirstTest(array[i]))
     {
        numPassed++;
     }
     else
     {
        numPassed = 0;
     }

     if ((numPassed > 5) && SecondTest(array[i + 1]))
     {
          foundindex = i;
          break;
     }
  }

一個高性能的LINQ解決方案是可能的,但坦率地說非常難看。 我們的想法是隔離與描述匹配的子序列(一系列N個項匹配謂詞,當謂詞找到與第二個謂詞匹配時結束),然后選擇具有最小長度的第一個。

假設參數是:

var data = new[] { 0, 1, 1, 1, 0, 0, 2, 2, 2, 2, 2 };

Func<int, bool> acceptPredicate = i => i != 0;

// The reverse of acceptPredicate, but could be otherwise
Func<int, bool> rejectPredicate = i => i == 0; 

使用GroupBy和一堆丑陋的有狀態代碼可以隔離子序列(這里是固有的尷尬 - 你必須保持非平凡的狀態)。 我們的想法是按人為和任意的“組號”分組,每當我們從一個可接受的子序列移動到一個絕對不可接受的子序列時,選擇一個不同的數字,當反向發生時:

var acceptMode = false;
var groupCount = 0;
var groups = data.GroupBy(i => {
    if (acceptMode && rejectPredicate(i)) {
        acceptMode = false;
        ++groupCount;
    }
    else if (!acceptMode && acceptPredicate(i)) {
        acceptMode = true;
        ++groupCount;
    }
    return groupCount;
});

最后一步(尋找可接受的長度的第一組)是容易的,但有一個最后的缺陷:確保你不要選擇符合規定條件的群體之一:

var result = groups.Where(g => !rejectPredicate(g.First()))
                   .FirstOrDefault(g => g.Count() >= 5);

通過源序列的單次傳遞實現上述所有操作。

請注意,此代碼將接受也結束源序列的一系列項目(即它不會終止,因為我們發現一個項目滿足rejectPredicate但因為我們用完了數據)。 如果您不希望這樣,則需要稍作修改。

看到它在行動

不優雅,但這會起作用:

var indexList = array
                 .Select((x, i) => new 
                     { Item = x, Index = i })
                 .Where(item => 
                     item.Index + 5 < array.Length && 
                     FirstTest(array[item.Index]) && 
                     FirstTest(array[item.Index+1]) && 
                     FirstTest(array[item.Index+2]) && 
                     FirstTest(array[item.Index+3]) && 
                     FirstTest(array[item.Index+4]) && 
                     SecondTest(array[item.Index+5]))
                 .Select(item => item.Index);

使用Enumerator器更加清晰,而不是嘗試組合現有的擴展方法。


例:

IEnumerable<T> MatchThis<T>(IEnumerable<T> source, 
                            Func<T, bool> first_predicate,
                            Int32 times_match,
                            Func<T, bool> second_predicate)
{
    var found = new List<T>();
    using (var en = source.GetEnumerator())
    {
        while(en.MoveNext() && found.Count < times_match)
            if (first_predicate((T)en.Current))
                found.Add((T)en.Current);
            else
                found.Clear();

        if (found.Count < times_match && !en.MoveNext() || !second_predicate((T)en.Current))
            return Enumerable.Empty<T>();

        found.Add((T)en.Current);
        return found;
    }
}

用法:

var valid_seq = new Int32[] {800, 3423, 423423, 1, 2, 3, 4, 5, 200, 433, 32};
var result = MatchThis(valid_seq, e => e<100, 5, e => e>100);

結果:

在此輸入圖像描述

看起來你想要連續的6個元素,前5個匹配謂詞1,最后一個(第6個)匹配謂詞2。 你的非linq版本運行正常,在這種情況下使用linq有點不情願。 並嘗試在一個linq查詢中解決問題使問題更難,這是一個(可能)更清潔的linq解決方案:

int continuous = 5;
var temp = array.Select(n => FirstTest(n) ? 1 : 0);
var result = array.Where((n, index) => 
               index >= continuous 
               && SecondTest(n) 
               && temp.Skip(index - continuous).Take(continuous).Sum() == continuous)
    .FirstOrDefault();

如果你有Morelinq.Batch方法會更容易

var result = array.GetSixth(FirstTest).FirstOrDefault(SecondTest);

internal static class MyExtensions
{
      internal static IEnumerable<T> GetSixth<T>(this IEnumerable<T> source, Func<T, bool> predicate)
       {
            var counter=0;
            foreach (var item in source)
            {
                if (counter==5) yield return item;
                counter = predicate(item) ? counter + 1 : 0;
            }
        }
}

與其他人提到的一樣,LINQ不是這種模式匹配需求的理想解決方案。 但是,它仍然是可能的,而且它不一定是丑陋的:

Func<int, bool> isBody = n => n == 8;
Func<int, bool> isEnd = n => n == 2;
var requiredBodyLength = 5;

//    Index:       0  1  2  3  4  5  6  7  8  9 10 11 12 13 14
int[] sequence = { 6, 8, 8, 9, 2, 1, 8, 8, 8, 8, 8, 8, 8, 2, 5 };
//                                         ^-----------^  ^
//                                             Body      End

// First we stick an index on each element, since that's the desired output.
var indexedSequence = sequence.Select((n, i) => new { Index = i, Number = n }).ToArray();

// Scroll to the right to see comments
var patternMatchIndexes = indexedSequence
    .Select(x => indexedSequence.Skip(x.Index).TakeWhile(x2 => isBody(x2.Number)))             // Skip all the elements we already processed and try to match the body
    .Where(body => body.Count() == requiredBodyLength)                                         // Filter out any body sequences of incorrect length
    .Select(body => new { BodyIndex = body.First().Index, EndIndex = body.Last().Index + 1 })  // Prepare the index of the first body element and the index of the end element
    .Where(x => x.EndIndex < sequence.Length && isEnd(sequence[x.EndIndex]))                   // Make sure there is at least one element after the body and that it's an end element
    .Select(x => x.BodyIndex)                                                                  // There may be more than one matching pattern, get all their indices
    .ToArray();

//patternMatchIndexes.Dump();   // Uncomment in LINQPad to see results

請注意,這種實現根本不具備高效性,它只是作為一種教學輔助工具,用於展示如何在LINQ中完成某些工作,盡管不能以這種方式解決它

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM