【问题标题】:How can I use LINQ to calculate the longest streak?如何使用 LINQ 计算最长的连胜?
【发布时间】:2012-10-16 10:40:25
【问题描述】:

目前,这只是我很好奇的事情,我没有正在处理的任何代码,但我想知道如何实现这一点......

例如,假设我有一个应用程序可以跟踪世界上所有足球队的结果。我想要做的是确定任何给定球队的最长“连胜”纪录。

我想我很可能会有这样的某种数据表:

  • MatchDate 日期时间
  • 团队字符串
  • TeamB 字符串
  • TeamAGoals int
  • TeamBGoals int

例如,我想做的是找到最长的连胜纪录,其中TeamA = "My Team" 显然这意味着TeamAGoals 必须大于TeamBGoals

正如我所说,这只是举例。对于这样的事情,不同的数据库设计可能会更好。但根本问题是如何计算最长的连续/运行匹配结果。

【问题讨论】:

  • 如果您想要“最高效”,您可能一开始就不想使用 LINQ。就我个人而言,除非性能成为问题,否则我更看重易于理解和可维护性而不是效率。如果您不需要它,那么为了提高性能而努力提高性能是没有意义的。
  • @DoctaJonez:公平地说,我只是想尝试鼓励答案来考虑性能。想象一下,如果我想要一个按连胜排序的球队网格——那么表现可能会成为一个问题。但是,我确实想在这一点上坚持使用 LINQ,因此我必须接受由此而损失的任何效率;)
  • 别忘了添加 Where TeamB = "My Team" 和 TeamBGoals > TeamAGoals,或者你是在最长的主场连胜之后吗?或者您的数据库是否包含每个匹配项的 2 个条目?
  • @JamesB:是的,好点子,我想同时获得所有胜利和主场胜利会很有用,但我想应该只是在我知道如何做另一个的时候解决一个问题

标签: c# linq


【解决方案1】:

现在这是一个老问题,但我只需要自己解决同样的问题,并且认为人们可能会对 Rawling 的 LongestStreak 扩展方法的完全 LINQ 实现感兴趣。这使用带有种子和结果选择器的聚合来遍历列表。

    public static int LongestStreak<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> predicate)
    {
        return source.Aggregate(
            new {Longest = 0, Current = 0},
            (agg, element) => predicate(element) ? 
                new {Longest = Math.Max(agg.Longest, agg.Current + 1), Current = agg.Current + 1} : 
                new {agg.Longest, Current = 0},
            agg => agg.Longest);
    }

【讨论】:

  • 谢谢!
【解决方案2】:

没有开箱即用的 LINQ 方法来计算条纹,因此您需要自定义 LINQy 方法,例如

public static int LongestStreak<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    int longestStreak = 0;
    int currentStreak = 0;
    foreach (TSource s in source)
    {
        if (predicate(s))
            currentStreak++;
        else
        {
            if (currentStreak > longestStreak) longestStreak = currentStreak;
            currentStreak = 0;
        }
    }
    if (currentStreak > longestStreak) longestStreak = currentStreak;
    return longestStreak;
}

然后,要使用它,首先将每个“比赛结果”变成一对“团队结果”。

var teamResults = matches.SelectMany(m => new[] {
        new {
            MatchDate = m.MatchDate,
            Team = m.TeamA,
            Won = m.TeamAGoals > m.TeamBGoals },
        new {
            MatchDate = m.MatchDate,
            Team = m.TeamB,
            Won = m.TeamBGoals > m.TeamAGoals }
    });

按团队分组。

var groupedResults = teamResults.GroupBy(r => r.Team);

然后计算条纹。

var streaks = groupedResults.Select(g => new
    {
        Team = g.Key,
        StreakLength = g
            // unnecessary if the matches were ordered originally
            .OrderBy(r => r.MatchDate)
            .LongestStreak(r => r.Won)
    });

如果您只想要最长的连胜,请使用MoreLinq's MaxBy;如果您想全部订购,可以使用OrderByDescending(s =&gt; s.StreakLength)

