【发布时间】:2018-06-25 05:36:32
【问题描述】:
为什么
在跨两个列表进行比较和重复数据删除时,编码人员在时间压力下通常不会找到运行时效率最高的实现。两个嵌套的 for 循环是许多程序员常用的 goto 解决方案。有人可能会尝试使用 LINQ 进行 CROSS JOIN,但这显然效率低下。为此,编码人员需要一种令人难忘且代码高效的方法,而且运行时效率也相对较高。
这个问题是在看到一个更具体的问题后提出的:Delete duplicates in a single dataset relative to another one in C# - 它更专业地使用数据集。 “数据集”一词在未来对人们没有帮助。没有找到其他笼统的问题。
什么
我使用术语列表/集合来帮助解决这个更一般的编码问题。
var setToDeduplicate = new List<int>() { 1,2,3,4,5,6,7,8,9,10,11,.....}; //All integer values 1-1M
var referenceSet = new List<int>() { 1,3,5,7,9,....}; //All odd integer values 1-1M
var deduplicatedSet = deduplicationFunction(setToDeduplicate, referenceSet);
通过实现 deduplicationFunction 函数,输入数据和输出应该是清晰的。输出可以是 IEnumerable。此输入示例中的预期输出将是 1-1M {2,4,6,8,...}
中的偶数注意:referenceSet 中可能存在重复项。两组中的值仅供参考,因此我不是在寻找数学解决方案 - 这也适用于两组中的随机数输入。
如果用简单的 LINQ 函数来解决这个问题,它会太慢 O(1M*0.5M)。对于如此大的集合,需要一种更快的方法。
速度很重要,但通过大量代码进行的增量改进将没有多大价值。此外,理想情况下,它适用于其他数据类型,包括数据模型对象,但回答这个特定问题就足够了。其他数据类型只会涉及更多的预处理或对答案的轻微更改。
解决方案总结
这是测试代码,结果如下:
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Preparing...");
List<int> set1 = new List<int>();
List<int> set2 = new List<int>();
Random r = new Random();
var max = 10000;
for (int i = 0; i < max; i++)
{
set1.Add(r.Next(0, max));
set2.Add(r.Next(0, max/2) * 2);
}
Console.WriteLine("First run...");
Stopwatch sw = new Stopwatch();
IEnumerable<int> result;
int count;
while (true)
{
sw.Start();
result = deduplicationFunction(set1, set2);
var results1 = result.ToList();
count = results1.Count;
sw.Stop();
Console.WriteLine("Dictionary and Where - Count: {0}, Milliseconds: {1:0.00}.", count, sw.ElapsedTicks / (decimal)10000);
sw.Reset();
sw.Start();
result = deduplicationFunction2(set1, set2);
var results2 = result.ToList();
count = results2.Count;
sw.Stop();
Console.WriteLine(" HashSet ExceptWith - Count: {0}, Milliseconds: {1:0.00}.", count, sw.ElapsedTicks / (decimal)10000);
sw.Reset();
sw.Start();
result = deduplicationFunction3(set1, set2);
var results3 = result.ToList();
count = results3.Count;
sw.Stop();
Console.WriteLine(" Sort Dual Index - Count: {0}, Milliseconds: {1:0.00}.", count, sw.ElapsedTicks / (decimal)10000);
sw.Reset();
sw.Start();
result = deduplicationFunction4(set1, set2);
var results4 = result.ToList();
count = results3.Count;
sw.Stop();
Console.WriteLine("Presorted Dual Index - Count: {0}, Milliseconds: {1:0.00}.", count, sw.ElapsedTicks / (decimal)10000);
sw.Reset();
set2.RemoveAt(set2.Count - 1); //Remove the last item, because it was added in the 3rd test
sw.Start();
result = deduplicationFunction5(set1, set2);
var results5 = result.ToList();
count = results5.Count;
sw.Stop();
Console.WriteLine(" Nested Index - Count: {0}, Milliseconds: {1:0.00}.", count, sw.ElapsedTicks / (decimal)10000);
sw.Reset();
Console.ReadLine();
Console.WriteLine("");
Console.WriteLine("Next Run");
Console.WriteLine("");
}
}
//Returns an IEnumerable from which more can be chained or simply terminated with ToList by the caller
static IEnumerable<int> deduplicationFunction(List<int> Set, List<int> Reference)
{
//Create a hashset first, which is much more efficient for searching
var ReferenceHashSet = Reference
.Distinct() //Inserting duplicate keys in a dictionary will cause an exception
.ToDictionary(x => x, x => x); //If there was a ToHashSet function, that would be nicer
int throwAway;
return Set.Distinct().Where(y => ReferenceHashSet.TryGetValue(y, out throwAway) == false);
}
//Returns an IEnumerable from which more can be chained or simply terminated with ToList by the caller
static IEnumerable<int> deduplicationFunction2(List<int> Set, List<int> Reference)
{
//Create a hashset first, which is much more efficient for searching
var SetAsHash = new HashSet<int>();
Set.ForEach(x =>
{
if (SetAsHash.Contains(x))
return;
SetAsHash.Add(x);
}); // .Net 4.7.2 - ToHashSet will reduce this code to a single line.
SetAsHash.ExceptWith(Reference); // This is ultimately what we're testing
return SetAsHash.AsEnumerable();
}
static IEnumerable<int> deduplicationFunction3(List<int> Set, List<int> Reference)
{
Set.Sort();
Reference.Sort();
Reference.Add(Set[Set.Count - 1] + 1); //Ensure the last set item is non-duplicate for an In-built stop clause. This is easy for int list items, just + 1 on the last item.
return deduplicationFunction4(Set, Reference);
}
static IEnumerable<int> deduplicationFunction4(List<int> Set, List<int> Reference)
{
int i1 = 0;
int i2 = 0;
int thisValue = Set[i1];
int thisReference = Reference[i2];
while (true)
{
var difference = thisReference - thisValue;
if (difference < 0)
{
i2++; //Compare side is too low, there might be an equal value to be found
if (i2 == Reference.Count)
break;
thisReference = Reference[i2];
continue;
}
if (difference > 0) //Duplicate
yield return thisValue;
GoFurther:
i1++;
if (i1 == Set.Count)
break;
if (Set[i1] == thisValue) //Eliminates duplicates
goto GoFurther; //I rarely use goto statements, but this is a good situation
thisValue = Set[i1];
}
}
static IEnumerable<int> deduplicationFunction5(List<int> Set, List<int> Reference)
{
var found = false;
var lastValue = 0;
var thisValue = 0;
for (int i = 0; i < Set.Count; i++)
{
thisValue = Set[i];
if (thisValue == lastValue)
continue;
lastValue = thisValue;
found = false;
for (int x = 0; x < Reference.Count; x++)
{
if (thisValue != Reference[x])
continue;
found = true;
break;
}
if (found)
continue;
yield return thisValue;
}
}
}
}
我将使用它来比较多种方法的性能。 (在这个阶段,我对 Hash-approach vs dual-index-on-sorted-approach 特别感兴趣,尽管ExceptWith 提供了一个简洁的解决方案)
目前 10k 个项目的结果(运行良好):
第一次运行
- 字典和位置 - 计数:3565,毫秒:16.38。
- HashSet exceptWith - 计数:3565,毫秒:5.33。
- 对双索引排序 - 计数:3565,毫秒:6.34。
- 预排序双索引 - 计数:3565,毫秒:1.14。
- 嵌套索引 - 计数:3565,毫秒:964.16。
跑得好
- 字典和位置 - 计数:3565,毫秒:1.21。
- HashSet exceptWith - 计数:3565,毫秒:0.94。
- 对双索引排序 - 计数:3565,毫秒:1.09。
- 预排序双索引 - 计数:3565,毫秒:0.76。
- 嵌套索引 - 计数:3565,毫秒:628.60。
选择的答案:
- @backs HashSet.ExceptWith 方法 - 用最少的代码稍微快一点,使用有趣的函数
ExceptWith,但由于缺乏通用性而被削弱,而且有趣的函数鲜为人知。 - 我的答案之一:HashSet > Where(..Contains..) - 只比@backs 慢一点点,但使用的代码模式使用 LINQ,并且在原始元素列表之外非常通用。我相信这是我在编码时发现自己更常见的情况,并且相信许多其他编码人员也是如此。
- 特别感谢@TheGeneral 对一些答案和一些有趣的
unsafe版本进行了基准测试,并帮助使@Backs 回答更有效地进行后续测试。
【问题讨论】:
-
我认为 HashSet 是一个非常通用的解决方案,可以解决一个相当具体的问题。如果您的列表是有序的,则并行扫描它们并比较当前项目是最快的。
-
@PepitoSh 这很好。我已经更新了 OP 以描述列表可能具有随机值。与基于哈希的方法相比,如果您首先对两个列表进行排序作为一种方法,看看性能如何比较会很有趣。如果没有其他人尝试,我将自己尝试并行索引增量方法。这也适用于其他数据类型。
-
如果你有一个集合
1,2,2,2,2,2,3,4,5,6和一个参考集1,3,5,7你想要结果2,4,6? -
@TheGeneral 正确。我会更新帖子以便其他人也清楚。