在 .NET 中高效合并字符串数组,保留不同的值
本文关键字:保留 数组 NET 高效 合并 字符串 | 更新日期: 2023-09-27 17:47:21
我正在使用.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();
但我想这对于大型阵列来说不是很有效。
有没有更好的方法?
string[] result = list1.Union(list2).ToArray();
来自 msdn:"此方法从返回集中排除重复项。这与 Concat(TSource) 方法的行为不同,后者返回输入序列中的所有元素,包括重复项。
你为什么认为它会是低效的?据我所知,Concat 和 Distinct 都是懒惰地评估的,在幕后使用 HashSet 让 Distinct 跟踪已经返回的元素。
我不确定您如何设法以一般方式使其更有效率:)
编辑: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;
}
}
}
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);
免责声明 这是过早的优化。对于示例数组,请使用 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);
可能用您的值作为键创建一个哈希表(仅添加那些尚不存在的值),然后将键转换为数组可能是一个可行的解决方案。
在测量之前,您不知道哪种方法更快。LINQ 方式优雅且易于理解。
另一种方法是将集合实现为哈希数组(字典),并将两个数组的所有元素添加到集合中。然后使用设置。Keys.ToArray() 方法创建生成的数组。