使用linq从List<T>中删除连续重复的项

8
我正在寻找一种方法来防止列表中的重复项,但仍然保留顺序。例如:
1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4 

应该转变为

1, 2, 3, 4, 1, 2, 3, 4

我使用了一个for循环,不太优雅地完成了这个任务,检查下一个项目的方法如下:

    public static List<T> RemoveSequencialRepeats<T>(List<T> input) 
    {
        var result = new List<T>();

        for (int index = 0; index < input.Count; index++)
        {
            if (index == input.Count - 1)
            {
                result.Add(input[index]);
            }
            else if (!input[index].Equals(input[index + 1]))
            {
                result.Add(input[index]);
            }
        }

        return result;
    }

有没有更加优雅的方法来完成这个操作,最好使用LINQ?


5
你为什么认为你的解决方案不够优雅? - Dennis
赞同。当您不需要考虑条目与其周围的关系时,LINQ 很好。但是,当需要考虑到周围条目时,编写优秀的老式命令式代码可能更清晰和直接。 - Eldritch Conundrum
@nawfal 这实际上不是重复的问题,另一个问题是如何_识别_连续重复。而这个问题是关于如何从列表中_删除_连续重复的。 - Ed W
1
@Dennis,根据您的输入大小,可能会出现内存抖动的情况。由于其性质,List<T>.Add 可能非常依赖 GC... - Aron
9个回答

12

你可以创建扩展方法:

public static IEnumerable<T> RemoveSequentialRepeats<T>(
      this IEnumerable<T> source)
{
    using (var iterator = source.GetEnumerator())
    {
        var comparer = EqualityComparer<T>.Default;

        if (!iterator.MoveNext())
            yield break;

        var current = iterator.Current;
        yield return current;

        while (iterator.MoveNext())
        {
            if (comparer.Equals(iterator.Current, current))
                continue;

            current = iterator.Current;
            yield return current;
        }
    }        
}

使用方法:

var result = items.RemoveSequentialRepeats().ToList();

2
你可以为此创建一个重载,允许指定自定义比较器。 - Trevor Pilley
1
是的,但这只是一个简单的布尔值。我估计你的代码会缩减到大约... 5行?尽管你会因为在每次迭代中进行布尔检查而受到惩罚...也许你的代码实际上更好^^ - Alxandr
1
有没有哪位点踩的朋友可以解释一下为什么会这样呢? - Sergey Berezovskiy
2
@KingKing - 这是普通的 foreach,只是特殊处理第一个元素。同时展示了如何使用 Equals 来进行通用版本的比较,适用于所有类型。 - Alexei Levenkov
2
@KingKing LINQ也使用委托,但是委托不等同于LINQ :) - Sergey Berezovskiy
显示剩余10条评论

7

您也可以使用纯 LINQ

List<int> list = new List<int>{1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4};
var result = list.Where((x, i) => i == 0 || x != list[i - 1]);

@AlexeiLevenkov 如果是这样的话,您的第一个评论应该是 This would not work for IEnumrable...。但是,在使用 WhereElementAt 之前,我们可以使用 Cast<>OfType<> - King King
1
你可以删除(i > 0)&&这部分,因为这个检查是多余的。 - sloth
@DominicKexel 我不确定,我认为这两个条件也被检查了,看起来只有第一个条件被检查是否为真(在VB中类似于 Or 而不是 OrAlso)。谢谢! - King King
1
@KingKing 是的,||运算符使用短路求值条件 OR 运算符(||)对其 bool 操作数执行逻辑 OR 运算,但仅在必要时才计算其第二个操作数。 - sloth
1
我认为它是常量,没有反射器可以看到,但无论如何,您可以使用 l.Where((x, i) => i == 0 || x != l[i - 1]) - Roman Pekar
显示剩余2条评论

4

您可以编写简单的LINQ:

var l = new int[] { 1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4 };
var k = new Nullable<int>();
var nl = l.Where(x => { var res = x != k; k = x; return res; }).ToArray();

int[8] { 1, 2, 3, 4, 1, 2, 3, 4 }

或者以Pythonic的方式表达(尽我所能)。
l.Zip(l.Skip(1), (x, y) => new[] { x, y })
   .Where(z => z[0] != z[1]).Select(a => a[0])
   .Concat(new[] { l[l.Length - 1] }).ToArray()

int[8] { 1, 2, 3, 4, 1, 2, 3, 4 }

最简单的方法(编辑:看到King King已经建议了这个方法)

l.Where((x, i) => i == l.Length - 1 || x != l[i + 1]).ToArray()
int[8] { 1, 2, 3, 4, 1, 2, 3, 4 }

这并不简单,因为它使用了一个额外的可变变量。 - Display Name
@SargeBorsch同意了,看看我的第三个解决方案。 - Roman Pekar

4
如果你真的非常讨厌这个世界,可以使用纯 LINQ:
var nmbs = new int[] { 1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4, 5 };
var res = nmbs
              .Take(1)
              .Concat(
                      nmbs.Skip(1)
                          .Zip(nmbs, (p, q) => new { prev = q, curr = p })
                          .Where(p => p.prev != p.curr)
                          .Select(p => p.curr));

