在 .NET 中有效地合并字符串数组,保持不同的值
Posted
技术标签:
【中文标题】在 .NET 中有效地合并字符串数组,保持不同的值【英文标题】:Efficiently merge string arrays in .NET, keeping distinct values 【发布时间】:2010-09-13 20:47:14 【问题描述】:我正在使用 .NET 3.5。我有两个字符串数组,它们可能共享一个或多个值:
string[] list1 = new string[] "apple", "orange", "banana" ;
string[] list2 = new string[] "banana", "pear", "grape" ;
我想要一种方法将它们合并到一个没有重复值的数组中:
"apple", "orange", "banana", "pear", "grape"
我可以用 LINQ 做到这一点:
string[] result = list1.Concat(list2).Distinct().ToArray();
但我认为这对于大型数组来说效率不高。
有没有更好的办法?
【问题讨论】:
【参考方案1】:string[] result = list1.Union(list2).ToArray();
来自msdn:“此方法从返回集中排除重复项。这与 Concat(TSource) 方法的行为不同,后者返回输入序列中的所有元素,包括重复项。”
【讨论】:
我回到这个话题来发布这个解决方案。我相信它在各方面都很理想! 一个小问题,但是 Union 的返回类型是 IEnumerable.NET 3.5 引入了可以做到这一点的 HashSet 类:
IEnumerable<string> mergedDistinctList = new HashSet<string>(list1).Union(list2);
不确定性能,但它应该优于您提供的 Linq 示例。
编辑: 我站得更正了。 Concat 和 Distinct 的惰性实现具有关键的内存和速度优势。 Concat/Distinct 大约快 10%,并且可以保存多个数据副本。
我通过代码确认:
Setting up arrays of 3000000 strings overlapping by 300000
Starting Hashset...
HashSet: 00:00:02.8237616
Starting Concat/Distinct...
Concat/Distinct: 00:00:02.5629681
是输出:
int num = 3000000;
int num10Pct = (int)(num / 10);
Console.WriteLine(String.Format("Setting up arrays of 0 strings overlapping by 1", num, num10Pct));
string[] list1 = Enumerable.Range(1, num).Select((a) => a.ToString()).ToArray();
string[] list2 = Enumerable.Range(num - num10Pct, num + num10Pct).Select((a) => a.ToString()).ToArray();
Console.WriteLine("Starting Hashset...");
Stopwatch sw = new Stopwatch();
sw.Start();
string[] merged = new HashSet<string>(list1).Union(list2).ToArray();
sw.Stop();
Console.WriteLine("HashSet: " + sw.Elapsed);
Console.WriteLine("Starting Concat/Distinct...");
sw.Reset();
sw.Start();
string[] merged2 = list1.Concat(list2).Distinct().ToArray();
sw.Stop();
Console.WriteLine("Concat/Distinct: " + sw.Elapsed);
【讨论】:
实际上,我希望它比 Concat/Distinct 方式效率低,因为 Union 需要形成第二个 HashSet。【参考方案3】:免责声明这是过早的优化。对于您的示例数组,请使用 3.5 扩展方法。在您知道您在该区域存在性能问题之前,您应该使用库代码。
如果您可以对数组进行排序,或者当您到达代码中的那个点时它们已排序,您可以使用以下方法。
这些将从两者中提取一个项目,并产生“最低”项目,然后从相应的来源获取一个新项目,直到两个来源都用尽。如果从两个来源获取的当前项目相等,它将从第一个来源生成一个,并在两个来源中跳过它们。
private static IEnumerable<T> Merge<T>(IEnumerable<T> source1,
IEnumerable<T> source2)
return Merge(source1, source2, Comparer<T>.Default);
private static IEnumerable<T> Merge<T>(IEnumerable<T> source1,
IEnumerable<T> source2, IComparer<T> comparer)
#region Parameter Validation
if (Object.ReferenceEquals(null, source1))
throw new ArgumentNullException("source1");
if (Object.ReferenceEquals(null, source2))
throw new ArgumentNullException("source2");
if (Object.ReferenceEquals(null, comparer))
throw new ArgumentNullException("comparer");
#endregion
using (IEnumerator<T>
enumerator1 = source1.GetEnumerator(),
enumerator2 = source2.GetEnumerator())
Boolean more1 = enumerator1.MoveNext();
Boolean more2 = enumerator2.MoveNext();
while (more1 && more2)
Int32 comparisonResult = comparer.Compare(
enumerator1.Current,
enumerator2.Current);
if (comparisonResult < 0)
// enumerator 1 has the "lowest" item
yield return enumerator1.Current;
more1 = enumerator1.MoveNext();
else if (comparisonResult > 0)
// enumerator 2 has the "lowest" item
yield return enumerator2.Current;
more2 = enumerator2.MoveNext();
else
// they're considered equivalent, only yield it once
yield return enumerator1.Current;
more1 = enumerator1.MoveNext();
more2 = enumerator2.MoveNext();
// Yield rest of values from non-exhausted source
while (more1)
yield return enumerator1.Current;
more1 = enumerator1.MoveNext();
while (more2)
yield return enumerator2.Current;
more2 = enumerator2.MoveNext();
请注意,如果其中一个来源包含重复项,您可能会在输出中看到重复项。如果要在已排序的列表中删除这些重复项,请使用以下方法:
private static IEnumerable<T> CheapDistinct<T>(IEnumerable<T> source)
return CheapDistinct<T>(source, Comparer<T>.Default);
private static IEnumerable<T> CheapDistinct<T>(IEnumerable<T> source,
IComparer<T> comparer)
#region Parameter Validation
if (Object.ReferenceEquals(null, source))
throw new ArgumentNullException("source");
if (Object.ReferenceEquals(null, comparer))
throw new ArgumentNullException("comparer");
#endregion
using (IEnumerator<T> enumerator = source.GetEnumerator())
if (enumerator.MoveNext())
T item = enumerator.Current;
// scan until different item found, then produce
// the previous distinct item
while (enumerator.MoveNext())
if (comparer.Compare(item, enumerator.Current) != 0)
yield return item;
item = enumerator.Current;
// produce last item that is left over from above loop
yield return item;
请注意,这些都不会在内部使用数据结构来保存数据的副本,因此如果输入已排序,它们会很便宜。如果您不能或不会保证,您应该使用您已经找到的 3.5 扩展方法。
下面是调用上述方法的示例代码:
String[] list_1 = "apple", "orange", "apple", "banana" ;
String[] list_2 = "banana", "pear", "grape" ;
Array.Sort(list_1);
Array.Sort(list_2);
IEnumerable<String> items = Merge(
CheapDistinct(list_1),
CheapDistinct(list_2));
foreach (String item in items)
Console.Out.WriteLine(item);
【讨论】:
+1 开箱即用:如果它们被排序怎么办?对于很多代码。再说一次,对它们进行排序所花费的时间可能会超出整个目的。因此免责声明:)【参考方案4】:您为什么认为它会效率低下?据我所知,Concat 和 Distinct 都是惰性求值的,在 Distinct 后台使用 HashSet 来跟踪已经返回的元素。
我不知道你是如何设法让它比一般的方式更有效:)
编辑:Distinct 实际上使用 Set(一个内部类)而不是 HashSet,但要点仍然正确。这是一个很好的例子,说明 LINQ 是多么的简洁。在没有更多领域知识的情况下,最简单的答案几乎与您所能达到的一样有效。
效果相当于:
public static IEnumerable<T> DistinctConcat<T>(IEnumerable<T> first, IEnumerable<T> second)
HashSet<T> returned = new HashSet<T>();
foreach (T element in first)
if (returned.Add(element))
yield return element;
foreach (T element in second)
if (returned.Add(element))
yield return element;
【讨论】:
【参考方案5】:在您测量之前,您不知道哪种方法更快。 LINQ 方式优雅且易于理解。
另一种方法是将集合实现为哈希数组(字典)并将两个数组的所有元素添加到集合中。然后使用 set.Keys.ToArray() 方法创建结果数组。
【讨论】:
【参考方案6】:可能以您的值作为键创建一个哈希表(仅添加那些不存在的键),然后将键转换为数组可能是一个可行的解决方案。
【讨论】:
以上是关于在 .NET 中有效地合并字符串数组,保持不同的值的主要内容,如果未能解决你的问题,请参考以下文章