HashSet是平时常用到的数据结构之一,其保证元素是不重复的。
本文将用一个简单的例子来解释下scala语言中HashSet内部的工作原理,看下add和remove到底是怎样工作的。
用法示例
val s = mutable.HashSet[String]()
s.add("a")
s.add("a")
s.add("b")
println(s)
HashSet实现原理
1、add方法
我们看一下add的代码实现:
protected def addElem(elem: A) : Boolean = {
addEntry(elemToEntry(elem))
}
/**
* Elems have type A, but we store AnyRef in the table. Plus we need to deal with
* null elems, which need to be stored as NullSentinel
*/
protected final def elemToEntry(elem : A) : AnyRef =
if (null == elem) NullSentinel else elem.asInstanceOf[AnyRef]
protected def addEntry(newEntry : AnyRef) : Boolean = {
var h = index(newEntry.hashCode)
var curEntry = table(h)
while (null != curEntry) {
if (curEntry == newEntry) return false
h = (h + 1) % table.length
curEntry = table(h)
//Statistics.collisions += 1
}
table(h) = newEntry
tableSize = tableSize + 1
nnSizeMapAdd(h)
if (tableSize >= threshold) growTable()
true
}
首先需要根据元素NewEntry计算出table数组的下标h,
然后找到table中的第h元素e,
当e为空时直接添加元素到table[h];
当e不为空并且e和要添加的元素相同时, 说明已存在,返回false;
当e不为空且和添加的元素不同时,每次使h加一(达到上限则从0开始)直到table[h]为空,将NewEntry添加至此,
扩容的方法和HashMap基本相同,
当HashSet中的元素个数超过数组大小threshold时,
就会进行数组扩容,threshold的默认值为table大小的0.75,这是一个折中的取值。
也就是说,默认情况下,数组大小为16,那么当HashSet中元素个数超过16*0.75=12的时候,
就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,
private def growTable() {
val oldtable = table
table = new Array[AnyRef](table.length * 2)
tableSize = 0
nnSizeMapReset(table.length)
seedvalue = tableSizeSeed
threshold = newThreshold(_loadFactor, table.length)
var i = 0
while (i < oldtable.length) {
val entry = oldtable(i)
if (null != entry) addEntry(entry)
i += 1
}
if (tableDebug) checkConsistent()
}
2、remove方法
理解了add操作则remove方法就会简单得多。
/**
* Removes an elem from the hash table returning true if the element was found (and thus removed)
* or false if it didn't exist.
*/
protected def removeElem(elem: A) : Boolean = {
if (tableDebug) checkConsistent()
def precedes(i: Int, j: Int) = {
val d = table.length >> 1
if (i <= j) j - i < d
else i - j > d
}
val removalEntry = elemToEntry(elem)
var h = index(removalEntry.hashCode)
var curEntry = table(h)
while (null != curEntry) {
if (curEntry == removalEntry) {
var h0 = h
var h1 = (h0 + 1) % table.length
while (null != table(h1)) {
val h2 = index(table(h1).hashCode)
//Console.println("shift at "+h1+":"+table(h1)+" with h2 = "+h2+"? "+(h2 != h1)+precedes(h2, h0)+table.length)
if (h2 != h1 && precedes(h2, h0)) {
//Console.println("shift "+h1+" to "+h0+"!")
table(h0) = table(h1)
h0 = h1
}
h1 = (h1 + 1) % table.length
}
table(h0) = null
tableSize -= 1
nnSizeMapRemove(h0)
if (tableDebug) checkConsistent()
return true
}
h = (h + 1) % table.length
curEntry = table(h)
}
false
}
对于要删除的元素removalEntry,首先计算其哈希值得到table的下标h,
然后如果table[h]为空,返回false,否则
比较table[h]和removalEntry是否相同,相同则删除,不相同则h逐一递增,直到table[h]为空返回false。
总结
在java中HashSet与TreeSet都是基于Set接口的实现类。
其中TreeSet是Set的子接口SortedSet的实现类。Set接口及其子接口、实现类的结构如下所示:
|——SortedSet接口——TreeSet实现类
Set接口——|——HashSet实现类
|——LinkedHashSet实现类
HashSet有以下特点
不能保证元素的排列顺序,顺序有可能发生变化
不是同步的
集合元素可以是null,但只能放入一个null
TreeSet类型是J2SE中唯一可实现自动排序的类型
TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。
TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。
向 TreeSet中加入的应该是同一个类的对象。
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。
这样使得元素看起 来像是以插入顺 序保存的,也就是说,当遍历该集合时候,
LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。