HashSet在内部执行排序作业吗



我的Set有时排序,有时不排序。

以下是示例:

public class SetOfInteger {
    public static void main(String[] args) {
        Random rand = new Random(47);
        Set<Integer> intset = new HashSet<>();
        for (int i = 0; i < 10; i++) {
            int j = rand.nextInt(30);
            System.out.print(j + " ");
            intset.add(j);
        }
        System.out.println();
        System.out.println(intset);
    }
}

结果表明,set没有被排序。

8 5 13 11 1 29 28 20 12 7 
[1, 20, 5, 7, 8, 11, 12, 29, 28, 13]

当我在for语句中将终止表达式更改为i < 20时,结果显示set已排序。

8 5 13 11 1 29 28 20 12 7 18 18 21 19 29 28 28 1 20 28 
[1, 5, 7, 8, 11, 12, 13, 19, 18, 21, 20, 29, 28]

这太奇怪了,是吗?我只是不知道该怎么解释,我需要一些帮助,非常感谢。

哈希集不能保证排序迭代,但在非常特殊的情况下,其内部数据结构可能像桶排序。

具体来说,对于范围为[065535]且表大小大于最大键的整数键,存储键的存储桶的索引等于键本身,并且由于迭代器按存储桶顺序迭代,因此它按排序顺序发送元素。

有一些很好的答案,但没有人试图解释在这种特殊情况下到底发生了什么,所以我将把我的答案限制在这一点上,而不是添加另一个关于HashSet如何工作的解释。我认为这种理解是理所当然的。

HashSet的默认构造函数创建一个容量为16、负载因子为0.75的集合。这意味着有16个存储箱,当您插入16*0.75=12个唯一元素时,该容量会增加。

这就是为什么在第一种情况下,数字除以16后按余数排序:该集合以16的表大小开始,通过取x % 16将每个元素"哈希"到一个bin。然后,当有12个元素时,它扩大了表格并进行了重新填充(如果不清楚,请参阅哈维尔·马丁的回答),可能会将表格扩大到32个。(我只能在java 6文档中找到关于它如何增长的信息,该文档指出桶的数量"大约"翻了一番,不管这意味着什么。)这给了30以下的每个整数自己的bin,所以当集合按顺序在每个bin上迭代时,它按顺序在数字上迭代。如果您插入64以下的数字,您可能会发现在迭代排序之前,您需要插入32*0.75=24个元素。

还要注意的是,这种分配bin的方式不能保证行为。其他Java版本/实现中的HashSet可能会对对象的hashCode()值执行比简单地取余数更复杂的操作。(正如ruakh在评论中指出的,谢谢!)

您的问题指出,随着集合的增加,物品顺序会发生变化。然而,你不能指望订单会被保留下来。Set有一个保证:每种元素中只有一个。还有其他Set对象提供了进一步的保证,但简单的HashSet不提供顺序保证。

由于HashSet的内部存储方式,您看到的重新排序只是一次内部重组。用一种非常简化的思维方式,HashSet有一定数量的"槽"来存储值,如果不是素数,这些值通常是奇数。getHashCode()中的散列码用于将对象分配给插槽。当发生哈希代码冲突时,HashSet会使用相等运算符equals()来确定对象是否确实是唯一的。

当您向HashSet添加项目时,会发生以下几件事:

  • 对象被指定到其内部插槽
    • 然后对散列码进行进一步散列,以找到它属于哪个插槽
    • 如果有插槽冲突,那么我们测试是否相等。如果它是同一个对象,我们会丢弃它,如果不是,我们会将它添加到该插槽中的列表中
  • 当对象数量超过插槽数量时,HashSet需要调整自身大小
    • 它创建了一组更大的槽,通常仍然是奇数或素数
    • 现有项目被重新映射到新的插槽集合中——这是可以更改顺序的地方

底线是,如果对象神奇地对自己进行了排序,那就不是一个可以指望的实现,除非您使用的是TreeSet,它对集合项强制执行排序顺序。

HashSet的迭代顺序没有定义,唯一的保证是它是一致的:对未修改的HashSet进行迭代将产生相同的序列。

正如一位评论者所说,在内部,该类使用每个元素的hashCode方法将它们存储在一定数量的bins中。因此,例如,如果它使用20个bin,那么它可以将o.hashCode() % 20作为bin索引。每个bin在一个列表中可以有几个项目,然后通过equals方法来区分这些项目。因此,即使Integer的散列是其int值,顺序也不必是自然整数排序。

此外,在插入和移除元素时,该集会监控其负载系数;考虑空闲垃圾箱的比例、最大垃圾箱列表大小、每个垃圾箱的平均项目数,等等。当它认为合适时,它会执行rehash,这意味着更改用于存储元素的bin的数量,因此它们的bin索引会更改,因为o.hashCode() % n中的n会更改每个元素都被"重新排列"到新的位置(这是一项成本高昂的操作),从而解释了添加更多元素后所看到的不同顺序。

有趣的问题。集合使用array of linked list来存储其元素。CCD_ 18用于(间接地)找到要存储在CCD_ 19中的对象的位置。

如果有两个对象需要存储在同一位置,则该对象将存储在该位置的链表的下一个槽中。

数组的大小是动态的,并且是根据其中对象的数量计算运行时的。这不确定,但我认为你认为你的数字是排序的,因为Set可能增加了大小。CCD_ 20取决于数值,因此将按顺序计算。因为底层数组的大小会随着循环大小的增加而增加。不会发生冲突,并且对输出进行排序。

但我还是想强调一下,这样我的回答就不会引起任何误解HashSet不保证元素的任何排序

您必须手动排序,因为不能保证哈希集会被排序。如果你想,你也可以使用TreeSet,它将提供你想要的功能,但如果你无论如何都想使用HashSet,请尝试这个:

Set intset = new HashSet();
List sortedIntList = new ArrayList(intset);
Collections.sort(sortedIntList);

最新更新