FASTSET : A Fast Data Structure for the Representation of Sets of Integers

: We describe a simple data structure for storing subsets of { 0, . . . , N − 1 } , with N a given integer, which has optimal time performance for all the main set operations, whereas previous data structures are non-optimal for at least one such operation. We report on the comparison of a Java implementation of our structure with other structures of the standard Java Collections.


Introduction
We describe a data structure for storing and updating a set S of integers whose elements are values taken in the range E = {0, . . . , N − 1} with N a given integer. Typically, the "superset", or "universe", E corresponds to the indices of the N elements of a problem instance, and we are interested in storing, using and updating subsets of the universe.
A set of integers is a very basic mathematical object and it finds use in uncountably many applications of computer programs. Performing the basic set operations (i.e., insertion, deletion, membership test and elements enumeration) in the most effective possible way would benefit all algorithms in which sets of integers play a relevant role (just to mention one example, consider the algorithms operating on graphs, for which the universe is the set of nodes/edges of a graph and the procedure needs to store and update a subset of nodes/edges). The goal of the paper is to provide optimal set primitives such as those mentioned above.
The literature on algorithms and data structures is very rich, and various possible representations and implementations of such sets are discussed in many textbooks, among which there are some classics, such as [1][2][3]. The implementations of the sets fall mainly in two categories: (i) those for which operations such as insertion, deletion and membership test are fast but other operations, such as listing all the elements of the set, performing union and intersection are slow; (ii) those for which operations such as insertion, deletion and membership test are slow but other operations, such as listing all the elements of the set, performing union and intersection are fast. In the first category, we recall the bitmap representation of the set (which uses an array v of N booleans, where v[i] = true iff i ∈ S) and some forms of hash tables using either optimal hashing or buckets [4]. In the second category, we can represent a set by an array of size N in which only the first |S| entries are filled and contain the elements of the set, a structure called a partially filled array (PFA). Usually the PFA is unsorted (maintaining the elements sorted can speed-up operations such as the membership test, but it slows down insertion and deletion) Another option is to store the elements in a linked list, again unsorted. Finally, one can resort to some tree-like structures, such as the AVL trees, a self-balancing type of binary search trees [5]. For these structures, insertion, deletion and membership tests are reasonably fast (but not optimal), but the trade-off is that some other operations are slowed down by the overhead paid in order to maintain the structure sorted.

Main Results and Paper Organization
In this paper, we propose a new data structure, called FASTSET, that has optimal time performance for all the main set operations. In particular, operations such as insertion and deletion of an element, membership test, and access to an element via an index in {1, . . . , |S|} are all O(1). From these primitive operations, we derive more complex operations, such as listing all elements at cost O(|S|), computing the intersection of two sets (i.e., S 3 := S 1 ∩ S 2 ) at cost O(min{|S 1 |, |S 2 |}) and the union of two sets (i.e., S 3 := S 1 ∪ S 2 ) at cost O(|S 1 | + |S 2 |).
In Table 1 we report a list of operations and their cost both for the various aforementioned types of data structures for set representation and for FASTSET. As far as the memory requirement is concerned, for some of them it is O(N) (namely Bitmap, PFAs, and FASTSET), for some it is O(|S|) (namely Linked List and AVL Tree), while it is O(b + |S|) for the Bucket Hashtable with b buckets.
The remainder of the paper is organized as follows. In Section 2 we describe the implementation of the various set operations for a FASTSET. In Section 3 we give two simple examples of algorithms using set data structures. In particular, we describe two popular greedy algorithms, one for Vertex Cover and the other for Max-Cut. Section 4 is devoted to computational experiments, in which FASTSETs are compared to various set implementations from the standard library of the Java distribution [6]. We have chosen Java since it is one of the popular languages and offers state-of-the-art implementations of class data structures for all the structures we want to compare to. Anyway, we remark that the main contribution of this paper is of theoretical nature and thus the results are valid for all implementations (i.e., in whichever language) of the data structures discussed. Some conclusions are drawn in Section 6.

