【问题标题】:How to create a HashSet<List<Int>> with distinct elements?如何创建具有不同元素的 HashSet<List<Int>>?
【发布时间】:2011-04-01 19:28:45
【问题描述】:

我有一个包含多个整数列表的 HashSet - 即HashSet&lt;List&lt;int&gt;&gt;

为了保持独特性,我目前必须做两件事: 1. 手动循环现有列表,使用SequenceEquals 查找重复项。 2. 对各个列表进行排序,使SequenceEquals 当前有效。

有没有更好的方法来做到这一点?是否有我可以提供给 HashSet 的现有 IEqualityComparer 以便 HashSet.Add() 可以自动处理唯一性?

var hashSet = new HashSet<List<int>>();

for(/* some condition */)
{
    List<int> list = new List<int>();

    ...

    /* for eliminating duplicate lists */

    list.Sort();

    foreach(var set in hashSet)
    {
        if (list.SequenceEqual(set))
        {
            validPartition = false;
            break;
        }
    }

    if (validPartition)
           newHashSet.Add(list);
}

【问题讨论】:

  • 在这里查看 Jon Skeet 的答案:stackoverflow.com/questions/1023424/…
  • 您能否提供更多关于您实际尝试解决的问题的信息? HashSet&lt;List&lt;int&gt;&gt; 似乎不太可能使用。
  • @marcind,我用它来维护一个数字的所有因式分解的列表。所以对于 24,您可以拥有例如 {4, 2, 3} , {2, 2, 6}等等......我现在使用的算法会创建重复的集合,我希望我知道如何解决这个问题,但遗憾的是我不知道:-/
  • 您可能想将其作为一个单独的问题提出。应该有比您目前尝试的更优雅的解决方案。
  • @CodeInChaos,是的,我绝对认为我应该这样做!任何解决方案都会比我现在遇到的混乱更优雅;-)

标签: c# collections hashset distinct-values


【解决方案1】:

这开始是错误的,它必须是HashSet&lt;ReadOnlyCollection&lt;&gt;&gt;,因为您不能允许列表更改并使集合谓词无效。然后,当您将集合添加到集合时,这允许您在 O(n) 中计算哈希码。如果所有散列结果都相等,则进行 O(n) 测试以检查它是否已经在一个非常罕见的 O(n^2) 最坏情况的集合中。将计算的哈希值与集合一起存储。

【讨论】:

  • 这不像ReadOnlyCollection 保证不变性。如果这个集合没有在公共 API 中公开,那么可变性也没关系。存储计算的哈希值也不是那么重要,因为我认为HashSet&lt;T&gt; 已经存储了它已经包含的元素的哈希值。
  • A ReadOnlyCollection 可以。是存储它还是创建一个覆盖Equals+GetHashCode的派生类取决于OP。
  • 我的意思是,如果您不自己创建 ReadOnlyCollection,那么局外人仍然拥有对基础 IList 的引用,并且可以更改该列表,然后将其反映在 ReadOnlyCollection 中。如果您控制 ReadOnlyCollection 的创建,则可以保证(浅)不变性。 (以及 int 深度不变性)
【解决方案2】:

这是一个可能的比较器,它通过其元素比较 IEnumerable&lt;T&gt;。添加前仍需手动排序。

可以将排序构建到比较器中,但我认为这不是一个明智的选择。添加列表的规范形式似乎更明智。

此代码仅适用于 .net 4,因为它利用了通用方差。如果您需要早期版本,则需要将 IEnumerable 替换为 List,或者为集合类型添加第二个通用参数。

class SequenceComparer<T>:IEqualityComparer<IEnumerable<T>>
{
    public bool Equals(IEnumerable<T> seq1,IEnumerable<T> seq2)
    {
        return seq1.SequenceEqual(seq2);
    }
    
    public int GetHashCode(IEnumerable<T> seq)
    {
        int hash = 1234567;
        foreach(T elem in seq)
            hash = unchecked(hash * 37 + elem.GetHashCode());
        return hash;
    }
}

void Main()
{
    var hashSet = new HashSet<List<int>>(new SequenceComparer<int>());

    List<int> test=new int[]{1,3,2}.ToList();
    test.Sort();
    hashSet.Add(test);

    List<int> test2=new int[]{3,2,1}.ToList();
    test2.Sort();       
    hashSet.Contains(test2).Dump();
}

