在一个 int 数组列表中删除重复项

25

有一个包含int数组的列表,例如:

List<int[]> intArrList = new List<int[]>();
intArrList.Add(new int[3] { 0, 0, 0 });
intArrList.Add(new int[5] { 20, 30, 10, 4, 6 });  //this
intArrList.Add(new int[3] { 1, 2, 5 });
intArrList.Add(new int[5] { 20, 30, 10, 4, 6 });  //this
intArrList.Add(new int[3] { 12, 22, 54 });
intArrList.Add(new int[5] { 1, 2, 6, 7, 8 });
intArrList.Add(new int[4] { 0, 0, 0, 0 });

如何删除重复项(指列表中具有相同长度和相同数字的元素)。

例如,您可以删除元素{20, 30, 10, 4, 6},因为它出现了两次。

我考虑对元素大小进行排序,然后循环遍历每个元素并与其余元素比较,但我不确定如何操作。

另一个问题是,如果使用其他数据结构如哈希表是否更好...如果是,如何使用?


2
小心!对于这个问题,一个强大和高效的答案实现起来并不容易,而一个貌似正确但又慢的解决方案则相对容易实现。 - Mick
2
@J.P,那么这个问题没有让你满意的答案吗?如果是这样,为什么呢?太慢了,太丑陋了吗? - Evk
2
@J.P HashSet<int>,注意名称中的“set”。它将会更加高效,但按原样解决问题是不可行的。这是因为无法存储数据。在集合中,顺序是没有意义的,并且不存在重复项。HashSet还有一个名为SetEquals的方法,比已有解决方案中的比较要更加高效。然而,按原样要求有重复项和顺序是问题的核心。如果没有这些要求,这个问题就很简单。可以考虑使用自定义类或Dictionary<int,int>。我明天会试试看。现在我要睡觉 :) - Alexandru Clonțea
1
@Evk 抱歉,我想为这个问题开始一个悬赏,原因是这个问题没有得到足够的关注,但不幸的是,我忘记改变悬赏原因选项,所以它以默认选项开始了。实际上,所有的答案都相当好和正确。我不知道是否有一种方法可以编辑悬赏原因,使其不会误导。 - user5032790
1
@J.P 老实说,我不知道这会有什么改变 :) 这个问题已经有4个赞同的答案和一个被接受的答案,所以我不明白为什么它没有得到足够的关注。 - Evk
显示剩余9条评论
8个回答

27

使用 GroupBy

var result = intArrList.GroupBy(c => String.Join(",", c))
                       .Select(c => c.First().ToList()).ToList();

结果:

{0, 0, 0}

{20, 30, 10, 4, 6}

{1, 2, 5}

{12, 22, 54}

{1, 2, 6, 7, 8}

{0, 0, 0, 0}

编辑:如果您想将{1,2,3,4}视为等同于{2,3,4,1},则需要像这样使用OrderBy

var result = intArrList.GroupBy(p => string.Join(", ", p.OrderBy(c => c)))
                       .Select(c => c.First().ToList()).ToList(); 

编辑2: 为了帮助理解LINQ的GroupBy解决方案是如何工作的,请考虑以下方法:

public List<int[]> FindDistinctWithoutLinq(List<int[]> lst)
{
    var dic = new Dictionary<string, int[]>();
    foreach (var item in lst)
    {
        string key = string.Join(",", item.OrderBy(c=>c));

        if (!dic.ContainsKey(key))
        {
            dic.Add(key, item);
        }
    }

    return dic.Values.ToList();
}

