So far, we have learned about sequential data structures such as arrays (lists), stacks, queues, and linked lists (and their variants). In this chapter, we will cover the data structure called a set.
A set is a collection of items that are unordered and consists of unique elements (meaning they cannot be repeated). This data structure uses the same math concept as of finite sets, but applied to a Computer Science data structure.
Let's take a look at the math concept of sets before we dive into the Computer Science implementation of it. In Mathematics, a set is a collection of distinct objects.
For example, we have a set of natural numbers, which consists of integer numbers greater than or equal to 0: N = {0, 1, 2, 3, 4, 5, 6, …}. The list of the objects within the set is surrounded by {}
(curly braces).
There is also the null set concept. A set with no element is called a null set or empty set. For example, a set of prime numbers between 24 and 29. As there is no prime number (a natural number greater than 1 that has no positive divisors other than 1 and itself) between 24 and 29, the set will be empty. We represent an empty set as { }.
You can also imagine a set as an array with no repeated elements and with no concept or order.
In Mathematics, a set also has some basic operations such as union, intersection, and difference. We will also cover these operations in this chapter.
The current implementation of JavaScript is based on ECMAScript 5.1 (supported by modern browsers) published on June 2011. It contains the Array
class implementation that we covered in earlier chapters. ECMAScript 6 (a work in progress, expected to be released in March 2015 at the time of writing this book) contains an implementation of the Set
class.
You can see the details of the ECMAScript 6 Set
class implementation at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set (or http://goo.gl/2li2a5).
The class we are going to implement in this chapter is based on the Set
implementation of ECMAScript 6.
This is the skeleton of our Set
class:
function Set() {
var items = {};
}
A very important detail is that we are using an object to represent our set (
items
) instead of an array. But we could also use an array to do this implementation. Let's use an object to implement things a little bit differently and learn new ways of implementing data structures that are similar. And also, objects in JavaScript do not allow you to have two different properties on the same key, which guarantees unique elements in our set.
Next, we need to declare the methods available for a set (we will try to simulate the same Set
class implemented in ECMAScript 6):
add(value)
: This adds a new item to the set.remove(value)
: This removes the value from the set.has(value)
: This returns true
if the value exists in the set and false
otherwise.clear()
: This removes all the items from the set.size()
: This returns how many elements the set contains. It is similar to the length
property of the array.values()
: This returns an array of all the values of the set.The first method we will implement is the has(value)
method. We will implement this method first because it will be used in other methods such as add
and remove
. We can see its implementation here:
this.has = function(value){ return value in items; };
As we are using an object to store all the values of the set, we can use JavaScript's in
operator to verify that the given value is a property of the items
object.
But there is a better way of implementing this method, which is as follows:
this.has = function(value){ return items.hasOwnProperty(value); };
All JavaScript objects have the hasOwnProperty
method. This method returns a Boolean indicating whether the object has the specified property or not.
The next method we will implement is the add
method:
this.add = function(value){ if (!this.has(value)){ items[value] = value; //{1} return true; } return false; };
Given a value
, we can check whether the value
already exists in the set. If not, we add the value
to the set (line {1}
) and return true
to indicate that the value was added. If the value already exists in the set, we simply return false
to indicate that the value was not added.
Next, we will implement the remove
method:
this.remove = function(value){ if (this.has(value)){ delete items[value]; //{2} return true; } return false; };
In the remove
method, we will verify that the given value
exists in the set. If this is positive, we remove the value
from the set (line {2}
) and return true
to indicate the value was removed; otherwise, we return false
.
As we are using an object to store the items
object of the set, we can simply use the delete
operator to remove the property from the items
object (line {2}
).
To use the Set
class, we can use the following code as an example:
var set = new Set(); set.add(1); set.add(2);
Just out of curiosity, if we output the items
variable on the console (console.log
) after executing the previous code, this will be the output in Google Chrome:
Object {1: 1, 2: 2}
As we can see, it is an object with two properties. The property name is the value we added to the set and its value as well.
If we want to remove all the values from the set, we can use the clear
method:
this.clear = function(){ items = {}; // {3} };
All we need to do to reset the items
object is assign it to an empty object again (line {3}
). We could also iterate the set and remove all the values one by one using the remove
method, but that is too much work as we have an easier way of doing it.
The next method we will implement is the size
method (which returns how many items are in the set). There are three ways of implementing this method.
The first method is to use a length
variable and control it whenever we use the add
or remove
method, as we used in the LinkedList
class in the previous chapter.
In the second method, we use a built-in function from the built-in Object
class in JavaScript (ECMAScript 5+):
this.size = function(){ return Object.keys(items).length; //{4} };
The Object
class in JavaScript contains a method called keys
that returns an array of all properties of a given object. In this case, we can use the length
property of this array (line {4}
) to return how many properties we have in the items
object. This code will work only in modern browsers (such as IE9+, FF4+, Chrome5+, Opera12+, Safari5+, and so on).
The third method is to extract each property of the items
object manually and count how many properties there are and return this number. This method will work in any browser and is the equivalent of the previous code:
this.sizeLegacy = function(){ var count = 0; for(var prop in items) { //{5} if(items.hasOwnProperty(prop)) //{6} ++count; //{7} } return count; };
So, first we iterate through all the properties of the items
object (line {5}
) and check whether that property is really a property (so we do not count it more than once—line {6}
). If positive, we increment the count
variable (line {7}
) and at the end of the method we return this number.
We cannot simply use the for-in
statement and iterate through the properties of the items
object and increment the count
variable value. We also need to use the has
method (to verify that the items
object has that property) because the object's prototype contains additional properties for the object as well (properties are inherited from the base JavaScript Object
class, but it still has properties of the object, which are not used in this data structure).
The same logic applies to the values
method, using which we want to extract all the properties of the items
object and return it as an array:
this.values = function(){ return Object.keys(items); };
This code will only work in modern browsers. As we are using Google Chrome and Firefox as testing browsers in this book, the code will work.
If we want code that can be executed in any browser, we can use the following code, which is equivalent to the previous code:
this.valuesLegacy = function(){ var keys = []; for(var key in items){ //{7} keys.push(key); //{8} } return keys; };
So, first we iterate through all the properties of the items
object (line {7}
), add them to an array (line {8}
), and return this array.
Now that we have finished implementing our data structure, let's see how we can use it. Let's give it a try and execute some commands to test our Set
class:
var set = new Set(); set.add(1); console.log(set.values()); //outputs ["1"] console.log(set.has(1)); //outputs true console.log(set.size()); //outputs 1 set.add(2); console.log(set.values()); //outputs ["1", "2"] console.log(set.has(2)); //true console.log(set.size()); //2 set.remove(1); console.log(set.values()); //outputs ["2"] set.remove(2); console.log(set.values()); //outputs []
So, now we have a very similar implementation of the Set
class as in ECMAScript 6. As mentioned before, we could also have used an array instead of an object to store the elements. As we used arrays in Chapter 2, Arrays, Chapter 3, Stacks, and Chapter 4, Queues, it is nice to know there are different ways of implementing the same thing.