或者,如果您想一次性完成此操作,并假设 matches 已订购,请使用以下类

class StreakAggregator<TKey>
{
    public Dictionary<TKey, int> Best = new Dictionary<TKey, int>();
    public Dictionary<TKey, int> Current = new Dictionary<TKey, int>();

    public StreakAggregator<TKey> UpdateWith(TKey key, bool success)
    {
        int c = 0;
        Current.TryGetValue(key, out c);
        if (success)
        {
            Current[key] = c + 1;
        }
        else
        {
            int b = 0;
            Best.TryGetValue(key, out b);
            if (c > b)
            {
                Best[key] = c;
            }
            Current[key] = 0;
        }
        return this;
    }

    public StreakAggregator<TKey> Finalise()
    {
        foreach (TKey k in Current.Keys.ToArray())
        {
            UpdateWith(k, false);
        }
        return this;
    }
}

你可以这样做

var streaks = teamResults.Aggregate(
    new StreakAggregator<string>(),
    (a, r) => a.UpdateWith(r.Team, r.Won),
    (a)    => a.Finalise().Best.Select(kvp => 
        new { Team = kvp.Key, StreakLength = kvp.Value }));

OrderBy 或其他任何东西。

【讨论】:

  • 理论上你可以一次性做到这一点(假设源是有序的)而不是这种“分组,然后迭代组”的方法,但我选择写一个更通用的“获得最长streak" 方法并使用它。
  • 添加了单通道版本,虽然它更不透明。
  • 这是一个很好的答案。我会在某个时候进行测试,一旦我这样做了就会接受(假设那时没有其他人能打败你:P)。似乎将其与 Linq to SQL 一起使用将不允许转换为 SQL,并且需要首先查询所有相关结果(即使用内存中的列表)......但也许我错了?
  • 不,遗憾的是,这不会转化为 SQL。我认为“streak”在 SQL 中是一个非常困难的概念——我不想手动编写代码。
【解决方案3】:

您可以通过单个查询获得团队的所有结果:

var results = from m in Matches
            let homeMatch = m.TeamA == teamName
            let awayMatch = m.TeamB == teamName
            let hasWon = (homeMatch && m.TeamAGoals > m.TeamBGoals) || 
                         (awayMatch && m.TeamBGoals > m.TeamAGoals)
            where homeMatch || awayMatch
            orderby m.MatchDate
            select hasWon;

然后只需简单计算最长条纹:

int longestStreak = 0;
int currentStreak = 0;

foreach (var hasWon in results)
{
    if (hasWon)
    {
        currentStreak++;
        if (currentStreak > longestStreak)
            longestStreak = currentStreak;

        continue;
    }

    currentStreak = 0;
}

您可以按原样使用它,提取到方法,或 create IEnumerable extension 用于计算结果中的最长序列。

【讨论】:

  • 如果最长的连续出现在结果列表的末尾,这不会给出错误的答案吗?
  • @MatthewStrawbridge 你是对的,我会更新我的答案。谢谢!
【解决方案4】:

您可以使用string.Split。像这样的:

int longestStreak = 
    string.Concat(results.Select(r => (r.ours > r.theirs) ? "1" : "0"))
          .Split(new[] { '0' })
          .Max(s => s.Length);

或者,更好的是,为IEnumerable&lt;T&gt; 创建一个Split 扩展方法以避免需要通过字符串,如下所示:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items, Predicate<T> p)
{
    while (true)
    {
        items = items.SkipWhile(i => !p(i));
        var trueItems = items.TakeWhile (i => p(i)).ToList();
        if (trueItems.Count > 0)
        {
            yield return trueItems;
            items = items.Skip(trueItems.Count);
        }
        else
        {
            break;
        }
    }   
}

然后你可以简单地这样做:

int longestStreak = results.Split(r => r.ours > r.theirs).Max(g => g.Count());

【讨论】:

  • 这是一个非常有趣的方法。有机会我会去看看
猜你喜欢
  • 1970-01-01
  • 2018-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-26
  • 1970-01-01
相关资源
最近更新 更多