1
你也可以实现一个 EqualityComparer 类,并在 LINQ 的 Distinct 方法中使用它。但我认为使用 GroupBy 更简单。 - Salah Akbari
GroupBy 会认为 {1,2,3,4}{2,3,4,1} 相等吗? - edgarmtze
1
你可以在对 c 进行分组之前使用 order by,这样它就会相等。 - Jannik
1
我有这样的想法:var result = intArrList.GroupBy(p => string.Join(", ", p.OrderBy(c => c))).Select(c => c.First().ToList()).ToList(); - Jannik
2
@cMinor...我为您提供了一种方法,以帮助您理解基于LINQ的解决方案是如何工作的。请再次查看更新后的答案。 - Salah Akbari
第一个建议返回的类型与输入列表不同。您使用了太多的ToList()。应该是:var result = intArrList.GroupBy(c => String.Join(",", c)) .Select(c => c.First()).ToList(); - Maxter

12

您可以定义自己的 IEqualityComparer 实现,并将其与 IEnumerable.Distinct 一起使用:

class MyComparer : IEqualityComparer<int[]> 
{
    public int GetHashCode(int[] instance) { return 0; } // TODO: better HashCode for arrays
    public bool Equals(int[] instance, int[] other)
    {
        if (other == null || instance == null || instance.Length != other.Length) return false;

        return instance.SequenceEqual(other);
    }
}

现在,为了从列表中获取唯一值,请按照以下方式编写代码:

var result = intArrList.Distinct(new MyComparer());

如果您想要不同的排列方式,您需要按照以下方式实现自己的比较器:

public bool Equals(int[] instance, int[] other)
{
    if (ReferenceEquals(instance, other)) return true; // this will return true when both arrays are NULL
    if (other == null || instance == null) return false;
    return instance.All(x => other.Contains(x)) && other.All(x => instance.Contains(x));
}

编辑:为了更好地实现GetHashCode,您可以查看此帖子,如@Mick的回答中所建议。


1
这个解决方案会考虑 OP 想要将 {1,2,3,4} 视为与 {2,3,4,1} 相等吗? - Salah Akbari
当然,正如您可以在第二个“Equals”实现中看到的那样。或者我错过了什么吗? - MakePeaceGreatAgain

8

我从这里这里借鉴了一些代码。实现一个更通用的GetHashCode可能会使它更通用,但是我认为以下实现是最健壮的。

class Program
{
    static void Main(string[] args)
    {
        List<int[]> intArrList = new List<int[]>();
        intArrList.Add(new int[3] { 0, 0, 0 });
        intArrList.Add(new int[5] { 20, 30, 10, 4, 6 });  //this
        intArrList.Add(new int[3] { 1, 2, 5 });
        intArrList.Add(new int[5] { 20, 30, 10, 4, 6 });  //this
        intArrList.Add(new int[3] { 12, 22, 54 });
        intArrList.Add(new int[5] { 1, 2, 6, 7, 8 });
        intArrList.Add(new int[4] { 0, 0, 0, 0 });

        var test = intArrList.Distinct(new IntArrayEqualityComparer());
        Console.WriteLine(test.Count());
        Console.WriteLine(intArrList.Count());
    }

    public class IntArrayEqualityComparer : IEqualityComparer<int[]>
    {
        public bool Equals(int[] x, int[] y)
        {
            return ArraysEqual(x, y);
        }

        public int GetHashCode(int[] obj)
        {
            int hc = obj.Length;
            for (int i = 0; i < obj.Length; ++i)
            {
                hc = unchecked(hc * 17 + obj[i]);
            }
            return hc;
        }

        static bool ArraysEqual<T>(T[] a1, T[] a2)
        {
            if (ReferenceEquals(a1, a2))
                return true;

            if (a1 == null || a2 == null)
                return false;

            if (a1.Length != a2.Length)
                return false;

            EqualityComparer<T> comparer = EqualityComparer<T>.Default;
            for (int i = 0; i < a1.Length; i++)
            {
                if (!comparer.Equals(a1[i], a2[i])) return false;
            }
            return true;
        }
    }
}

编辑:任意类型数组的IEqualityComparer的通用实现:

public class ArrayEqualityComparer<T> : IEqualityComparer<T[]>
{
    public bool Equals(T[] x, T[] y)
    {
        if (ReferenceEquals(x, y))
            return true;

        if (x == null || y == null)
            return false;

        if (x.Length != y.Length)
            return false;

        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < x.Length; i++)
        {
            if (!comparer.Equals(x[i], y[i])) return false;
        }
        return true;
    }

    public int GetHashCode(T[] obj)
    {
        int hc = obj.Length;
        for (int i = 0; i < obj.Length; ++i)
        {
            hc = unchecked(hc * 17 + obj[i].GetHashCode());
        }
        return hc;
    }
}
编辑2: 如果数组中整数的顺序不重要,我会
var test = intArrList.Select(a => a.OrderBy(e => e).ToArray()).Distinct(comparer).ToList();

不错的 HashCode 实现。 - MakePeaceGreatAgain
我不会为此而取得功劳,正如所述,这是来自其中一个链接的内容。一个优秀的哈希码实现是一件相当棘手的事情。一个普通的哈希码实现将在99.99999%的时间内工作,并在0.00001%的时间里让你受到相当严重的打击。 - Mick
这就是为什么我在回答中集中精力讲有用的东西 :). 不过回答很好,已点赞。 - MakePeaceGreatAgain
对于 x!= null 且 y == null,返回 false,这很公平。无论如何,我认为这个解决方案是最有效的,也更适用于更大的列表(在内存方面)相比接受的答案。尝试想出自己的答案,但遗憾的是集合理论不喜欢重复项。不过有一个问题:你认为 GroupBy(l => l.Count),然后 SelectMany.Distinct,并仅对需要比较的列表进行排序,即 group.Count>1,会更好吗?我的直觉是这将导致更少的比较,但也许这是 CLR 已经优化的东西? - Alexandru Clonțea
我的意思是在组内使用Distinct(相同计数)然后再使用SelectMany。当然,我很可能完全错了。我知道GroupBy也很昂贵,但对我来说,它似乎值得减少对所有列表进行排序并在整个列表上调用distinct的成本。 - Alexandru Clonțea

4
List<int[]> CopyString1 = new List<int[]>();
CopyString1.AddRange(intArrList);
List<int[]> CopyString2 = new List<int[]>();
CopyString2.AddRange(intArrList);
for (int i = 0; i < CopyString2.Count(); i++)
{
    for (int j = i; j < CopyString1.Count(); j++)
    {
        if (i != j && CopyString2[i].Count() == CopyString1[j].Count())
        {
            var cnt = 0;
            for (int k = 0; k < CopyString2[i].Count(); k++)
            {
                if (CopyString2[i][k] == CopyString1[j][k])
                    cnt++;
                else
                    break;
            }
            if (cnt == CopyString2[i].Count())
                intArrList.RemoveAt(i);
        }
    }
}

你测试过这个实现了吗?第一次移除后,RemoveAt(i) 显然是不正确的。因为在你遍历 i 通过列表时,当你移除第一个条目后,CopyString1intArrList 中的元素将不再对齐。 - Trevor

3

使用BenchmarkDotNet对@S.Akbari和@Mick的解决方案进行性能比较。

编辑:

SAkbari_FindDistinctWithoutLinq存在冗余调用ContainsKey,因此我添加了改进并更快的版本:SAkbari_FindDistinctWithoutLinq2

                           方法 |     平均值 |     误差 |    标准偏差 |
--------------------------------- |---------:|----------:|----------:|
  SAkbari_FindDistinctWithoutLinq | 4.021 us | 0.0723 us | 0.0676 us |
 SAkbari_FindDistinctWithoutLinq2 | 3.930 us | 0.0529 us | 0.0495 us |
         SAkbari_FindDistinctLinq | 5.597 us | 0.0264 us | 0.0234 us |
            Mick_UsingGetHashCode | 6.339 us | 0.0265 us | 0.0248 us |
BenchmarkDotNet=v0.10.13, 操作系统=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-7700 CPU 3.60GHz (Kaby Lake),1个CPU,8个逻辑核心和4个物理核心
频率=3515625 Hz,分辨率=284.4444 ns,计时器=TSC
.NET Core SDK=2.1.100
  [主机]     : .NET Core 2.0.5 (CoreCLR 4.6.26020.03, CoreFX 4.6.26018.01),64位 RyuJIT
  DefaultJob : .NET Core 2.0.5 (CoreCLR 4.6.26020.03, CoreFX 4.6.26018.01),64位 RyuJIT

