C#实现List<T>去重的多种高效方法

好的,在 C# 中实现 List<T> 去重有多种方法,每种方法都有其特定的应用场景和性能特点。下面我将为您详细介绍几种高效且常用的方法,并从性能和功能角度进行分析。

图片[1]_C#实现List<T>去重的多种高效方法_知途无界

核心思路分类

  1. 使用 LINQ (简洁,适合大多数场景)​
  2. 使用 HashSet (高性能,适合大型列表和复杂类型)​
  3. 使用传统循环 (最可控,可自定义逻辑)​
  4. 使用 DistinctBy (根据特定属性去重,.NET 6+)​

方法一:使用 LINQ 的 Distinct() 方法 (最简洁)

这是最常用、代码最简洁的方法,适用于快速去重。

原理​:Distinct() 方法返回一个序列,其中包含源序列中的唯一元素(基于元素的默认相等比较器)。

代码示例​:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
        
        // 使用 Distinct() 去重,并转换回 List
        List<int> distinctNumbers = numbers.Distinct().ToList();
        
        Console.WriteLine(string.Join(", ", distinctNumbers)); // 输出: 1, 2, 3, 4, 5
    }
}

优点​:

  • 极其简洁,一行代码搞定。
  • 可读性强。

缺点​:

  • 对于自定义类,需要提供正确的 IEqualityComparer<T> 或使用默认的基于所有属性的比较(通常不适用于自定义类)。
  • 性能不是最优的,因为它内部使用了延迟执行和哈希集,但有额外的封装开销。

适用场景​:快速对简单数据类型(int, string, Guid 等)或已正确实现了相等比较器的类型进行去重。


方法二:使用 HashSet<T> (高性能首选)

HashSet<T> 本身就是为存储唯一元素设计的,利用其特性进行去重是性能最高的方法之一。

原理​:遍历列表,将每个元素尝试添加到 HashSet<T> 中。由于 HashSet<T> 不允许重复,重复的元素会被自动忽略。最后再将 HashSet<T> 转换回 List<T>

代码示例​:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
        
        // 使用 HashSet 去重
        HashSet<int> set = new HashSet<int>(numbers);
        List<int> distinctNumbers = set.ToList();
        
        Console.WriteLine(string.Join(", ", distinctNumbers)); // 输出: 1, 2, 3, 4, 5
    }
}

更高效的原地修改方式 (避免创建新列表)​​:
如果你不需要保留原始列表,可以直接清空并重新填充,减少内存分配。

List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
HashSet<int> set = new HashSet<int>(numbers);
numbers.Clear();
numbers.AddRange(set); // AddRange 比循环 Add 更高效
Console.WriteLine(string.Join(", ", numbers)); // 输出: 1, 2, 3, 4, 5

优点​:

  • 性能极高,特别是对于大型列表。HashSet<T> 的查找和插入操作的平均时间复杂度接近 O(1)。
  • 代码清晰易懂。

缺点​:

  • 对于自定义类,同样需要实现 GetHashCode()Equals(object) 方法,或者使用自定义的 IEqualityComparer<T>
  • 会改变元素的顺序(HashSet<T> 不保证顺序)。如果需要保持原始顺序,请看下面的进阶版。

保持顺序的 HashSet 用法​:
HashSet<T> 在 .NET Framework 中不保证顺序,但在 .NET Core/.NET 5+ 中,HashSet 会保持元素的插入顺序。为了在所有版本中确保顺序,可以结合 LinkedHashSet 的思想(在 C# 中可用 Dictionary 模拟)或使用 Distinct()(它保证顺序)。

适用场景​:追求极致性能的去重操作,尤其是对顺序无要求或在使用现代 .NET 运行时。


方法三:使用传统循环与 HashSet (手动控制,保持顺序)

这种方法结合了 HashSet 的高性能和循环的灵活性,​可以保持元素的第一次出现的顺序

原理​:遍历列表,用一个 HashSet<T> 来记录已经出现过的元素。对于每个元素,如果它不在 HashSet<T> 中,就将其加入结果列表和 HashSet<T> 中。

代码示例​:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
        List<int> distinctOrderedNumbers = new List<int>();
        HashSet<int> seen = new HashSet<int>();

        foreach (int number in numbers)
        {
            // 如果 HashSet 中没有这个元素,说明是第一次遇到
            if (seen.Add(number)) // Add 方法在元素已存在时返回 false
            {
                distinctOrderedNumbers.Add(number);
            }
        }

        Console.WriteLine(string.Join(", ", distinctOrderedNumbers)); // 输出: 1, 2, 3, 4, 5 (保持原顺序)
    }
}

