Problem

Let d∈N+d \in \mathbb{N}^+ such that log⁡log⁡d∈N+\log \log d \in \mathbb{N}^+. We want to design a data structure VV, that maintains nn elements from the finite domain Zd≐{0,…,d−1}\mathbb{Z}_d \doteq \{0, \ldots, d - 1 \}, with the following properties:

It supports insert, successor operations in O(log⁡log⁡d)O(\log \log d) time.

Base Case

If d=2d = 2, then the tree consists of a bit array VV of size two. Insertion and deletion involves setting the corresponding bit to 11 and 00, respectively. Finding successor of an element involving scanning the array. All operations has time complexity O(1)O(1).

General Cases

Naive Array

We could maintain a bit array VV of size dd. Insertion and deletion can still be performed in O(1)O(1) time. However, successor has time complexity O(d)O(d).

Naive Recursion

We can partition the bit array VV into d\sqrt{d} clusters, denoted VV.cluster[0][0], …\ldots, VV.cluster[d−1][\sqrt{d} - 1].
Each cluster consists of d\sqrt{d} consecutive elements, and maintains a summary bit for deciding whether it is empty. The summary bits are grouped into an array VV.summary.

Now insertion time is still O(1)O(1). Deletion and successor can be performed in O(d)O(\sqrt{d}) time.

van Emde Boas Tree

van Emde Boas Tree (vEB Tree) is an advanced data structure based on the divide-and-conquer strategy.

Definition

Denote the structure over domain dd as vEB(d)\text{vEB}(d). It is defined recursively as follows:

Definition vEB(d)\text{vEB}(d):

  1. If d=2d = 2, then vEB(d)\text{vEB}(d) is just a simple bit array of size 22.
  2. Otherwise, vEB(d)\text{vEB}(d) has the following components:
    • Two elements, max and min, each of size log⁡d\log d bits.
    • Children trees, c0,…,cd−1c_0, \ldots, c_{\sqrt{d} - 1}, each of type vEB(d)\text{vEB} \left(\sqrt{d}\right).
    • Summary tree, ss, of type vEB(d)\text{vEB} \left(\sqrt{d}\right).

Hence, if d>2d > 2, then vEB(d)\text{vEB}(d) contains (1+d) \big(1 + \sqrt d\big)\, vEB(d)\text{vEB} \left(\sqrt{d}\right) subtrees.

Let VV be an instance of vEB(d)\text{vEB}(d). If an element x∈Zdx \in \mathbb{Z}_{d} is stored in VV, we write x∈Vx \in V. The components of VV have the following functionalities.

  • VV.max and VV.min maintain the maximum and minimum elements in the tree. These two elements will not be stored in the children trees or the summary tree.
  • For each i∈Zdi \in \mathbb{Z}_{\sqrt{d}}, the child tree V.ciV.c_i stores elements x∈Vx \in V for which 1) x∉{V.max⁡,V.min⁡}x \notin \{ V.\max, V.\min \}; 2) ⌊x/d⌋=i\lfloor x / \sqrt{d} \rfloor = i.
  • The summary tree V.sV.s stores the i∈Zdi \in \mathbb{Z}_{\sqrt d} for which V.ciV.c_i is not empty.

Operations

We need the following subroutine.

Procedure exchange(V,x)(V, x):
If x<V.min⁡x < V.\min then
\qquad swap(V.min⁡,x)(V.\min , x);
Elif V.max⁡<xV.\max < x then
\qquad swap(V.max⁡,x)(V.\max , x);

Next we consider inserting an element x∈Zdx \in \mathbb{Z}_d into and vEB(d)\text{vEB}(d) structure VV.

If VV is empty, then the algorithm sets V.max⁡V.\max and V.min⁡V.\min to xx.

If VV consists of one element (in this case, V.max⁡=V.min⁡V.\max = V.\min), then xx is stored in either V.max⁡V.\max or V.min⁡V.\min.

Otherwise, xx is swapped with V.max⁡V.\max, if x>V.max⁡x > V.\max, or xx is swapped with V.min⁡V.\min, if x<V.min⁡x < V.\min. Then the algorithm extracts the first and the last (1/2)⋅log⁡d(1 / 2) \cdot \log d bits, denoted as xhx_h and xℓx_\ell respectively, from the binary representation of xx. The value of xℓx_\ell will be inserted into the child tree V.cxhV.c_{x_h}. If V.cxhV.c_{x_h} is empty, the algorithm needs to insert xhx_h into the summary tree V.sV.s, indicating that V.cxhV.c_{x_h} is not empty after inserting xℓx_\ell into it.

Procedure insert(V,x)(V, x):
If VV is empty then
\qquad V.min⁡←xV.\min \leftarrow x;  V.max⁡←x\,V.\max \leftarrow x;
If VV contains only one element then
\qquad exchange(V,x)(V, x);
else
\qquad exchange(V,x)(V, x);
\qquad <xh,xℓ>←(⌊x/d⌋,xmod  d)\big< x_h, x_\ell \big> \leftarrow \left( \lfloor x / \sqrt{d} \rfloor, x \mod{\sqrt{d}} \right)
\qquad If V.cxhV.c_{x_h} is empty then
\qquad \qquad insert(V.s,xh)(V.s, x_h);
\qquad insert(V.cxh,x)(V.c_{x_h}, x);

To find the successor of xx from VV, the algorithm first checks whether x≥V.max⁡x \ge V.\max. If so, it returns null.

Otherwise, it inspects the child tree V.cxhV.c_{x_h}. If it is non-empty, and xx is smaller than the largest element in V.cxhV.c_{x_h}, the algorithm returns the successor of xℓx_\ell in V.cxhV.c_{x_h}.