基准测试:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    public class Program
    {
        List<int[]> intArrList = new List<int[]>
        {
            new int[] { 0, 0, 0 },
            new int[] { 20, 30, 10, 4, 6 },  //this
            new int[] { 1, 2, 5 },
            new int[] { 20, 30, 10, 4, 6 },  //this
            new int[] { 12, 22, 54 },
            new int[] { 1, 2, 6, 7, 8 },
            new int[] { 0, 0, 0, 0 }
        };

        [Benchmark]
        public List<int[]> SAkbari_FindDistinctWithoutLinq() => FindDistinctWithoutLinq(intArrList);

        [Benchmark]
        public List<int[]> SAkbari_FindDistinctWithoutLinq2() => FindDistinctWithoutLinq2(intArrList);

        [Benchmark]
        public List<int[]> SAkbari_FindDistinctLinq() => FindDistinctLinq(intArrList);

        [Benchmark]
        public List<int[]> Mick_UsingGetHashCode() => FindDistinctLinq(intArrList);

        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Program>();
        }

        public static List<int[]> FindDistinctWithoutLinq(List<int[]> lst)
        {
            var dic = new Dictionary<string, int[]>();
            foreach (var item in lst)
            {
                string key = string.Join(",", item.OrderBy(c => c));

                if (!dic.ContainsKey(key))
                {
                    dic.Add(key, item);
                }
            }

            return dic.Values.ToList();
        }

        public static List<int[]> FindDistinctWithoutLinq2(List<int[]> lst)
        {
            var dic = new Dictionary<string, int[]>();

            foreach (var item in lst)
                dic.TryAdd(string.Join(",", item.OrderBy(c => c)), item);

            return dic.Values.ToList();
        }

        public static List<int[]> FindDistinctLinq(List<int[]> lst)
        {
            return lst.GroupBy(p => string.Join(", ", p.OrderBy(c => c)))
                       .Select(c => c.First().ToArray()).ToList();
        }

        public static List<int[]> UsingGetHashCode(List<int[]> lst)
        {
            return lst.Select(a => a.OrderBy(e => e).ToArray()).Distinct(new IntArrayEqualityComparer()).ToList();
        }
    }

    public class IntArrayEqualityComparer : IEqualityComparer<int[]>
    {
        public bool Equals(int[] x, int[] y)
        {
            return ArraysEqual(x, y);
        }

        public int GetHashCode(int[] obj)
        {
            int hc = obj.Length;
            for (int i = 0; i < obj.Length; ++i)
            {
                hc = unchecked(hc * 17 + obj[i]);
            }
            return hc;
        }

        static bool ArraysEqual<T>(T[] a1, T[] a2)
        {
            if (ReferenceEquals(a1, a2))
                return true;

            if (a1 == null || a2 == null)
                return false;

            if (a1.Length != a2.Length)
                return false;

            EqualityComparer<T> comparer = EqualityComparer<T>.Default;
            for (int i = 0; i < a1.Length; i++)
            {
                if (!comparer.Equals(a1[i], a2[i])) return false;
            }
            return true;
        }
    }
}

2

输入列表;

List<List<int>> initList = new List<List<int>>();
initList.Add(new List<int>{ 0, 0, 0 });
initList.Add(new List<int>{ 20, 30, 10, 4, 6 });  //this
initList.Add(new List<int> { 1, 2, 5 });
initList.Add(new List<int> { 20, 30, 10, 4, 6 });  //this
initList.Add(new List<int> { 12, 22, 54 });
initList.Add(new List<int> { 1, 2, 6, 7, 8 });
initList.Add(new List<int> { 0, 0, 0, 0 });

