【问题标题】:Fastest way to find duplicate items in a list in C#在 C# 中查找列表中重复项的最快方法
【发布时间】:2017-12-18 17:55:21
【问题描述】:

我知道关于这个主题有很多类似的问题,但我找不到我正在寻找的答案。这是我的要求。

我有一长串字符串(很容易超过 50,000 甚至 100K 项),我需要在其中找到重复项。但是仅仅查找重复项是行不通的;我真正想做的是遍历列表并在每个项目的末尾添加一个增量索引以指示项目重复的次数。为了更好地说明,让我举个例子。我的列表实际上包含路径,所以示例大致类似。

我原来的列表:

AAA\BBB
AAA\CCC
AAA\CCC
BBB\XXX
BBB
BBB\XXX
BBB\XXX

添加了索引的调整列表:

AAA\BBB[1]
AAA\CCC[1]
AAA\CCC[2]
BBB\XXX[1]
BBB[1]
BBB\XXX[2]
BBB\XXX[3]

首先我使用 Linq 尝试了以下方法:

List<string> originalList = new List<string>();
List<string> duplicateItems = new List<string>();

// pathList is a simple List<string> that contains my paths.
foreach (string item in pathList)
{
    // Do some stuff here and pick 'item' only if it fits some criteria.
    if (IsValid(item))
    {
        originalList.Add(item);
        int occurences = originalList.Where(x => x.Equals(item)).Count();
        duplicateItems.Add(item + "[" + occurences + "]");
    }
}

这工作得很好,给了我想要的结果。问题是考虑到我的列表可以包含 10 万个项目,它的速度非常慢。所以我环顾四周,了解到 HashSet 可能是一种可能更有效的替代方案。但我不太清楚如何使用它来获得我想要的结果。

我想我可以试试这样的:

HashSet<string> originalList = new HashSet<string>();
List<string> duplicateItems = new List<string>();

foreach (string item in pathList)
{
    // Do some stuff here and pick 'item' only if it fits some criteria.
    if (IsValid(item))
    {
        if (!originalList.Add(item))
        {
            duplicateItems.Add(item + "[" + ??? + "]");
        }
    }
}

稍后我可以将“[1]”添加到 HashSet 中的所有项目,但是如何在将项目添加到我的重复列表时正确地获得索引(上面标有通用混淆符号,???) ?我无法保留可以传递给我的方法的引用 int,因为可能有数百个不同的重复项,每个重复的次数都不同,如我的示例所示。

我还能使用 HashSet,还是有更好的方法来实现我的目标?即使是指向正确方向的轻微指针也会有很大帮助。

【问题讨论】:

  • 你想把它们都打到最后吗?
  • 最好,是的。但如果它不是太慢且列表不是太多,我也会考虑其他替代方案。
  • 结果列表中元素的顺序重要吗?
  • 您不能将HashSet&lt;string&gt; 用于原始列表,因为HashSet&lt;T&gt; 不存储重复项。
  • @NineBerry 不,它没有。

标签: c# list linq duplicates hashset


【解决方案1】:

你可以试试这个,虽然我还没有进行性能测试:

List<string> originalList = new List<string>()
{
    @"AAA\BBB",
    @"AAA\CCC",
    @"AAA\CCC",
    @"BBB\XXX",
    @"BBB",
    @"BBB\XXX",
    @"BBB\XXX"
};
List<string> outputList = new List<string>();

foreach(var g in originalList.GroupBy(x => x).Select(x => x.ToList()))
{   
    var index = 1;  
    foreach(var item in g)
    {
        outputList.Add(string.Format("{0}[{1}]", item, index++));
    }
}

小提琴here

【讨论】:

  • 是的,这看起来更接近我的预期。
  • 让我试一试并报告。
  • 是的,我确实需要递增计数。我确实尝试了@maccettura 的解决方案,效果很好; 76131 个项目的列表在 1 秒内完成,而 Linq 花费的时间太长。
  • 不错。考虑到要求,我喜欢这个实现。
  • 我在一组 116 个列表中运行了 maccettura、Ivan Stoev 和 Markus 的三个答案,每个列表 10 次。以毫秒为单位得出的平均值如下:maccettura = 13819,Ivan Stoev = 13809,Markus = 12966。所以看起来它们或多或少都相同。
【解决方案2】:

您可以只使用 Group() 将字符串拉在一起,然后使用 value 和 count 的组合来投影这些组。

鉴于您的字符串列表:

var listOfStrings;
var grouped = listOfStrings.GroupBy(x => x);
var groupedCount = grouped.Select(x => new {key = x.Key, count = group.Count()});

【讨论】:

  • 这不会给出每个元素的总数吗?不是像 OP 想要的那样递增?
  • 是的,我在实现中也错过了这一点。
【解决方案3】:

由于您要求最快,最好的 IMO 将使用 foreach 循环并计数 Dictionary&lt;string, int&gt;。它与HashSet 具有相同的时间复杂度,并且比LINQ GroupBy 使用更少的内存:

var counts = new Dictionary<string, int>(pathList.Count); // specify max capacity to avoid rehashing
foreach (string item in pathList)
{
    // Do some stuff here and pick 'item' only if it fits some criteria.
    if (IsValid(item))
    {
        int count;
        counts.TryGetValue(item, out count);
        counts[item] = ++count;
        duplicateItems.Add(item + "[" + count + "]");
    }
}

【讨论】:

  • 谢谢,让我也试试这个并与其他解决方案进行比较。
  • 我在一组 116 个列表中运行了 maccettura、Ivan Stoev 和 Markus 的三个答案,每个列表 10 次。以毫秒为单位得出的平均值如下:maccettura = 13819,Ivan Stoev = 13809,Markus = 12966。所以看起来它们或多或少都相同。
  • Markus 的回答与我的相同。基于 LINQ GroupBy 的解决方案怎么样,正如我在答案中提到的,区别在于使用的内存和 GC 压力 - 如果您有 100K 唯一项,GrouoBy 将额外分配 100K 数组。
  • @IvanStoev @Sach 有趣的是,这两种方法之间的差异几乎是一秒钟,尽管它们都使用字典。我认为您的应该更快,因为您初始化了字典的大小。但是我们的答案之间仍然存在差异。也许这是我在方法中使用的序数字符串比较,迭代器或string.Format
  • @Markus 请注意,尽管上面的时间测量是快速而肮脏的。NET DateTime 值测量仓促完成,可能不是最准确的。因此,请务必在此警告中考虑这些值。
【解决方案4】:

这个怎么样?

    static IEnumerable<string> MyCounter(IEnumerable<string> data)
    {
        var myDic = new Dictionary<string, int>();
        foreach (var d in data)
        {
            if (!myDic.ContainsKey(d))
                myDic[d] = 1;
            else
                myDic[d] = myDic[d] + 1 ;
            yield return d +"[" + myDic[d] + "]";
        }
    }

【讨论】:

  • 谢谢。他的例子是 1 based no?
  • 我不知道我在想什么。对不起,哈哈
【解决方案5】:

您可以遍历列表并使用字典来获取计数,如下所示:

private int GetCount(IDictionary<string, int> counts, string item)
{
  int count;
  if (!counts.TryGetValue(item, out count))
    count = 0;
  count++;
  counts[item] = count;
  return count;
}

private IEnumerable<string> GetItems(IEnumerable<string> items)
{
  // Initialize dict for counts with appropriate comparison
  var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
  foreach(var item in items)
    yield return string.Format("{0}[{1}]", item, GetCount(counts, item));
}

【讨论】:

  • 我在一组 116 个列表中运行了 maccettura、Ivan Stoev 和 Markus 的三个答案,每个列表 10 次。以毫秒为单位得出的平均值如下:maccettura = 13819,Ivan Stoev = 13809,Markus = 12966。所以看起来它们或多或少都相同。
【解决方案6】:

你可以使用这个清晰易懂的代码:

public static void Main()
{
    var originalList  = new List<string>()
    {
        @"AAA\BBB",
        @"AAA\CCC",
        @"AAA\CCC",
        @"BBB\XXX",
        @"BBB",
        @"BBB\XXX",
        @"BBB\XXX"
    };

    var outputList = originalList.GroupBy(x => x).SelectMany(x => x.Select((y, i) => string.Format("{0}[{1}]", y, i + 1)));     

    Console.WriteLine(string.Join("\n", outputList));
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-05
    • 2016-06-09
    • 2017-05-11
    • 1970-01-01
    • 2010-12-13
    • 1970-01-01
    • 2021-07-17
    相关资源
    最近更新 更多