但需要注意的是,你需要枚举(至少部分)可枚举对象3次(TakeZip的“左”部分和Zip的第一个参数)。这种方法比建立yield方法或直接执行要慢。 解释:
  • 您取第一个数字(.Take(1)
  • 您获取第二个数字之后的所有数字(.Skip(1)),并将其与所有数字配对(.Zip(nmbs)。我们称第一个“集合”中的数字为curr,第二个“集合”中的数字为prev(p, q) => new { prev = q, curr = p } )。然后,只选择不同于前一个数字的数字(.Where(p => p.prev != p.curr)),并从中获取curr值并且舍弃prev值(.Select(p => p.curr)
  • 您连接这两个集合(.Concat(

3

如果你想要一个不依赖于调用内部捕获值的LINQ语句,那么你需要使用一些带有聚合的结构,因为它是唯一可以在操作中携带值的方法。例如,基于Zaheer Ahmed的代码:

array.Aggregate(new List<string>(), 
     (items, element) => 
     {
        if (items.Count == 0 || items.Last() != element)
        {
            items.Add(element);
        }
        return items;
     });

或者你甚至可以尝试不使用if来构建列表:

 array.Aggregate(Enumerable.Empty<string>(), 
    (items, element) => items.Concat(
       Enumerable.Repeat(element, 
           items.Count() == 0 || items.Last() != element ? 1:0 ))
    );

请注意,要在上述示例中使用Aggregate获得合理的性能,您还需要携带最后一个值(Last将不得不在每个步骤上迭代整个序列),但携带{IsEmpty,LastValue,Sequence}三个值的代码在Tuple中看起来非常奇怪。这些示例仅供娱乐目的。
另一种选择是将数组与自身移位1个单位的数组进行Zip,并返回不相等的元素...
更实用的选项是构建过滤值的迭代器:
IEnumerable<string> NonRepeated(IEnumerable<string> values)
{
    string last = null;
    bool lastSet = false;

    foreach(var element in values)
    {
       if (!lastSet || last != element)
       {
          yield return element;
       }
       last = element;
       lastSet = true;
    }
 }

2
检查新列表的最后一个项目和当前项目是否不同,如果不同则添加到新列表中:
List<string> results = new List<string>();
results.Add(array.First());
foreach (var element in array)
{
    if(results[results.Length - 1] != element)
        results.Add(element);
}

或者使用LINQ:

List<int> arr=new List<int>(){1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4 };
List<int> result = new List<int>() { arr.First() };
arr.Select(x =>
               {
                if (result[result.Length - 1] != x) result.Add(x);
                    return x;
               }).ToList();

请对空对象进行适当的验证。


+1:在循环之前将array.First()添加到列表中可能更有效率。 - Sayse
没问题 :) 当然,这需要现在数组中有一个元素,但我相信 OP 可以处理这个问题 :P - Sayse
除非编译器有非常神奇的优化,否则在更大的列表上性能会非常糟糕。你应该用results[results.Length - 1]替换results.Last()(除非绝对必要,一般不应使用 Last())。虽然第二个只是查找1个变量和偏移量(即2个变量查找),但第一个(使用Last())每次都要遍历(增长)集合。如果您的列表有10个唯一的项目,则第一个项目需要迭代约9!次。 - Alxandr
@ZaheerAhmed 另外,通常不应该将 LINQ 用于副作用(就像您在第二个示例中所做的那样)。 - Alxandr

1

试试这个:

class Program
{
    static void Main(string[] args)
    {
        var input = "1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4 ";
        var list = input.Split(',').Select(i => i.Trim());

        var result = list
            .Select((s, i) => 
                (s != list.Skip(i + 1).FirstOrDefault()) ? s : null)
            .Where(s => s != null)
            .ToList();
    }
}

7
你可以这样初始化数组 int[] list = {1, 2, 3, 4, 4, 4, 1, 1, 2, 3, 4, 4} - 简单多了 :) - Sergey Berezovskiy

1
这是您需要的代码:


public static List<int> RemoveSequencialRepeats(List<int> input)
{
     var result = new List<int>();

     result.Add(input.First());
     result.AddRange(input.Where(p_element => result.Last() != p_element);
     return result;
 }

LINQ的魔力在于:
 result.Add(input.First());
 result.AddRange(input.Where(p_element => result.Last() != p_element);

或者您可以创建这样的扩展方法:

public static class Program
{

    static void Main(string[] args)
    {       
        List<int> numList=new List<int>(){1,2,2,2,4,5,3,2};

        numList = numList.RemoveSequentialRepeats();
    }

    public static List<T> RemoveSequentialRepeats<T>(this List<T> p_input)
    {
        var result = new List<T> { p_input.First() };

        result.AddRange(p_input.Where(p_element => !result.Last().Equals(p_element)));

        return result;
    }
}

0
如果你想引用一个 F# 项目,可以写成:


let rec dedupe = function
  | x::y::rest when x = y -> x::dedupe rest
  | x::rest -> x::dedupe rest
  | _ -> []

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