您可以创建一个结果列表,并在添加元素之前检查它是否已经被添加。我只是比较了列表计数,并使用p.Except(item).Any()调用来检查列表是否包含该元素。

List<List<int>> returnList = new List<List<int>>();

foreach (var item in initList)
{
    if (returnList.Where(p => !p.Except(item).Any() && !item.Except(p).Any()
                             && p.Count() == item.Count() ).Count() == 0)
    returnList.Add(item);
}

1
你可以使用HashSet。HashSet是用于保证唯一性并且可以比较集合中的项、交集、并集等的集合。 优点:无重复项,易于操作数据组,更高效 缺点:不能获取集合中的特定项,例如:list[0]对于HashSets无效。只能枚举项。例如:foreach
以下是一个示例:
using System;
using System.Collections.Generic;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            HashSet<HashSet<int>> intArrList = new HashSet<HashSet<int>>(new HashSetIntComparer());
            intArrList.Add(new HashSet<int>(3) { 0, 0, 0 });
            intArrList.Add(new HashSet<int>(5) { 20, 30, 10, 4, 6 });  //this
            intArrList.Add(new HashSet<int>(3) { 1, 2, 5 });
            intArrList.Add(new HashSet<int>(5) { 20, 30, 10, 4, 6 });  //this
            intArrList.Add(new HashSet<int>(3) { 12, 22, 54 });
            intArrList.Add(new HashSet<int>(5) { 1, 2, 6, 7, 8 });
            intArrList.Add(new HashSet<int>(4) { 0, 0, 0, 0 });

            // Checking the output
            foreach (var item in intArrList)
            {
                foreach (var subHasSet in item)
                {
                    Console.Write("{0} ", subHasSet);
                }

                Console.WriteLine();
            }            

            Console.Read();
        }

        private class HashSetIntComparer : IEqualityComparer<HashSet<int>>
        {
            public bool Equals(HashSet<int> x, HashSet<int> y)
            {
                // SetEquals does't set anything. It's a method for compare the contents of the HashSet. 
                // Such a poor name from .Net
                return x.SetEquals(y);
            }

            public int GetHashCode(HashSet<int> obj)
            {
                //TODO: implemente a better HashCode
                return base.GetHashCode();
            }
        }
    }
}


Output:
0
20 30 10 4 6
1 2 5
12 22 54
1 2 6 7 8

注意:由于0重复多次,HashSet只会将0计算一次。如果您需要区分0 0 0 0和0 0 0,则可以将 HashSet<HashSet<int>> 替换为 HashSet<List<int>>并针对List实现一个比较器。 您可以使用此链接了解如何比较列表: https://social.msdn.microsoft.com/Forums/en-US/2ff3016c-bd61-4fec-8f8c-7b6c070123fa/c-compare-two-lists-of-objects?forum=csharplanguage 如果你想学习更多关于集合和数据类型的知识,这门课程是一个完美的学习场所:https://app.pluralsight.com/player?course=csharp-collections&author=simon-robinson&name=csharp-collections-fundamentals-m9-sets&clip=1&mode=live

1
使用MoreLINQ和DistinctBy,这可以变得非常简单。
var result = intArrList.DistinctBy(x => string.Join(",", x));

与GroupBy答案类似,如果您希望区分不考虑顺序,请在连接中排序。
var result = intArrList.DistinctBy(x => string.Join(",", x.OrderBy(y => y)));

编辑:这是它的实现方式

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

            return _(); IEnumerable<TSource> _()
            {
                var knownKeys = new HashSet<TKey>(comparer);
                foreach (var element in source)
                {
                    if (knownKeys.Add(keySelector(element)))
                        yield return element;
                }
            }
        }

如果您不需要使用MoreLINQ的其他功能,您可以使用以下方法:
private static IEnumerable<int[]> GetUniqueArrays(IEnumerable<int[]> source)
    {
        var knownKeys = new HashSet<string>();
        foreach (var element in source)
        {
            if (knownKeys.Add(string.Join(",", element)))
                yield return element;
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接