Implementation
The main idea in order to achieve optimal time performance is remarkably simple. Our goal is to achieve both the benefits of a PFA, in which listing all elements is optimal (i.e., it is O(|S|)) but accessing the individual elements, for removal and membership tests is slow (i.e., it is O(|S|), and of a bitmap implementation where accessing the individual elements is optimal (i.e., it has cost O(1)) but listing all elements is slow (i.e., it is O(N)). To this end, in our implementation we use the array elem[ ] as a PFA, and the array pos[ ] as a bitmap. Moreover, not only pos[ ] is a bitmap, but it provides a way to update the partially filled array elem[ ] after each deletion in time O(1) rather than O(|S|).
We will now describe how the set operations can be implemented with the complexity stated in Table 1. The implementation is quite straightforward. We will use a pseudocode, similar to C. In particular, our functions will have as parameters pointers to FASTSETs, in order to avoid passing the entire data structures. Table 1. Comparison of asymptotic worst-case time performance for the main set operations of the most used data structures. We let s = |S 1 | + |S 2 |, m = min{|S 1 |, |S 2 |} and M = max{|S 1 |, |S 2 |}. In the Bucket Hashtable row, b is the number of buckets.

Membership Insertion Deletion List All
To check for membership of an element v, we just need to look at pos [v] and see if it is non-zero, at cost O(1).

Deletion
Assume we want to delete an element v (which may or may not be in S), so that S := S − {v}. When v ∈ S, this is obtained by copying the last element of elem, let it be w, onto v (by using pos, we know where v is) and decreasing |S| (i.e., elem

Intersection
Assume A and B are sets. We want to compute their intersection and store it in C, initially empty. We go through all the elements of the smaller set, and, if they are also in the other set, we put them in C. The final cost is O(min{|S 1 |, |S 2 |}). if ( Belongs( other, GetElement( smaller, k ) ) ) Insert( C, GetElement( smaller, k ) ) }

Union
Assume A and B are sets. We want to compute their union and store it in C, initially empty. We go through all the elements of each of the two sets and put them in C. The final cost is O(|S 1 | + |S 2 |). Insert( C, GetElement( B, k )) }