If not, the algorithm try to find the smallest xh′>xhx_h' > x_h such that the child tree V.cxh′V.c_{x_h'} is non-empty subtree. If such xh′x_h' exists, the algorithm returns the smallest element from V.cxh′V.c_{x_h'}. Otherwise, V.max⁡V.\max is returned.

Procedure successor(V,x)(V, x):
If x≥V.max⁡x \ge V.\max then
\qquad return null;
<xh,xℓ>←(⌊x/d⌋,xmod  d)\big< x_h, x_\ell \big> \leftarrow \left( \lfloor x / \sqrt{d} \rfloor, x \mod{\sqrt{d}} \right);
If V.cxhV.c_{x_h} is empty or V.cxh.max⁡≤xV.c_{x_h}.\max \le x then
\qquad xh′←x_h' \leftarrow successor(V.s,x)(V.s, x);
\qquad If xh′≠x_h' \neq null then
\qquad \qquad return V.cxh′.min⁡V.c_{x_h'}.\min;
\qquad Else
\qquad \qquad return V.max⁡V.\max
else
\qquad return successor(V.cxh,xℓ)(V.c_{x_h}, x_\ell).

Time Analysis

Theorem. Inserting an element xx into an vEB(d)\text{vEB}(d) instance VV has time complexity O(log⁡log⁡d)O(\log \log d).

Proof. The claim holds trivially if VV contains zero or one element. Otherwise, let T(d)T(d) be the time complexity for inserting xx into VV. We show that

T(d)≤T(d)+O(1). T(d) \le T(\sqrt{d}) + O(1).

Solving the recursion gives T(d)∈O(log⁡log⁡d)T(d) \in O(\log \log d).

If V.chV.c_h is non-empty, the only one recursion, i.e., insert(V.cxh,x)(V.c_{x_h}, x), is executed. Therefore, T(d)≤T(d)+O(1)T(d) \le T(\sqrt{d}) + O(1) holds.

If V.chV.c_h is empty, two recursions, i.e., insert(V.s,x)(V.s, x), insert(V.cxh,x)(V.c_{x_h}, x), are executed. However, in this case, the second recursion has time complexity O(1)O(1). Hence, T(d)≤T(d)+O(1)T(d) \le T(\sqrt{d}) + O(1) still holds.

□\square

Theorem. Finding the successor of an element xx from an vEB(d)\text{vEB}(d) instance VV has time complexity O(log⁡log⁡d)O(\log \log d).

Proof. The successor operation of VV invokes at most one recursive call, therefore its running time T(d)T(d) satisfies T(d)≤T(d)+O(1)T(d) \le T(\sqrt{d}) + O(1), which solves to T(d)∈O(log⁡log⁡d)T(d) \in O(\log \log d).

□\square

Space Analysis

Theorem. vEB(d)\text{vEB}(d) can be implemented with O(d)O(d) space.

Proof. Denote the space usage of vEB(d)\text{vEB}(d) as S(d)S(d). Then we have S(2)=O(1)S(2) = O(1). For d>2d > 2, a vEB(d)\text{vEB}(d) structure stores

  1. Elements min⁡\min and max⁡\max, which takes Θ(1)\Theta(1) words.
  2. Pointers to 1+d1 + \sqrt{d} subtrees, which takes Θ(d)\Theta(\sqrt{d}) words.
  3. 1+d1 + \sqrt{d} subtrees, each taking space S(d)S(\sqrt{d}).

Therefore, the total space usage is given by

S(d)=(1+d)⋅S(d)+Θ(d). S(d) = (1 + \sqrt{d}) \cdot S(\sqrt{d}) + \Theta (\sqrt{d}).

To solve the recursion, assume that S(4)=1S(4) = 1 (by proper scaling). Further, assuming that S(d)≤d−2S(d) \le d - 2. Then, we can verity that the recursion holds:

S(d)≤(1+d)⋅(2d−2)+d=d−2. S(d) \le (1 + \sqrt{d}) \cdot (2 \sqrt{d} - 2) + \sqrt{d} = d - 2.

□\square

Theorem. vEB(d)\text{vEB}(d) can be implemented with O(nlog⁡log⁡d)O(n \log \log d) space.

Proof. One improvement to the previous approach is to avoid maintaining all d\sqrt{d} pointers to the children trees c0,…,cdc_0, \ldots, c_{\sqrt{d}}, and constructing all of them explicitly.

Instead, we construct a child tree only if it is non-empty, and keep a hash table in the vEB(d)\text{vEB}(d) structure, which stores the pointers to these non-empty children trees.

Inserting an element has time complexity O(log⁡log⁡d)O(\log \log d), therefore it can creates at most O(log⁡log⁡d)O(\log \log d) pointers, as well as subtrees. Since when a subtree is created, it takes space O(1)O(1), therefore the total space usage is O(nlog⁡log⁡d)O(n \log \log d).

This bound is tight. Suppose we want to insert an element xx into a vEB(d)\text{vEB}(d) structure. If cxhc_{x_h} is empty, then we invoke the recursion insert(s,xh)(s, x_h) to insert xhx_h into the summary. However, in this case, we need to crete the subtree for cxhc_{x_h} (which takes O(1)O(1) space), insert the pointer for cxhc_{x_h} into the hash table (which also takes O(1)O(1) space). This can happen for each recursion, therefore the total space overhead is O(log⁡log⁡d)O(\log \log d) for a single insertion.

□\square

Reference

[1] T. H. Cormen, Ed., Introduction to algorithms 3rd, 3rd ed. Cambridge, Mass: MIT Press, 2009.