使用 Linq 查找连续重复的元素。

7

假设我有一个类型为Value的对象列表。 Value有一个Name属性:

private List<Value> values = new List<Value> {
    new Value { Id = 0, Name = "Hello" },
    new Value { Id = 1, Name = "World" },
    new Value { Id = 2, Name = "World" },
    new Value { Id = 3, Name = "Hello" },
    new Value { Id = 4, Name = "a" },
    new Value { Id = 5, Name = "a" },
};

现在我想获取所有“重复”的值的列表(名称属性与前一个元素的名称属性相同的元素)。 在这个例子中,我希望返回一个包含两个元素“world”和“a”(id = 2和5)的列表。
使用Linq可以实现这个功能吗? 当然,我可以像这样做:
List<Value> tempValues = new List<Value>();
String lastName = String.Empty();
foreach (var v in values)
{
    if (v.Name == lastName) tempValues.Add(v);
    lastName = v.Name;
}

但是由于我想在更复杂的情境中使用这个查询,也许有一种“linqish”解决方案。

7个回答

7

虽然没有内置此类功能,但如果您需要经常使用此功能,则可以开发一些定制的通用工具:

static IEnumerable<TSource> WhereRepeated<TSource>(
    this IEnumerable<TSource> source)
{
    return WhereRepeated<TSource,TSource>(source, x => x);
}
static IEnumerable<TSource> WhereRepeated<TSource, TValue>(
    this IEnumerable<TSource> source, Func<TSource, TValue> selector)
{
    using (var iter = source.GetEnumerator())
    {
        if (iter.MoveNext())
        {
            var comparer = EqualityComparer<TValue>.Default;
            TValue lastValue = selector(iter.Current);
            while (iter.MoveNext())
            {
                TValue currentValue = selector(iter.Current);
                if (comparer.Equals(lastValue, currentValue))
                {
                    yield return iter.Current;
                }
                lastValue = currentValue;
            }
        }
    }
}

使用方法:

    foreach (Value value in values.WhereRepeated(x => x.Name))
    {
        Console.WriteLine(value.Name);
    }

您可能需要考虑如何处理三元组等内容 - 目前除第一个元素外,所有元素都将被生成(这与您的描述相符),但这可能并不完全正确。


这种方法比Zip方法更有效率。但我发现Zip方法读起来更好(它做什么更加清晰明了)。 - Sam Saffron

4
你可以实现一个 Zip扩展,然后将列表与.Skip(1)一起压缩,然后选择匹配的行。
这应该很有效并且易于维护:
values
  .Skip(1)
  .Zip(items, (first,second) => first.Name==second.Name?first:null)
  .Where(i => i != null);

这种方法的轻微劣势是需要对列表进行两次迭代。

很棒的解决方案。在我的情况下,性能不是问题(只有几百个元素)。 - Jürgen Steinblock

1

我知道这个问题很古老,但我正在做同样的事情,所以......

static class utils
{
    public static IEnumerable<T> FindConsecutive<T>(this IEnumerable<T> data, Func<T,T,bool> comparison)
    {
        return Enumerable.Range(0, data.Count() - 1)
        .Select( i => new { a=data.ElementAt(i), b=data.ElementAt(i+1)})
        .Where(n => comparison(n.a, n.b)).Select(n => n.a);
    }
}

应该适用于任何东西 - 只需提供一个比较元素的函数


1

我认为这个应该可以(未经测试)-- 这将为你提供重复的单词和它的索引。对于多个重复的单词,你可以遍历这个列表并检查连续的索引。

 var query = values.Where( (v,i) => values.Count > i+1 && v == values[i+1] )
                   .Select( (v,i) => new { Value = v, Index = i } );

1
这让我感觉不太像 LINQ ... 而且不能用于一般的 IEnumerable。 - Sam Saffron
不错 - 我喜欢 :) @ Sam:你说它不是LINQy的意思是什么?对我来说它很LINQy :) (或者如果你真的想变得技术化,Lambday..可以在一瞬间变成LINQy):) - Pure.Krome
@Pure,如果values仅仅是IEnumerable(而不是IList),那么这个方法就无法工作,因此它只是一个真正特定的解决方案,只适用于IList。然而,它确实符合规范并完成了工作。 - Sam Saffron
你可以使用ElementAt()扩展来处理通用的可枚举类型。但我会担心后面需要优化其他情况,因此暂时不去动它们。 - tvanfosson

1
这是另一种简单的方法,如果ID始终像您的示例中那样连续,则应该可以工作:
var data = from v2 in values
            join v1 in values on v2.Id equals v1.Id + 1
            where v1.Name == v2.Name
            select v2;

-1

类似这样的东西

var dupsNames = 
  from v in values
  group v by v.Name into g
  where g.Count > 1 // If a group has only one element, just ignore it
  select g.Key;

应该可以工作。然后您可以在第二个查询中使用结果:

dupsNames.Select( d => values.Where( v => v.Name == d ) )

这应该返回一个以名称为键,值为 { 具有名称的元素 } 的分组。

免责声明:我没有对上述内容进行测试,因此可能完全错误。


1
这将提取任何重复的内容,不仅仅是连续重复。 - tvanfosson

-1
你可以使用 GroupBy 扩展来实现这个功能。

我猜他的意思是这样的: `values.GroupBy(x => x.Name) .Where(x => x.Count() > 0) .Select(x => x.First());` - max_cervantes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接