Initialization
A FASTSET is initialized by specifying the range for the elements value, and then simply allocating memory for two arrays: We assume that calloc allocates a block of memory initialized to 0s, in which case there is nothing else to be done. If, on the other hand, calloc returns a block of memory not initialized to 0, we must perform a for cycle to initialize pos[ ] to 0s (note that there is no need to initialize the other entries of elem[ ], since they will be written before read).
Finally, sometimes the following operation is useful for re-initializing a FASTSET, since, for large N, it can be faster (namely O(|S|)) than creating an empty FASTSET from scratch (that is O(N) because

Example Algorithms Using Sets
A graph G = (V, E) can be represented in memory by an array of sets. Namely, for each v ∈ V, we store the set N(v) of its neighbors in V. In this section, we give two simple examples of algorithms for graph problems in which we assume the graph is represented by the array of neighbor sets. The algorithms are two greedy procedures, one for the Vertex Cover problem and the other for Max-Cut. Please note that we are not saying that these are the best possible versions for these algorithms. All we want to do is to give some specific, simple examples in which the implementation of the set data structure can affect the overall time performance of a procedure. The results will be described in detail in Section 4.

Vertex Cover
Let us consider the greedy algorithm for the Vertex Cover problem. The procedure works by repeatedly selecting the highest-degree vertex, sayv, among the vertices still in the graph. The edges incident inv are then removed from the graph, together with isolated nodes, and the process is repeated until the graph is empty. Besides the sets of neighbors for each vertex, the algorithm uses a set C to store the nodes in the cover, and L to store the vertices with degree > 0 still in the graph. The pseudocode for the greedy algorithm, with the main set operations clearly marked, is described in Algorithm 1.

Max-Cut
Let us consider a greedy local-search procedure for Max-Cut. Assume being given a starting solution, represented by a partition of the nodes into two sets, i.e., shore[0] (the left shore of the cut) and shore [1] (the right shore). The procedure checks if it is profitable to flip the color of any node (i.e., to move the node from one shore to the other). If there exists a node v for which more than half of its neighbors are on the same shore of v, it is profitable to move the node from its shore to the opposite, since the value of the cut will strictly increase. The solution is then updated and the search is repeated, until there are no nodes that can be moved with profit. The pseudocode for this greedy local-search is described in Algorithm 2.

Computational Experiments
In this section, we report on some computational experiments (performed on Intel R Core TM i3 CPU M 350 @ 2.27 GHz with 2.8 GB of RAM) in which we have compared the performance of FASTSETs to that of other set data structures included in the standard library of the Java distribution. In particular, Java provides three classes implementing sets via different data structures, namely (i) BitSet [8] implementing the Bitmap data structure; (ii) TreeSet [9] implementing a set as a self-balancing tree with logarithmic cost for all main operations; (iii) HashSet [10] implementing a set by a hash table.
We have coded the class FASTSET in Java and have run a first set of experiments in which we have performed a large number of random operations (such as insertions and removals of random elements) for various values of N. The results are listed in Table 2. From the table it appears that FASTSET and BitSet perform very similarly with respect to single-element operations, and they are both better that the other two data structures. It should be remarked that in actual implementations such as this one, Bitmaps are very effective at these type of operations, since they can exploit the speed of some low-level instructions (such as logical operators) for accessing the individual bits of a word. When we turn to listing all elements, however, FASTSETs outperform the other implementations. In particular, a GetAll after 50,000 random insertion on a FASTSET is from 10 up to 30 times faster than for the other data structures. Table 2. Time comparison (in milliseconds) for some set implementations found in the java.util package of the Java standard library vs. FASTSET. The row labels report different values of N. The GetAll column refers to 1000 GetAll operations over a set after 50000 random insertions. Each Insert column is labeled by the total number of random insertions; each Belongs column is labeled by the total number of random searches in the set produced by the previous insertions; each Delete column is labeled by the total number of random deletions from the same previous set. In a second run of experiments, we have considered the two simple combinatorial algorithms described in Section 3, which make heavy use of sets during their execution. The pseudocode listed in Algorithms 1 and 2 refers to an implementation using FASTSETs. The algorithms were translated in equivalent algorithms using BitSet, TreeSet and HashSet in place of FASTSET. The translation was made so as to be as fair and accurate as possible. This was easy for most operations, since they translate directly into equivalent operations on the other data structures. Other operations, such as using GetElem() in a loop to access all the elements of a FASTSET, were realized by calling the iterator methods that the Java classes provide, and which are optimized to make sequential access to all the elements of each structure.
For both Vertex Cover and Max-Cut the tests were run on random graphs of n vertices and an average of p( n 2 ) edges, where each edge has probability p of being in the graph. We have used n ∈ {1000, 3000, 5000} and p ∈ {0.001, 0.005, 0.01}, so that the largest graphs have 5000 nodes and about 125, 000 edges. For each pair (n, p) we have generated 5 instances and computed the algorithms average running time. The results are reported in Table 3 for Vertex Cover and Table 4 for Max-Cut. For both problems, the implementation using FASTSETs was the fastest. In particular, on the Vertex Cover problem, the use of FASTSETs yields running times between one half and one quarter with respect to the other data structures. The results for Max-Cut are even better, with the other data structures being, on average, from 3 times to 30 times slower than FASTSETs for this procedure.
situation, since most of the times the integers that we deal with in our sets are indices, identifying the elements of a size-N array representing a problem instance.

Conclusions
We have described FASTSET, a new data structure for storing sets of integers in a range. Previous implementations of set data structures were either good at "direct access" to the individual set elements or at "sequential access" to all the set elements, but not at both. Our structure has two main advantages over these implementations, namely (i) it possesses the good qualities of both a "direct access" and a "sequential" structure, and (ii) it is very easy to implement. Some computational experiments have shown that FASTSETs can be profitably added, e.g., to the library of Java implementation of set data structures, to which they compare favorably.