51

Is there a "nice" way to eliminate consecutive duplicates of list elements?

Example:

["red"; "red"; "blue"; "green"; "green"; "red"; "red"; "yellow"; "white"; "white"; "red"; "white"; "white"] 

should become

["red"; "blue"; "green"; "red"; "yellow"; "white"; "red"; "white"]

--By "nice" I mean most readable and understandable to a new user and fast execution :)


Since this asks only about consecutive duplicates, questions like How do I remove duplicates from a C# array? or Remove duplicates from a List<T> in C# are not applicable.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Wolfy
  • 4,213
  • 8
  • 35
  • 42

12 Answers12

76

A simple and very readable solution:

List<string> results = new List<string>();
foreach (var element in array)
{
    if(results.Count == 0 || results.Last() != element)
        results.Add(element);
}

This is efficient with a List<T>. Specifically, Last() is fast if the sequence implements IList<T>. And List<T> does.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Simon Bartlett
  • 2,010
  • 14
  • 15
19

You can roll your own, linq-style.

// For completeness, this is two methods to ensure that the null check 
// is done eagerly while the loop is done lazily. If that's not an issue, 
// you can forego the check and just use the main function.

public static IEnumerable<T> NonConsecutive<T>(this IEnumerable<T> input)
{
  if (input == null) throw new ArgumentNullException("input");
  return NonConsecutiveImpl(input);
}

static IEnumerable<T> NonConsecutiveImpl<T>(this IEnumerable<T> input)
{
   bool isFirst = true;
   T last = default(T);
   foreach (var item in input) {
      if (isFirst || !object.Equals(item, last)) {
          yield return item;
          last = item;
          isFirst = false;
      }
   }
}

And use as

array.NonConsecutive().ToArray()

The advantage is that it's lazily evaluated, so you can use it on any enumeration without having to consume it in its entirety, and chain it with other linq methods (eg: array.Where(i => i != "red").NonConsecutive().Skip(1).ToArray()). If you don't have that requirement and you just want to work with arrays, something like Simon Bartlett's solution might be slightly more performant.

For more information on why it has to be two methods, see here

Alex J
  • 9,905
  • 6
  • 36
  • 46
8

You can create simple generic method for this purpose, like below:

[EDIT 2] (great thanks to Eric Lippert)

    public static List<T> ExcludeConsecutiveDuplicates<T>(List<T> InputList)
    {
        object lastItem = null;
        List<T> result = new List<T>();

        for (int i = 0; i < InputList.Count; i++)
        {
            if (i==0 || Object.Equals(InputList[i],lastItem) != true)
            {
                lastItem = InputList[i];
                result.Add((T)lastItem);
            }
        }

        return result;
    }
Anton Semenov
  • 6,227
  • 5
  • 41
  • 69
  • 4
    Why is it necessary to be able to *sort* the items? All you need is to be able to compare them for equality. – Eric Lippert Apr 20 '11 at 14:47
  • @Eric: sorry, cant clearly get why you are asking about sorting. I specified `IComparable` inteface only to able use `CompareTo` method. did I understand you correctly? – Anton Semenov Apr 20 '11 at 14:52
  • 5
    Right; why do you need CompareTo? CompareTo is for *sorting*; it is for determining which element is *bigger* than another. Why should this algorithm be limited to only types that define a sort order? This algorithm only requires checking for *equality* and you can do that with the convenient "Equals" method that is on every object. – Eric Lippert Apr 20 '11 at 14:57
  • @Eric: Yes, you are right. I fogot about `Equals` method. Ive updated my answer, thank you! – Anton Semenov Apr 20 '11 at 15:08
  • 2
    OK, but we're not done yet. What if its a list of strings and the first string in the list is null? What happens? – Eric Lippert Apr 20 '11 at 15:21
  • @Eric: it was hard case and I had to change my answer again. Thank you again! – Anton Semenov Apr 20 '11 at 19:01
  • 3
    Better! However it is not necessary for you to write IsObjectEquals, since we implement that for you. You can just call the static method System.Object.Equals and it will do the null check for you. – Eric Lippert Apr 20 '11 at 19:19
  • @Eric: Wow! It is a revelation for me, I didnt know about `System.Object.Equals` static method many thanks for you! it was great training for me today :) – Anton Semenov Apr 20 '11 at 19:25
5

I think this is the simplest it can get with Linq:

colors.Where((color, i) => i == 0 || color != colors[i - 1]);

You can try it in C# Interactive:

> var colors = new[] { "red", "red", "blue", "green", "green", "red", "red", "yellow", "white", "white", "red", "white", "white" };
> colors.Where((color, i) => i == 0 || color != colors[i - 1])
WhereIterator { "red", "blue", "green", "red", "yellow", "white", "red", "white" }

The trick here is to use the Where() overload that accepts a predicate with index, then compare to the previous item in the original array.

aschoenebeck
  • 439
  • 7
  • 9
  • This solution is no less readable than the accepted answer. The condition `i == 0 || color != colors[i - 1]` spells out what "consecutive duplicate" means clearly and concisely. – Daniel Chin Jul 27 '22 at 17:10
5

You can do it in LINQ:

list.Aggregate(new List<string>(), 
   (current, next) => {
      if (current.Length <= 0 || current[current.Length-1] != next) current.Add(next);
      return current;
   });

Essentially, this creates an initially-empty list, runs through the entire source list, and only add an item to the target list if it is not the same as the last item of the target list.

