What a hash set is
A set is a collection that holds each value at most once. A hash set is a set built on hashing, which makes three operations fast: adding a value, checking whether a value is present and removing a value. Each takes constant time on average, no matter how many elements the set holds. That is the reason to reach for one whenever a problem asks "have I seen this before?" or "which values are unique?".
In Kotlin the hash set is HashSet, which is the Java class of the same name made available under the name HashSet in every Kotlin file. The elements can be numbers, strings or objects, as long as their type has a sensible equals and hashCode; the standard types all do.
Creating a hash set
There are three common ways. All of them give you a HashSet you can change:
val a = HashSet<String>() // empty, you name the element type
val b = hashSetOf<String>() // the same, as a helper function
val c = hashSetOf("x", "y", "x") // holds "x" and "y"
To build a set from data you already have, call toHashSet() on an array or a list. Duplicates are dropped as the elements go in:
val elements = listOf("b", "c", "c", "d")
val mySet = elements.toHashSet()
println(mySet.size) // 3
Building from a collection of n elements takes O(n) time. Kotlin also has toSet(), but its result is typed as a read-only Set: it has no add or remove, so code that tries to change it doesn't compile. Use toHashSet() when you need to change the set afterwards.
Adding, checking and removing
val mySet = hashSetOf("b", "c", "d")
println(mySet.add("x")) // true: it was new
println(mySet.add("d")) // false: it was already there
println(mySet.contains("c")) // true
println("z" in mySet) // false, same as contains
println(mySet.remove("c")) // true: it was removed
println(mySet.remove("z")) // false: nothing to remove
Three details are worth knowing:
addreturns aBoolean:truewhen the value was new,falsewhen the set already had it. Adding a duplicate is not an error and changes nothing, so code often callsaddwithout checking first.removeworks the same way: it returns whether something was removed, and removing a missing value is safe.inis the idiomatic spelling ofcontains.!inchecks for absence.
Looping over a set
A set is iterable, so a for loop with in visits each element once:
for (value in mySet) {
println(value)
}
A loop over a set takes O(n) time, and the set itself uses O(n) space. The important gotcha is the order. A hash set does not promise any order: elements may come out in a different order than you added them, and you shouldn't write code that depends on a particular one. If you need insertion order, Kotlin's linkedSetOf and mutableSetOf keep it, but those are a different kind of set.
The exercise, step by step
The lesson walks through every operation on a small list of fruit.
- Start with data that has a duplicate.
fruitsArrayis declared withvalandarrayOf, holding"apples","bananas"and"apples". - Convert it to a set.
fruitsisfruitsArray.toHashSet(). The second"apples"is dropped, leaving two elements. - Add a value.
fruits.add("oranges")grows the set, because"oranges"wasn't there. - Check membership. An
ifwithfruits.contains("apples")printsWe have appleswhen the check is true. - Remove a value.
fruits.remove("bananas")takes it out. - Loop and print. A
for (fruit in fruits)loop prints what is left, which shows that the duplicate and the removed fruit are gone.
The complete solution:
val fruitsArray = arrayOf("apples", "bananas", "apples")
val fruits = fruitsArray.toHashSet()
fruits.add("oranges")
if (fruits.contains("apples")) {
println("We have apples")
}
fruits.remove("bananas")
for (fruit in fruits) {
println(fruit)
}
It prints We have apples, followed by apples and oranges on separate lines. The order of those last two lines is up to the hash set.
Common mistakes
- Expecting a fixed order. Don't compare the printed output of a hash set to a fixed list unless you sort it first.
- Using
toSet()and then callingadd. The read-onlySettype has no such method, so the compiler rejects the call. UsetoHashSet(). - Forgetting the element type on an empty set.
hashSetOf()with no arguments can't infer the type, so writehashSetOf<String>()or declare the variable's type. - Treating
valas "frozen".val fruitsonly stops you from pointing the variable at a different set. You can still add to and remove from the set it refers to. - Putting mutable objects in a set. If an element's contents change after it is added, its hash changes and the set can no longer find it.
Key takeaways
- A hash set stores unique values and gives average constant-time
add,containsandremove. - Create one with
HashSet<T>(),hashSetOfortoHashSet(). addandremovereturn aBooleanand are safe to call with duplicates or missing values.- Iteration is
O(n)and the order is unspecified. - The set uses
O(n)space.