【问题标题】:Why does this HashSet look sorted when printed?为什么这个 HashSet 在打印时看起来是排序的?
【发布时间】:2021-01-26 03:41:24
【问题描述】:
Set<Integer> s = new HashSet<Integer>();
s.add(77);
s.add(0);
s.add(1);
 
System.out.println(s);
 
TreeSet<Integer> s1 = new TreeSet<Integer>();
s1.add(77);
s1.add(0);
s1.add(1);
 
System.out.println(s1);

输出:

s = [0, 1, 77]
s1= [0, 1, 77]

根据教程点页面上的定义

Set 是一组没有重复元素的通用值。 TreeSet 是对元素进行排序的集合。

为什么ss1 的输出都进行了排序?我只希望对 s1 的输出进行排序。

【问题讨论】:

  • 只是机会。添加更多数字,HashSet 将无序。
  • s1 已排序,因为它是一个 TreeSet,s 看起来 像已排序,但再添加一些元素,您会发现它因取模而变得奇怪. How do HashSets in Java work?

标签: java collections hashset


【解决方案1】:

你说得对,s1 已排序,因为它是 TreeSet,但 s 并没有真正排序。如果您向s 添加更多元素,您会看到一些奇怪的事情发生。

After adding 32, 12, 13, and 14
[0, 32, 1, 12, 77, 13, 14]

所以现在你看到它有点有序,但不是真的。原因是HashSet默认容量为16,以后有需要还会增加。因此,当您向其中添加一个元素时,想象它采用该元素的哈希码,模 16,以便它适合 16 个存储桶的内部“列表”。由于Integer 的哈希码是它所代表的int,我们可以假设它所做的只是(the element to be added) % 16

所以当您添加 0、1 和 77 时,它可能在内部看起来像这样:

Bucket   Elements
0        0
1        1
2
3
4
5
...
13       77 (77 % 16 is 13, so it's placed here)
14
15
16

然后我们添加 32。32 % 16 为 0,但我们在第一个桶中已经有了 0。幸运的是,为了防止这样的冲突,HashSets 使用链表而不是单个元素,因此我们只需将 32 附加到包含 0 的链表。

Bucket   Elements
0        0 -> 32
1        1  
2
3
4
5
...
13       77
14
15
16

对于 12、13 和 14 也一样:

Bucket   Elements
0        0 -> 32
1        1
2
3
4
5
...
12       12
13       77 -> 13
14       14
15
16

如果您按顺序遍历存储桶,打印该存储桶中的每个链表,您会得到[0, 32, 1, 12, 77, 13, 14]

run

【讨论】:

    【解决方案2】:

    为什么Set接口返回的是有序列表?

    Set Interface 在Java 中,或者Set 概念,一般来说,可以被认为是抽象数据类型,作为行为规范的契约,它声明了一个不包含重复元素的集合

    所以,Set&lt;E&gt; 接口本身返回任何东西;但是,它可以实现in different ways

    TreeSet&lt;E&gt;定义says, that

    元素使用它们的natural ordering 排序,或者通过在集合创建时提供的Comparator 排序,具体取决于使用的构造函数。

    HashSet&lt;E&gt; 定义,另一方面,says, that

    它不保证订单会随着时间的推移保持不变。

    【讨论】:

      【解决方案3】:

      这只是偶然发生的。

      HashSetsHashMap 的特殊实现,但它们仍然使用 hashCode 将对象放入桶中。

      Integer 的标准 hashCodeint 值本身。

      指定如此低的值以及负载因子和存储桶算法会导致它们根据该代码放置在不同的存储桶中,但存储桶恰好是连续的。如果您将值更改为更大的值,它们将不会被排序,因为该算法仅使用部分 hashCode 来选择存储桶,因此降低了它们连续的机会。对于更大的随机分布数字集也是如此。

      Set<Integer> s = new HashSet<Integer>();
      
      s.add(57999999);
      s.add(67999999);
      s.add(77999999);
      
      System.out.println(s);
      
      TreeSet<Integer> s1 = new TreeSet<Integer>();
      
      s1.add(57999999);
      s1.add(67999999);
      s1.add(77999999);
      
      
      System.out.println(s1);
      

      在我运行 Windows 和 Java 14 的机器上,它们打印如下:

      [67999999, 77999999, 57999999]
      [57999999, 67999999, 77999999]
      

      【讨论】:

      • 或者:Set&lt;Integer&gt; s = new HashSet&lt;&gt;(4); s.add(77); s.add(0); s.add(1); System.out.println(s); 打印 [0, 77, 1]
      • 我认为我可以强制执行类似的行为来设置负载因子。我没有考虑容量。因此,当我这样做时,我将容量任意设置为 16。
      • 关键点是最低三位,0 → 0001 → 00177 → 101。这意味着如果容量为 8 或更高,则不会发生冲突,并且这三个特定值将碰巧以它们的自然顺序出现。负载因子唯一能做的就是影响插入是否会使容量更大。如果容量为两个或四个,您将在177(最低位01)上发生冲突,这两个项目将按其插入顺序出现。使用new HashSet&lt;&gt;(1, 3f),强制一个容量,三个元素都会按插入顺序出现……
      【解决方案4】:

      TreeSet 实现了SortedSet,这就是为什么您添加到其中的所有元素都会在实际添加之前进行排序。

      在您的情况下,您正在使用Integer。它实现了Comparable,它具有默认的比较机制。你 TreeSet 使用这个 Integer 的 Comparable 实现在将它们添加到集合之前比较你的整数。

      因此,通常在使用 TreeSet 时,您有两种可能的解决方案:

      1. 您的TreeSet 应该只适用于实现Comparable 接口的对象,因为它们需要有compareTo 实现。 TreeSet 将使用此实现来执行排序。
      2. 如果您的类没有实现Comparable,您可以在创建TreeSet 的实例时将您的Comparator 实现传递给构造函数。

      HashSet 里面不做任何排序,这里只是随机的。元素根据它们的哈希码进入不同的存储桶,在你的情况下它们是匹配的。

      UPD:HashSet 这不是随机的,我的回答可能会令人困惑,有兴趣的可以看看this explanation of some HashSet principles

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-08-24
        • 1970-01-01
        • 2012-04-07
        • 2017-06-17
        • 1970-01-01
        • 2020-09-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多