You can just as easily (probably easier) do it without LINQ:

var target = new List<string>();
foreach (var item in list) {
   if (target.Length <= 0 || target[target.Length-1] != item) target.Add(item);
}
Stephen Chung
  • 14,497
  • 1
  • 35
  • 48
2

Resolution:

IList<string> stringList = new List<string>() { "red", "red", 
                                                "blue", "green", 
                                                "green", "red", 
                                                "red", "yellow", 
                                                "white", "white", 
                                                "red", "white", "white" };      
  for (int i = 0; i < stringList.Count; i++)
  {
    // select the first element
    string first = stringList[i];

    // select the next element if it exists
    if ((i + 1) == stringList.Count) break;
    string second = stringList[(i + 1)];

    // remove the second one if they're equal
    if (first.Equals(second))
    {
      stringList.RemoveAt((i + 1));
      i--;
    }
  }

correct me in the comments if something is wrong please!

/e: Edited code so it works on "white","white","white","white"

bastianwegge
  • 2,435
  • 1
  • 24
  • 30
2

The advantage of this solution compared to the accepted answer (by Simon Bartlett) is that it doesn't require a source of type IList<T> to be efficient.

public static IEnumerable<TSource> ExcludeConsecutiveDuplicates<TSource>(
    this IEnumerable<TSource> source,
    IEqualityComparer<TSource> comparer = null)
{
    ArgumentNullException.ThrowIfNull(source);
    comparer ??= EqualityComparer<TSource>.Default;
    (TSource Value, bool HasValue) previous = default;
    foreach (var item in source)
    {
        bool isDuplicate = previous.HasValue && comparer.Equals(previous.Value, item);
        if (!isDuplicate) yield return item;
        previous = (item, true);
    }
}

This approach is similar to AlexJ's answer, with the addition of an IEqualityComparer parameter that allows to customize the equality check. I also removed the otherwise correct separation of argument-check and implementation, because this solution is not intended to be of library-grade quality. As for the name I adopted the ExcludeConsecutiveDuplicates from AntonSemenov's answer.

Usage example:

var source = new string[]
{
    "Red", "red", "blue", "green", "green", "red", "red", "yellow",
    "WHITE", "white", "red", "white", "white"
};
var result = source.ExcludeConsecutiveDuplicates(StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"Result: {String.Join(", ", result)}");

Output:

Result: Red, blue, green, red, yellow, WHITE, red, white

Online demo.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

Like this you don't need a new object.

public static void RemoveConsecutiveDuplicates<T>(this List<T> collection)
{
    for (int i = 0; i < collection.Count - 1; i++)
    {
        if (collection[i].Equals(collection[i + 1]))
        {
            collection.RemoveAt(i);
            i--;
        }
    }
}

var collection = new [] { 2, 7, 7, 7, 2, 6, 4 }.ToList();
collection.RemoveConsecutiveDuplicates();
NtFreX
  • 10,379
  • 2
  • 43
  • 63
1

Try this:

using System;    
using System.Linq;    
using System.Collections.Generic;

namespace RemoveDuplicates
{
    class MainClass
    {
        public static void Main (string[] args)
        {

            string[] a = new string[] 
            { "red", "red", "red", "blue", 
                      "green", "green", "red", "red", 
                      "yellow", "white", "white", "red", "white", "white" };

            for(int i = 0; i < a.Length; ++i)
                if (i == a.Length-1 || a[i] != a[i+1])
                    Console.WriteLine(a[i]);

        }
    }
}

Output:

red
blue
green
red
yellow
white
red
white
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
1

Taking @Simon Bartlett's clean approach and improving upon it, you could also perform this generically.

public static IEnumerable<T> UniqueInOrder<T>(IEnumerable<T> iterable)
{
    var returnList = new List<T>();

    foreach (var item in iterable)
    {
        if (returnList.Count == 0 || !returnList.Last().Equals(item))
            returnList.Add(item);
    }

    return returnList;
}
Bonez024
  • 1,365
  • 1
  • 13
  • 21
0

Functional approach:

var input = new[] {"red", "red", "blue", 
                   "green", "green", "red", "red", "yellow",
                   "white", "white", "red", "white", "white"};

var output = input.Aggregate(new List<string>(),
                             (runningOutput, value) =>
                             (runningOutput.LastOrDefault() == value
                                      ? runningOutput
                                      : runningOutput.Append(value)));

Presupposes the existence of an extension method similar to:

static class Ex
{
    public static List<T> Append<T>(this List<T> source, T value)
    {
        return new List<T>(source) { value };
    }
}

Supply your own validation as you feel is necessary.

AakashM
  • 62,551
  • 17
  • 151
  • 186
0

Here is an efficient solution that is an extension method and operates on an IEnumerable:

public static class IEnumerableHelpers
{
    public static IEnumerable<T> EliminateConsecutiveDuplicates<T>(this IEnumerable<T> input)
    {
        var enumerator = input.GetEnumerator();
        if (!enumerator.MoveNext())
            yield break;
        
        yield return enumerator.Current;

        var lastValue = enumerator.Current;

        while (enumerator.MoveNext())
        {
            if (! enumerator.Current.Equals(lastValue))
            {
                yield return enumerator.Current;
                lastValue = enumerator.Current;
            }
        }
    }
}
Johan Franzén
  • 2,355
  • 1
  • 17
  • 15