优点​:

  • 性能高​(O(n) 时间复杂度)。
  • 保持元素的原始顺序
  • 完全可控,可以在循环中加入任何自定义逻辑。

缺点​:

  • 代码量稍多。

适用场景​:​最常用的高性能去重场景,尤其当需要保持元素顺序时。这是很多框架和库的 Distinct 方法内部可能采用的实现方式。


方法四:根据特定属性去重 (DistinctBy, .NET 6+)

如果要基于对象的某个或多个属性去重(例如,一个 Person 列表,只根据 Id 去重),Distinct() 就不够用了。.NET 6 引入了 DistinctBy 方法完美解决这个问题。

原理​:允许你指定一个键选择器函数,该方法会根据这个键的唯一性来判断元素是否重复。

代码示例​:
假设有一个 Person 类:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

使用 DistinctBy 根据 Id 去重:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<Person> people = new List<Person>
        {
            new Person { Id = 1, Name = "Alice" },
            new Person { Id = 2, Name = "Bob" },
            new Person { Id = 1, Name = "Charlie" }, // Id=1 重复
            new Person { Id = 3, Name = "David" }
        };

        // 根据 Id 属性去重
        List<Person> distinctPeople = people.DistinctBy(p => p.Id).ToList();

        foreach (var person in distinctPeople)
        {
            Console.WriteLine($"Id: {person.Id}, Name: {person.Name}");
        }
        // 输出:
        // Id: 1, Name: Alice
        // Id: 2, Name: Bob
        // Id: 3, Name: David
        // 注意:保留了第一个遇到的 Id=1 的元素 (Alice)
    }
}

优点​:

  • 功能强大,可以轻松实现复杂的去重逻辑。
  • 代码简洁明了。

缺点​:

  • 需要 .NET 6 或更高版本。

适用场景​:需要根据对象的一个或多个特定字段进行去重。


性能对比与总结

方法性能保持顺序代码简洁性适用场景
​**LINQ Distinct()**​良好非常高快速开发,简单类型,无需极致性能
​**HashSet<T> 直接构造**​最佳否 (.NET Core+ 是)追求性能,对顺序无要求或在新版.NET
​**循环 + HashSet<T>**​最佳中等通用高性能场景,需保序
​**DistinctBy (NET 6+)​**​良好非常高根据特定属性去重

如何选择?

  1. 日常快速开发,简单类型​:直接用 ​**list.Distinct().ToList()**。
  2. 追求极致性能,且不关心顺序​:使用 ​**new HashSet<T>(list).ToList()**。
  3. 追求极致性能,且必须保持顺序​:使用 ​**循环 + HashSet<T>​ 或 ​list.Distinct().ToList()**​(Distinct 内部实现类似循环+哈希表,并保证顺序)。
  4. 需要根据对象属性去重 (且环境是 .NET 6+)​​:毫不犹豫选择 ​**list.DistinctBy(x => x.Property).ToList()**。
  5. 自定义去重逻辑(如复杂条件判断)​​:使用 ​传统循环,在其中编写你的判断规则。

对于大多数应用来说,​方法三(循环 + HashSet)​​ 是实现“高效且保持顺序去重”的最可靠和通用的选择。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞50 分享
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容