最近在工作中遇到了一个小的功能,就是需要向一个服务发送请求命令,需要判断请求是否发生变化,如果发生变化了,则重新请求。该问题实际上就是判断两个集合是否相等,只需要记录最后一次请求的元素的集合,然后将其和最新一次进行比较是否相等。需要说明的是这里定义的集合相等是指:两个集合如果元素值一样并且出现的次数也一样,即使顺序不一样也认为是相等,比如集合A={1,2,3,4,4,5} 集合B={1,4,4,2,3,5} 这两个集合也认为是相等的。后面讨论的集合相等都是基于这一假设的。

    就这么个简单的问题,也有不同种解决方法,这里和大家分享一下。

方法一 使用Dictionary计数来实现

    这种方法思路很简单,创建一个Dictionary对象,将第一个集合中的元素作为key添加到Dictionary中,value即为出现的次数。然后遍历第二个集合,如果包含相同的key,则value减1,如果不好含,则直接返回false,表示两个集合不同。最后,如果Dictionary中所有key对应的value都为0即表示两个集合相等,否则不相等。

/// <summary>
/// 判断两个集合是否相等,相等 表示元素值及出现的次数一样即可,顺序可以不一样。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list1"></param>
/// <param name="list2"></param>
/// <returns></returns>
public static bool ScrambledEquals<T>(IEnumerable<T> list1, IEnumerable<T> list2)
{
    //如果集合个数不相等,则集合不同
    if (list1.Count() != list2.Count()) return false;

    var cnt = new Dictionary<T, int>();
    foreach (T s in list1)
    {
        if (cnt.ContainsKey(s))
        {
            cnt[s]++;
        }
        else
        {
            cnt.Add(s, 1);
        }
    }
    foreach (T s in list2)
    {
        if (cnt.ContainsKey(s))
        {
            cnt[s]--;
        }
        else //如果第二个集合中有第一个集合中未包含的元素,表示两个集合不同
        {
            return false;
        }
    }
    return cnt.Values.All(c => c == 0);
}

    算法需要对象实现IEquatable接口,而该接口一般对象均默认实现。

    以上算法的效率是很高的,时间复杂度为O(N),因为对Dictionary的查找时间复杂度为O(1),所以主要时间都花在遍历集合上。

      以上方法对.NET不同的版本都兼容,如果是.NET 2.0版本也非常容易改造,只需要把最后一句代码改为遍历即可。由于项目原因,本人开发环境为2.0所以才用的是该方法。

方法二 使用IEnumerable的SequenceEqual 扩展方法

    在.NET 3.5 中,比较集合元素的相等性有了新的方法,IEnumerable接口提供了名为SequenceEqual的方法,该方法用于判断两个序列是否顺序相等,即两个源序列的长度相等,且其相应元素相等。

    我们可以先对两个集合进行排序,然后直接调用Enumerable.SequenceEqual 方法即可,这大概是最简单的实现方法了。

public static bool ScrambledEqualsUsingSequenceEqual<T>(IEnumerable<T> list1, IEnumerable<T> list2)
{
    return Enumerable.SequenceEqual(list1.OrderBy(t => t), list2.OrderBy(t => t));
}

    需要注意的是,如果要自定义相等特性,需要实现IEquatable<T>接口,并提供自定义的Equal和GetHashCode实现。

方法三 使用CollectionAssert.AreEquivalent方法

    在Visual Studio的单元测试框架中,位于Microsoft.VisualStudio.TestTools.UnitTesting命名空间下,以及在NUnit中都存在有CollectionAssert.AreEquivalent方法,该方法的解释是:

Two collections are equivalent if they have the same elements in the same quantity, but in any order. Elements are equal if their values are equal, not if they refer to the same object.

    从定义可以看出 这正是我们定义的集合相等性。所以可以直接使用该方法。需要注意的是该命名空间位于Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll这个dll中。

Unit Test in VS

    使用Reflector工具,可以查看其具体实现,对其代码进行简单修改可以看到其原理如下:

public class MultiSetComparer<T> : IEqualityComparer<IEnumerable<T>>
{
    public bool Equals(IEnumerable<T> first, IEnumerable<T> second)
    {
        if (first == null)
            return second == null;

        if (second == null)
            return false;

        if (ReferenceEquals(first, second))
            return true;

        var firstCollection = first as ICollection<T>;
        var secondCollection = second as ICollection<T>;
        if (firstCollection != null && secondCollection != null)
        {
            if (firstCollection.Count != secondCollection.Count)
                return false;

            if (firstCollection.Count == 0)
                return true;
        }

        return !HaveMismatchedElement(first, second);
    }

    private static bool HaveMismatchedElement(IEnumerable<T> first, IEnumerable<T> second)
    {
        int firstCount;
        int secondCount;

        var firstElementCounts = GetElementCounts(first, out firstCount);
        var secondElementCounts = GetElementCounts(second, out secondCount);

        if (firstCount != secondCount)
            return true;

        foreach (var kvp in firstElementCounts)
        {
            firstCount = kvp.Value;
            secondElementCounts.TryGetValue(kvp.Key, out secondCount);

            if (firstCount != secondCount)
                return true;
        }

        return false;
    }

    private static Dictionary<T, int> GetElementCounts(IEnumerable<T> enumerable, out int nullCount)
    {
        var dictionary = new Dictionary<T, int>();
        nullCount = 0;

        foreach (T element in enumerable)
        {
            if (element == null)
            {
                nullCount++;
            }
            else
            {
                int num;
                dictionary.TryGetValue(element, out num);
                num++;
                dictionary[element] = num;
            }
        }

        return dictionary;
    }

    public int GetHashCode(IEnumerable<T> enumerable)
    {
        int hash = 17;

        foreach (T val in enumerable.OrderBy(x => x))
            hash = hash * 23 + val.GetHashCode();

        return hash;
    }
}

    其实现方法原理和第一个方法类似,首先是进行了一系列条件判断,是否为空,元素个数是否相等等等。然后也是创建一个Dictionary进行元素个数计算,并比较。