【讨论】:

  • @downvoter 你能解释一下这个解决方案的问题是什么,所以我可以修复/改进它吗?
  • (不是我的反对票)非常接近,但在添加之前或在 Equals 中都缺少排序
  • 所以我一开始就说明他还是需要手动排序的。
  • @CodeInChaose 我同意您概述的设计选择。对所有已经排序的列表进行排序似乎是一种浪费。这是对您的(现已更新的)Main() 的评论。 (对我来说,一开始就不值得投反对票)
  • Main 中添加.Sort() 可以更好地说明如何使用它。所以这是一个很好的建议:)
【解决方案3】:

你不只是使用数组有什么原因吗? int[] 会表现得更好。另外我假设列表包含重复项,否则您只会使用集合而没有问题。

一旦将它们添加到HashSet,它们的内容似乎不会(太多)改变。归根结底,您将不得不使用依赖于SequenceEqual 的比较器。但是您不必每次都这样做。相反,或者进行指数级的序列比较(例如——随着哈希集的增长,对每个现有成员进行SequenceEqual)——如果你预先创建了一个好的哈希码,你可能需要做很少的这样的比较。虽然生成良好哈希码的开销可能与执行SequenceEqual 大致相同,但您只需为每个列表执行一次。

因此,当您第一次对特定的List&lt;int&gt; 进行操作时,您应该根据有序的数字序列生成一个哈希并将其缓存。然后下次比较列表时,就可以使用缓存的值了。我不确定您如何使用我头顶上的比较器(可能是静态字典?)来做到这一点——但您可以实现 List 包装器来轻松做到这一点。

这是一个基本的想法。您需要小心确保它不脆弱(例如,确保在成员更改时使任何缓存的哈希码无效),但对于您使用的方式而言,这看起来不会是典型情况这个。

public class FasterComparingList<T>: IList<T>, IList, ... 
    /// whatever you need to implement
{
   // Implement your interfaces against InnerList
   // Any methods that change members of the list need to
   // set _LongHash=null to force it to be regenerated
   public List<T> InnerList { ... lazy load a List }
   public int GetHashCode()
   {
       if (_LongHash==null) {
           _LongHash=GetLongHash();
       }
       return (int)_LongHash;
   }
   private int? _LongHash=null;
   public bool Equals(FasterComparingList<T> list)
   {
       if (InnerList.Count==list.Count) {
           return true;
       }
       // you could also cache the sorted state and skip this if a list hasn't
       // changed since the last sort
       // not sure if native `List` does
       list.Sort();
       InnerList.Sort();
       return InnerList.SequenceEqual(list);
   }
   protected int GetLongHash()
   {
       return .....
       // something to create a reasonably good hash code -- which depends on the 
       // data. Adding all the numbers is probably fine, even if it fails a couple 
       // percent of the time you're still orders of magnitude ahead of sequence
       // compare each time
   } 
}

如果列表在添加后不会更改,这应该非常快。即使在列表可能经常更改的情况下,创建新哈希码的时间也可能与进行序列比较的时间差别不大(甚至更大)。

【讨论】:

  • 没有特别的理由使用 List,我不知道 int[]s 表现更好。谢谢!而且您的假设是正确的,列表确实包含重复项,这就是我不使用集合的原因。
  • 一般来说,一个更简单的结构可能会比一个更复杂的结构更快,除非你正在做一些依赖于复杂结构的某些方面的事情(例如,插入链表会快得多项目比非链表)。我冗长的回应的长短是您应该使用可以缓存哈希码的构造。由于比较列表或创建可以唯一标识一个的东西的成本很高,而且您在同一个对象上做了很多次,所以只需设置一些能够记住该唯一 ID 的东西。
【解决方案4】:

如果您不指定 IEQualityComparer,则将使用默认类型,因此我认为您需要创建自己的 IEQualityComparer 实现,并将其传递给 HashSet 的构造函数。 Here is a good example.

【讨论】:

    【解决方案5】:

    在比较列表的哈希集时,您总是有一个选择,即您可以对列表进行排序并使用逗号连接它们并比较生成的字符串,而不是比较每个元素。

    因此,在这种情况下,当您创建自定义比较器而不是迭代元素并计算自定义哈希函数时,您可以应用此逻辑。

    【讨论】:

      猜你喜欢
      • 2020-08-10
      • 2021-09-28
      • 1970-01-01
      • 2011-04-19
      • 1970-01-01
      • 1970-01-01
      • 2020-08-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多