Project Nayuki


Binary array set

Introduction

The binary array set[0] is a very space-efficient data structure that allows adding elements and testing membership reasonably quickly. It basically works as a collection of sorted arrays with power-of-2 sizes.

Adding one element to a BAS takes amortized \(Θ(1)\) time (worst-case \(Θ(n)\)), searching/testing for an element takes worst-case \(Θ((\log n)^2)\) time, and removing an element is not supported at all (although the entire set can be cleared easily). Despite the lack of support for deletion, the data structure is still useful in applications that only add and test but don’t delete – for example, breadth-first search maintains an ever-growing set of visited nodes that shouldn’t be revisited. To compare time complexities with a popular alternative, a balanced binary search tree takes worst-case \(Θ(\log n)\) time alike for adding, testing, or removing one element.

The binary array set data structure is succinct because it only uses \(Θ(\log n)\) extra space for overhead (i.e. the space not used for storing the element values themselves). For modest sizes of \(n\) (e.g. over 1000), the amount of overhead space used by a BAS is practically negligible. The data structure that best minimizes overhead space would be the single flat array (which uses only \(Θ(1)\) extra space), but it doesn’t support both fast insertion and testing simultaneously. Note that despite the word “array” in the data structure’s name, the set is in fact orderless and can present elements in any order.

By comparison, a balanced BST uses \(Θ(n)\) extra space for pointers to all the tree nodes. (Even B-trees use \(Θ(n)\) extra space, but the scaling constant approaches zero as the degree increases.) This extra space usage for node pointers would be problematic in the situation where the array of raw data values (without pointers and other overheads) barely fit in system memory. This disadvantage of node-based data structures is especially painful in languages like Java, where each object (i.e. the tree nodes) inherently has tens of bytes of overhead.

Internal details

How this data structure works is that it has a sequence of \(\lfloor \log_2 n \rfloor + 1\) slots, where the slot at index \(k\) (0-based counting) is either null or an ascending-sorted array of length \(2^k\). For example, the abstract conceptual set {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} could be represented as a binary array set in one way like this:

Number of slots: \(\lfloor \log_2 13 \rfloor + 1 = 4\).
Slot 0: [5]. (length 1)
Slot 1: Null.
Slot 2: [2, 3, 9, 13]. (length 4)
Slot 3: [1, 4, 6, 7, 8, 10, 11, 12]. (length 8)

Note that the size of the set is 13, whose binary representation is 11012. The binary representation corresponds with which slots are filled. Also note that the union of all the arrays in the binary array set is exactly equal to the set of elements being represented, no more, no less, without duplicates.

To test if a given value is an member of the set, we simply perform standard binary search in each array (in the non-null slots) and return whether a result was found.

To add a new element, we wrap the element in a new array of length 1. If slot 0 is empty, we simply store the new array there. Otherwise we merge the new array with the array already at slot 0, which forms a new sorted array of length 2. Next we check if slot 1 is empty; we repeat this merge/check process on increasing slot indexes until we find an empty slot.

Source code

Java (SE 7+)

The class implements the java.util.Set interface, so it can be used as a drop-in replacement of HashSet or TreeSet as long as remove() is not called. The iterator is not fail-fast on concurrent modifications. The maximum set size is Integer.MAX_VALUE (i.e. \(2^{31} - 1\)).

Python (compatible with 2 and 3)

This implementation only supports the methods {add(), clear()}, built-in functions {len(), iter()}, and operator in – so it only supports a fraction of the methods that the built-in set type has.

C++ (C++11 and above)

The class is modeled after the Java version, supporting the methods size(), add(), contains(), empty(), and clear(). It has far fewer features than std::set.

Rust

This struct and its methods are modeled after std::collections::BTreeSet, but has fewer features.

License: MIT (open source)

Notes