I have a lot of code in which I do something like this
bool GetIsUnique(IEnumerable values)
{
return values.Count() == values.Distinct().Count;
}
Two basic rules:
The other methods are going to be faster (they'll short circuit when a repeated value is found, returning false), but, I'd still stick with your version if it were my code.
I would make this a nice extension method
public static bool IsUnique<T>(this IEnumerable<T> list)
{
var hs = new HashSet<T>();
return list.All(hs.Add);
}
Checks that all items can be added to a HashSet.
Your method needs to iterate through the sequence twice, with a few of potential drawbacks:
Count
which needs to iterate the entire sequence each time. There's no reason why you shouldn't break-out early as soon as you know that there's a duplicate value.The following method only needs to iterate through the sequence once, and will break-out early as soon as any duplicate value is encountered:
bool GetIsUnique<T>(IEnumerable<T> values)
{
var set = new HashSet<T>();
foreach (T item in values)
{
if (!set.Add(item))
return false;
}
return true;
}
A quick way of finding the first duplicate, if present, is:
public static bool TryFindFirstDuplicate<T>(this IEnumerable<T> source, out T duplicate)
{
var set = new HashSet<T>();
foreach (var item in source)
{
if (!set.Add(item))
{
duplicate = item;
return true;
}
}
duplicate = default(T);
return false;
}
You would be doing two loops through the data for the above - once to get the count, once to get the distinct count. Especially bad if the first two items are identical! Try something like this:
bool GetIsUnique<T>(IEnumerable<T> values)
{
HashSet<T> hashSet = new HashSet<T>();
foreach(var value in values)
{
if (hashSet.Contains(value))
{
return false;
}
hashSet.Add(value);
}
return true;
}
This one will finish as soon as it finds a duplicate. Obviously it on the speed of the hash lookup but given Distinct uses a set internally I'd still expect it to be quicker.
I'm suprised no one has tested this just yet:
Comparing Gluip's version in the question with JamieC, LukeK, and MrKWatkins, All three answers are better than the asker's version.
Of the three answers, they are all pretty comparable but in most case JamieC's is slightly faster.
When the data has no duplicates or a duplicate is at the end of the IEnumerable, the size or algorithm had no major differences.
When the data has a duplicate near the middle, or at the beginning, Gluip's version in the original question shows its slowness compared to the other three.
The time to check a set appears to scale linearly with the size of set, meaning no algorithm is preferable for large or small sets.
Here's a test program that spits out a CSV that you can load into a spreadsheet program and sort and graph if you like:
Test Program:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AreUniqueTest
{
class Program
{
const int Iterations = 1000;
enum DupeLocation
{
None,
Early,
Center,
Late,
}
enum SetSize
{
Tiny= 10,
Small = 100,
Medium = 1000,
Large = 10000,
Huge = 100000,
}
static void Main()
{
Dictionary<string, Func<IEnumerable<int>, bool>> functions = new Dictionary<string, Func<IEnumerable<int>, bool>>
{
{"Gluip", GetIsUniqueGluip},
{"LukeH", GetIsUniqueLukeH },
{"Jamiec", GetIsUniqueJamiec },
{"MrKWatkins", GetIsUniqueMrKWatkins }
};
var output = new StringBuilder();
Console.WriteLine("Function,SetSize,DupeLocation,TotalTicks,AverageTicks");
output.AppendLine("Function,SetSize,DupeLocation,TotalTicks,AverageTicks");
foreach (SetSize size in Enum.GetValues(typeof(SetSize)))
{
var sizevalue = (int) size;
foreach (DupeLocation location in Enum.GetValues(typeof(DupeLocation)))
{
var data = CreateTestData((int)size, location);
foreach (string functionKey in functions.Keys)
{
var ticks = RunSet(functions[functionKey], data, Iterations);
var avg = ticks / Iterations;
Console.WriteLine($"{functionKey},{sizevalue},{location},{ticks},{avg}");
output.AppendLine($"{functionKey},{sizevalue},{location},{ticks},{avg}");
}
}
}
File.WriteAllText("output.csv", output.ToString());
Process.Start("output.csv");
}
static long RunSet<T>(Func<IEnumerable<T>, bool> getIsUnique, IEnumerable<T> values, int iterations)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < iterations; i++)
{
getIsUnique.Invoke(values);
}
stopwatch.Stop();
return stopwatch.ElapsedTicks;
}
static bool GetIsUniqueLukeH<T>(IEnumerable<T> values)
{
var set = new HashSet<T>();
foreach (T item in values)
{
if (!set.Add(item))
return false;
}
return true;
}
static bool GetIsUniqueGluip<T>(IEnumerable<T> values)
{
return values.Count() == values.Distinct().Count();
}
static bool GetIsUniqueJamiec<T>(IEnumerable<T> list)
{
var hs = new HashSet<T>();
return list.All(hs.Add);
}
static bool GetIsUniqueMrKWatkins<T>(IEnumerable<T> values)
{
HashSet<T> hashSet = new HashSet<T>();
foreach (var value in values)
{
if (hashSet.Contains(value))
{
return false;
}
hashSet.Add(value);
}
return true;
}
static int[] CreateTestData(int size, DupeLocation location)
{
var result = new int[size];
Parallel.For(0, size, i => { result[i] = i; });
return SetDupe(result, location);
}
static int[] SetDupe(int[] values, DupeLocation location)
{
switch (location)
{
case DupeLocation.Early:
values[1] = values[0];
break;
case DupeLocation.Center:
var midpoint = values.Length / 2;
values[midpoint] = values[midpoint + 1];
break;
case DupeLocation.Late:
values[values.Length - 1] = values[values.Length - 2];
break;
// case DupeLocation.None: // do nothing.
}
return values;
}
}
}