Introduction
First introduced in the 1950s [48 , 35 ] , the linear-probing hash table has stood for more than half a century as one of the simplest and most widely used hash table designs. In its most basic form, the hash table works as follows. Elements live in an array of some size m m m , and each element x x x hashes to a uniformly random position h ( x ) h(x) h ( x ) in that array. To insert an item x x x , we examine cells h ( x ) , h ( x ) + 1 , h ( x ) + 2 , … h(x), h(x) + 1, h(x) + 2, \ldots h ( x ) , h ( x ) + 1 , h ( x ) + 2 , … (mod m m m ) until we find a free cell where we place the item; and, likewise, to query an item x x x , we scan the cells h ( x ) , h ( x ) + 1 , h ( x ) + 2 , … h(x), h(x) + 1, h(x) + 2, \ldots h ( x ) , h ( x ) + 1 , h ( x ) + 2 , … (mod m m m ) and until we either find x x x , or until we encounter a free cell (meaning that x x x is not present).
What makes linear probing special—both in practice and in theory—is its spatial locality [37 , 44 , 14 , 16 , 24 , 50 ] : each operation accesses just one small contiguous region of the hash table. We refer to the size of this region as the scan cost of the operation. (This quantity has also been called the locality of the operation [57 ] ).
At constant load factor 1 − Θ ( 1 ) 1 - \Theta(1) 1 − Θ ( 1 ) , the expected scan cost per operation is O ( 1 ) O(1) O ( 1 ) . When the hash table is filled to a high load factor 1 − ε 1 - \varepsilon 1 − ε for a small parameter ε ∈ ( 0 , 1 ) \varepsilon\in(0,1) ε ∈ ( 0 , 1 ) , naive linear probing suffers from a phenomenon known as primary clustering [35 , 38 , 37 ] , which increases the worst-case expected scan cost to Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) . Fortunately, this clustering effect can actually be eliminated with the help of additional algorithmic optimizations (the use of the Robin-Hood ordering to improve queries [3 , 19 ] and the use of graveyard hashing [14 ] to improve insertions). With these optimizations in place, the expected scan cost per operation is O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) .
The above results all operate under the assumption that items have a fixed size. In many applications, however, this is not the case: An item x x x may occupy an arbitrary number ∣ x ∣ ≥ 1 |x| \ge1 ∣ x ∣ ≥ 1 of words (representing, for instance, a variable-length key together with a variable-length value). A common workaround is to use indirection : store a fixed-size pointer in the hash table and allocate the item itself elsewhere in memory. But indirection defeats the very spatial locality that makes linear probing so desirable—each operation must now access multiple separate regions of memory.
This raises a natural question: Can linear probing be adapted to store variable-length items inline , so that the expected scan cost per query is close to O ( ε − 1 ⋅ ∣ x ∣ ) O(\varepsilon^{-1} \cdot|x|) O ( ε − 1 ⋅ ∣ x ∣ ) , where ∣ x ∣ |x| ∣ x ∣ is the length of the item being queried? In other words, does there exist a data structure that behaves like linear probing, but that supports items of arbitrary sizes?
Such a data structure would also be of direct interest in the external-memory model [1 ] , where memory is divided into blocks of size B B B and the cost of an operation is the number of blocks it accesses (called I/Os). In this model, a linear-probing operation with scan cost S S S incurs 1 + O ( S / B ) 1 + O(S/B) 1 + O ( S / B ) I/Os in expectation, so bounds on scan cost translate directly into bounds on I/Os. Thus, another way to frame our question is simply: does there exist a cache-oblivious1 external-memory data structure in which querying an item x x x costs 1 + O ( ε − 1 ∣ x ∣ / B ) 1 + O(\varepsilon^{-1}|x|/B) 1 + O ( ε − 1 ∣ x ∣/ B ) I/Os in expectation?
This Paper: Catalan Hashing. In this paper, we introduce Catalan hashing , which successfully extends linear probing to support variable-length items. In the final version of the data structure (Theorem 1.4), we are able to support queries with an amortized expected scan cost of
O ( ε − 1 ⋅ ∣ x ∣ ) , O(\varepsilon^{-1} \cdot|x|), O ( ε − 1 ⋅ ∣ x ∣ ) ,
and insertions and deletions with an amortized expected scan cost of
O ( ε − 1 ⋅ ∣ x ∣ ⋅ log 1 + δ ∣ x ∣ ) , O(\varepsilon^{-1} \cdot|x| \cdot\log^{1+\delta}|x|), O ( ε − 1 ⋅ ∣ x ∣ ⋅ log 1 + δ ∣ x ∣ ) ,
where δ > 0 \delta> 0 δ > 0 is a positive constant of our choice. The query bound applies to both positive queries (for elements that are present) and negative ones (for elements that are absent). In fact, for negative queries, we are able to achieve an even stronger expected scan-cost bound of O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) , independent of the item size ∣ x ∣ |x| ∣ x ∣ .
We develop our results systematically in five parts. Parts 1–3 cover static and then dynamic constructions in the constant-load-factor regime (load factor 1 − Θ ( 1 ) 1-\Theta(1) 1 − Θ ( 1 ) ). Parts 4–5 then extend these constructions to the high-load-factor regime (load factor 1 − ε 1-\varepsilon 1 − ε ). We state our results in terms of scan cost, which implies corresponding I/O bounds. We state these explicitly here for our final construction and following the theorems in the main text.
Part 1: Simple Catalan Hashing (Static, Low Load Factor). A naive way to store items inline in a linear-probing hash table would be to store each item x x x as a contiguous block of ∣ x ∣ |x| ∣ x ∣ words starting at or after its hash location h ( x ) h(x) h ( x ) . However, as observed by Thorup [54 ] , this can blow up the cost of small items: a single large item of length L L L occupies L L L consecutive cells, so an item of length 1 that hashes into that interval is pushed Ω ( L ) \Omega(L) Ω ( L ) cells away from its hash location.
In Part 1 (§ 4 and § 5 ), we introduce (Simple) Catalan hashing , a static linear probing hash table (designed for low load factors 1 − Θ ( 1 ) 1-\Theta(1) 1 − Θ ( 1 ) ) that resolves the problem described above. The basic idea is to store items non-contiguously, nesting smaller items inside larger ones. We then augment each cell of the table to store an O ( 1 ) O(1) O ( 1 ) -bit tag indicating where items begin and end. The items are laid out in such a way that, whenever a smaller item x x x overlaps a larger one y y y , x x x is contained entirely within y y y . If one thinks of each tag as encoding either an open parenthesis (starting a new item) or a closing parenthesis (ending the current item), then the tags form a balanced-parentheses sequence (i.e., a Catalan sequence [53 ] ), hence the name of the data structure.
The main result of Part 1 is the following theorem:
Theorem 1.1 (Simple Catalan Hashing). Let S S S be a static set of variable-length items stored in an m m m -cell table at load factor load ( S ) = 1 m ∑ y ∈ S ∣ y ∣ ≤ 1 − Ω ( 1 ) \operatorname{load}(S)=\frac{1}{m}\sum_{y\in S}|y|\le1-\Omega(1) load ( S ) = m 1 ∑ y ∈ S ∣ y ∣ ≤ 1 − Ω ( 1 ) . Simple Catalan hashing uses O ( 1 ) O(1) O ( 1 ) bits of metadata per cell, answers positive queries for any item x ∈ S x\in S x ∈ S with expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) , and answers negative queries with expected scan cost O ( 1 ) O(1) O ( 1 ) .
Note that the bound for negative queries (queries to keys that are absent) does not even depend on the length ∣ x ∣ |x| ∣ x ∣ of the item being queried. Negative queries make use of the following mechanism: If an item x x x hashes to some cell c c c , and x x x is the last item in the table with that hash, then at the end of x x x we store an O ( 1 ) O(1) O ( 1 ) -bit tag that indicates this. A negative query for a key k k k with hash c c c can therefore stop as soon as it reaches the tag at the end of x x x . Of course, if x x x is very long, then this could make the query very expensive! Critically, however, long items are necessarily rare (the sum of the lengths of all items is at most the size of the table), so a negative query is unlikely to share its hash with that of a long item. Summing over the items in the table, we show that the expected scan cost of a negative query is just O ( 1 ) O(1) O ( 1 ) .
Part 2: Write Optimization via Erasure Codes (Dynamic, Low Load Factor). Next, in Part 2, we turn to the question of how to support insertions (deletions are deferred to Part 5). Note that, here, we are still only focusing on hash tables that operate at low load factors—we consider higher load factors in Parts 4 and 5.
Note that naively dynamizing the simple Catalan hash table results in severe write amplification (insertions are very expensive). Suppose, for example, that the table stores a long item y y y of length L L L , and we insert an item x x x of length 1 whose hash lands in the middle of y y y . In the static layout, x x x interrupts y y y , so inserting x x x requires shifting the remaining Ω ( L ) \Omega(L) Ω ( L ) words of y y y forward, giving the insertion a scan cost of Ω ( L ) \Omega(L) Ω ( L ) . Worse, if y y y is itself nested inside an even longer item y ′ y' y ′ , then shifting y y y forces y ′ y' y ′ to shift as well, triggering a cascade of updates across items of increasing size.
In the external-memory literature, the classic way to resolve write amplification is to make the data structure write optimized [42 , 4 , 18 , 9 , 29 , 10 , 21 ] . However, existing write-optimized data structures (such as B ε B^{\varepsilon} B ε -trees [18 ] , LSM-trees [42 ] , cache-oblivious streaming B-trees [9 ] , etc.) rely on hierarchical trees of buffers to batch small updates as they trickle down the tree. In a linear-probing hash table, however, there is no tree, and no natural way to add hierarchical buffers in a way that respects spatial locality. Thus, a priori , write optimization would seem to be off the table.
Our solution is a new mechanism for write optimization in linear-probing hash tables: using erasure codes so that large items can themselves act as “buffers” for small items . Instead of storing an item y y y of length s = ∣ y ∣ s = |y| s = ∣ y ∣ directly, we encode y y y using an erasure code Enc ( y ) \operatorname{Enc}(y) Enc ( y ) of length 2 s 2s 2 s from which y y y can be reconstructed using any s s s surviving symbols. When a newly inserted short item x x x needs a cell that is currently occupied by a longer element y y y , x x x does not move y y y at all—it simply overwrites y y y ’s cell in place. As long as at least s s s of the 2 s 2s 2 s cells of Enc ( y ) \operatorname{Enc}(y) Enc ( y ) survive, any query for y y y can still decode y y y in a single contiguous scan, just as before. Only when half of Enc ( y ) \operatorname{Enc}(y) Enc ( y ) has been overwritten do we reinflate y y y by reinserting it as a fresh length-2 s 2s 2 s codeword; critically, at that point, at least s = ∣ y ∣ s = |y| s = ∣ y ∣ words of shorter items have been written on top of y y y , which is enough to amortize the cost of reinflating y y y .
To illustrate the high-level idea behind this technique, we begin in § 6 with a warmup in which we analyze this erasure-coded Catalan hash table under the simplifying assumption that an erasure oracle notifies the algorithm whenever half of an item’s codeword has been overwritten (and supplies the original item to be reinflated). Under this oracle assumption, we prove (Theorem 5.5 ) that positive queries and insertions both have amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) .
Part 3: Removing the Oracle (Dynamic, Low Load Factor). In Part 3 (§ 7 ), we show how to remove the oracle assumption from Part 2. What makes removing the oracle nontrivial is that, when a short insertion overwrites a cell belonging to a long item y y y , the algorithm cannot, for example, afford to update any counter that is stored at the start of y y y . Because y y y may be very long, the start of y y y may lie far before the cell being overwritten, so updating a per-item counter on every overwritten cell would destroy our O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) bound on the scan cost of an insertion. This makes it difficult to identify when an item y y y has had more than half of its codewords overwritten.
Our solution is to do nothing at all to y y y when its cells are overwritten, and instead reinflate heavily overwritten items lazily during queries . Whenever an item x x x is inserted, we also store a backup copy of x x x in a secondary backup table B \mathcal{B} B that is interleaved into the same physical array. Most queries for an item y y y simply find at least ∣ y ∣ |y| ∣ y ∣ surviving symbols of Enc ( y ) \operatorname{Enc}(y) Enc ( y ) in the primary table and decode y y y with scan cost O ( ∣ y ∣ ) O(|y|) O ( ∣ y ∣ ) . If, however, more than half of Enc ( y ) \operatorname{Enc}(y) Enc ( y ) has been overwritten, the query falls back to recovering y y y from the backup table B \mathcal{B} B and reinflates Enc ( y ) \operatorname{Enc}(y) Enc ( y ) in the primary table.
At first glance, introducing B \mathcal{B} B might appear to be circular, since B \mathcal{B} B must itself dynamically store variable-length items. The crucial difference is that a query for y y y only accesses B \mathcal{B} B when at least ∣ y ∣ |y| ∣ y ∣ cells of Enc ( y ) \operatorname{Enc}(y) Enc ( y ) have been overwritten since y y y was last inserted or reinflated. We can therefore amortize the cost of querying B \mathcal{B} B (and reinflating y y y ) against the insertions that caused those overwrites. Critically, this means that we can get away with B \mathcal{B} B supporting slower queries than what we want in the full data structure—it just needs to support all operations with scan cost O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) . The final contribution of Section 6 is to construct a backup table that does just that. Integrating this into the full data structure gives the following theorem:
Theorem 1.2 (Dynamic Catalan Hashing at Low Load Factor). Fix any constant δ > 0 \delta> 0 δ > 0 . There is an insertion-only dynamic Catalan hash table that supports some constant load factor Ω ( 1 ) \Omega(1) Ω ( 1 ) , and that answers positive queries for any item x x x with amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) , answers negative queries with expected scan cost O ( 1 ) O(1) O ( 1 ) , and supports insertions of any item x x x with amortized expected scan cost O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) .
Part 4: Fair Catalan Hashing (Static, High Load Factors). In Part 4 (§ 8 ), we move from constant load factors to the high-load-factor regime (load factor 1 − ε 1-\varepsilon 1 − ε for some small ε ∈ ( 0 , 1 ) \varepsilon\in(0,1) ε ∈ ( 0 , 1 ) ). To tackle this regime, we begin by returning to the static setting.
At high load factors, simple Catalan hashing ends up encountering the same quadratic barrier as classical linear probing—the worst-case expected scan cost for an item x x x becomes Θ ( ε − 2 ∣ x ∣ ) \Theta(\varepsilon^{-2}|x|) Θ ( ε − 2 ∣ x ∣ ) . The standard fix in the unit-size setting is to use the Robin Hood hashing [3 , 19 ] , which sorts elements in each run in increasing order of hash. But, for variable-length items, this approach fails, because a single long item can blow up the query time of many subsequent short items with larger hashes.
To overcome this obstacle, we establish a connection between variable-length linear probing and preemptive single-server scheduling [51 , 33 , 32 , 6 , 5 , 26 , 28 ] . If we view each item x x x as a job of size ∣ x ∣ |x| ∣ x ∣ arriving at time h ( x ) h(x) h ( x ) , then we can reinterpret the task of placing the items into the table as a scheduling problem. Under this view, the scan cost of an item x x x divided by its size ∣ x ∣ |x| ∣ x ∣ is the same as the stretch of the corresponding job.
What we want, then, is a scheduling policy that is fair across all job sizes, guaranteeing worst-case expected stretch O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) . In queuing theory, the simplest such policy is last-in-first-out with preemption , in which newly-arrived jobs immediately preempt any existing jobs. In our hash-table setting, this policy can be viewed as giving priority to items with larger hashes. Note that this prioritization is the exact opposite of the classical Robin Hood ordering [3 , 19 ] . We call the resulting data structure anti-Robin-Hood Catalan hashing (ARCH).
In Section 7, we give a simple, self-contained proof that the ARCH table does, indeed, guarantee an expected scan length of at most ε − 1 ∣ x ∣ \varepsilon^{-1}|x| ε − 1 ∣ x ∣ .2 Perhaps ironically, this means that the anti-Robin-Hood ordering is actually fairer than its Robin Hood counterpart, once we allow for variable-length items.
Theorem 1.3 (Anti-Robin-Hood Catalan Hashing). Let ε ∈ ( 0 , 1 ) \varepsilon\in(0,1) ε ∈ ( 0 , 1 ) , and let S S S be a static set of items stored in an m m m -cell table with load ( S ) ≤ 1 − ε \operatorname{load}(S) \le1-\varepsilon load ( S ) ≤ 1 − ε . Anti-Robin-Hood Catalan hashing (ARCH) uses O ( 1 ) O(1) O ( 1 ) bits of metadata per cell, answers positive queries for any item x ∈ S x \in S x ∈ S with expected scan cost at most ∣ x ∣ / ε |x|/\varepsilon ∣ x ∣/ ε , and answers negative queries with expected scan cost at most 1 + 1 / ε 1+1/\varepsilon 1 + 1/ ε .
We remark that, although the anti-Robin-Hood ordering bounds the expected stretch of every item, which is what we need in Part 5, its expected maximum stretch can be quite large: if the table contains many items of length 2 and a single item of length L L L , then with constant probability the long item preempts one of the short items, giving that item stretch Ω ( L ) \Omega(L) Ω ( L ) . As an additional result, we show how to bound the expected maximum stretch as well. We adapt the fair-sojourn scheduling protocol of Friedman and Henderson [26 ] to design a variant of Catalan hashing with expected scan cost at most 1 + ∣ x ∣ / ε 1+|x|/\varepsilon 1 + ∣ x ∣/ ε and expected maximum stretch at most 1 + ( ln m + 1 ) / ε 1+(\ln m+1)/\varepsilon 1 + ( ln m + 1 ) / ε (Theorem 7.5 ).
Part 5: Dynamic Catalan Hashing at High Load Factors
Finally, in Part 5 (§ 9 ), we combine our static ARCH construction from Part 4 with our dynamic low-load-factor construction from Parts 2 and 3 to obtain a fully dynamic Catalan hash table at load factor 1 − ε 1-\varepsilon 1 − ε (supporting both insertions and deletions).
Note that the dynamic erasure-coded hash table from Parts 2 and 3 cannot be run directly at load factor 1 − ε 1-\varepsilon 1 − ε : its write-optimization machinery inherently loses a constant factor in space. Conversely, the scheduling-based approach from Part 4 achieves load factor 1 − ε 1-\varepsilon 1 − ε with only O ( 1 ) O(1) O ( 1 ) bits of metadata per cell.
To get the best of both worlds, we use a structure inspired by graveyard hashing [14 ] . At a high level, we store almost all of the items in a static ARCH table H f u l l \mathcal{H}_{\mathrm{full}} H full at load factor 1 − O ( ε ) 1-O(\varepsilon) 1 − O ( ε ) , while routing new insertions into a much smaller dynamic erasure-coded table H s m a l l \mathcal{H}_{\mathrm{small}} H small of size O ( ε m ) O(\varepsilon m) O ( ε m ) that operates at constant load factor. Because H s m a l l \mathcal{H}_{\mathrm{small}} H small is only a Θ ( ε ) \Theta(\varepsilon) Θ ( ε ) fraction of the size of H f u l l \mathcal{H}_{\mathrm{full}} H full , its constant-factor space overhead consumes only O ( ε m ) O(\varepsilon m) O ( ε m ) cells in total. Moreover, so that an operation that accesses both tables still scans just one small region of memory, we interleave H s m a l l \mathcal{H}_{\mathrm{small}} H small uniformly into H f u l l \mathcal{H}_{\mathrm{full}} H full within a single physical array—placing one slot of H s m a l l \mathcal{H}_{\mathrm{small}} H small every Θ ( ε − 1 ) \Theta(\varepsilon^{-1}) Θ ( ε − 1 ) slots of H f u l l \mathcal{H}_{\mathrm{full}} H full —and use a shared hash function h ( x ) h(x) h ( x ) for both tables. That way, any operation that probes both H f u l l \mathcal{H}_{\mathrm{full}} H full and H s m a l l \mathcal{H}_{\mathrm{small}} H small still traverses a single contiguous region of memory around h ( x ) h(x) h ( x ) (stretching the physical scan cost of H s m a l l \mathcal{H}_{\mathrm{small}} H small by a factor of Θ ( ε − 1 ) \Theta(\varepsilon^{-1}) Θ ( ε − 1 ) ).
Insertions go directly into H s m a l l \mathcal{H}_{\mathrm{small}} H small , and a query simply searches both tables. After every Θ ( ε m ) \Theta(\varepsilon m) Θ ( ε m ) words of insertions, we perform a global rebuild that merges H s m a l l \mathcal{H}_{\mathrm{small}} H small into H f u l l \mathcal{H}_{\mathrm{full}} H full using O ( 1 ) O(1) O ( 1 ) sequential passes over the table. Because the rebuild occurs only once every Θ ( ε m ) \Theta(\varepsilon m) Θ ( ε m ) words of updates, its O ( m ) O(m) O ( m ) cost amortizes to O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) per inserted word—or O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) for an item x x x . To support deletions without making negative queries depend on the key length, we keep constant-size fingerprints of deleted static keys in another interleaved table, checking for collisions during rebuilding. The details, together with the low-load deletion mechanism, appear in § 9 and Appendix § 14 . Putting these pieces together gives our main result:
Theorem 1.4 (Dynamic High-Load Catalan Hashing). Let ε ∈ ( 0 , 1 / 2 ) \varepsilon\in(0,1/2) ε ∈ ( 0 , 1/2 ) and let δ > 0 \delta> 0 δ > 0 be a constant. There is a dynamic Catalan hash table operating at load factor 1 − ε 1-\varepsilon 1 − ε that supports positive queries for any item x x x with amortized expected scan cost
O ( ∣ x ∣ ε ) , O\left(\frac{|x|}{\varepsilon}\right), O ( ε ∣ x ∣ ) , supports negative queries with amortized expected scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) , and supports insertions and deletions of any item x x x with amortized expected scan cost
O ( ∣ x ∣ log 1 + δ ∣ x ∣ ε ) . O\left(\frac{|x|\log^{1+\delta}|x|}{\varepsilon}\right). O ( ε ∣ x ∣ log 1 + δ ∣ x ∣ ) .
As an immediate corollary we also get the following result in the external memory model, establishing the existence of a linear-probing-like data structure for items of arbitrary sizes.
Corollary 1.5 (Dynamic High-Load Catalan Hashing in External Memory). Suppose M ≥ C M ( B + s max ε − 2 log 3 m ) M \ge C_M(B+s_{\max}\varepsilon^{-2}\log^3 m) M ≥ C M ( B + s m a x ε − 2 log 3 m ) for a sufficiently large constant C M C_M C M , where s max s_{\max} s m a x is the maximum length of any item stored or inserted during the operation sequence. The hash table of Theorem 1.4 is cache-oblivious and, for every block size B B B , supports positive queries for any item x x x in amortized expected
1 + O ( ∣ x ∣ ε B ) 1+O\left(\frac{|x|}{\varepsilon B}\right) 1 + O ( εB ∣ x ∣ ) I/Os, supports negative queries in amortized expected 1 + O ( 1 / ( ε B ) ) 1+O(1/(\varepsilon B)) 1 + O ( 1/ ( εB )) I/Os, and supports insertions and deletions of any item x x x in amortized expected
1 + O ( ∣ x ∣ log 1 + δ ∣ x ∣ ε B ) 1+O\left(\frac{|x|\log^{1+\delta}|x|}{\varepsilon B}\right) 1 + O ( εB ∣ x ∣ log 1 + δ ∣ x ∣ ) I/Os.
Paper Outline. The rest of the introduction is spent discussing in more detail related work on linear probing and on external-memory data structures. Then, after preliminaries in § 3 , we introduce the basic machinery that all of our static constructions share in § 4 , and then we complete Parts 1–5 in Sections 4 to 8, respectively.
Additional Related Work
For fixed-size items, linear probing has been studied for more than half a century. The first analysis is due to Knuth in 1963 [35 ] , who showed that, when the hash table is 1 − ε 1-\varepsilon 1 − ε full, insertions and negative queries take Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) expected time; Knuth’s analysis was never published, and the first published analysis was in 1966 by Konheim and Weiss [38 ] . Amble and Knuth [3 ] showed that, by keeping the items in each run in sorted order, one can reduce the worst-case expected query time to O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) (this has become known as Robin-Hood hashing [19 ] due to the way in which it gives priority to elements which have traveled farther). Much more recently, Bender, Kuszmaul, and Kuszmaul [14 ] showed how to also achieve this O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) bound for insertions and deletions; this is achieved by graveyard hashing , in which one occasionally rebuilds the hash table to have strategically placed gaps (tombstones) that break apart runs for subsequent insertions. The authors also showed [14 , 16 ] that, even in standard ordered linear probing with tombstone-based deletions, the amortized expected insertion time is better than one might assume, clocking in at Θ ( ε − 1 log 1.5 ε − 1 ) \Theta(\varepsilon^{-1}\log^{1.5}\varepsilon^{-1}) Θ ( ε − 1 log 1.5 ε − 1 ) . Most recently, there has been interest in what the optimal scan cost is in a (insert-only) hash table that never moves items after they are inserted. Zamir [57 ] showed that, in this setting, the Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) scan cost of linear probing is, in a certain sense, optimal: no such hash table can achieve scan cost o ( ε − 2 ) o(\varepsilon^{-2}) o ( ε − 2 ) at all load factors simultaneously. In concurrent work, Krapivin, Kuszmaul, and Wang [39 ] showed that, if ε \varepsilon ε is known in advance, then it is possible to achieve a worst-case expected scan cost for insertions and positive queries of O ( ε − 1 log ε − 1 ) O(\varepsilon^{-1}\log\varepsilon^{-1}) O ( ε − 1 log ε − 1 ) , and that (somewhat surprisingly) no strategy can do better than Ω ( ε − 1 log ε − 1 ) \Omega(\varepsilon^{-1}\sqrt{\log\varepsilon^{-1}}) Ω ( ε − 1 log ε − 1 ) . All of this work, however, focuses on the setting in which items have a fixed size.
All of the data structures in this paper come with natural corollaries for the external-memory model [1 ] (see § 3 for a more formal treatment of this model). In many settings, the algorithm may not know the block-size parameter B B B in advance (or may even be interacting with multiple different types of caches, using different block sizes, simultaneously). Algorithms that get strong guarantees even in this setting (such as those in this paper) are known as cache oblivious [27 ] . These models underlie a large body of search structures and write-optimized dictionaries, including cache-oblivious B-trees [8 ] , packed-memory arrays and their adaptive variants [13 ] , B ε B^\varepsilon B ε -trees [18 ] , log-structured merge (LSM) trees [42 ] , and cache-oblivious streaming B-trees [9 ] . There has also been a great deal of work on designing hash tables and unordered dictionaries in this setting [23 , 41 , 40 , 30 , 45 , 29 , 21 , 14 , 7 , 16 ] , but with a focus on settings where all items have the same lengths.
Prior to this paper, most of the work on external-memory data structures for variable-length objects has focused on how to extend the B B B -tree and its variants to the variable-length setting [25 , 17 , 11 ] . In contrast, in the current paper, we study the question of how to best extend the guarantees of a hash table . Perhaps surprisingly (with the exception of [54 ] which we will discuss shortly), this question does not appear to have been considered in prior work.
The idea of placing variable-length strings into a linear-probing hash table appears to have been first discussed in Thorup’s paper on designing hash functions for hash tables storing strings [54 Section 6, Theorem 6.1] . For load factor α ≤ 0.9 \alpha\le0.9 α ≤ 0.9 , Thorup bounds the expected probe length for a positive query to a string x x x by ∣ x ∣ + O ( α R ) |x|+O(\alpha R) ∣ x ∣ + O ( α R ) , where R = ( ∑ y ∈ S ∣ y ∣ 2 ) / ( ∑ y ∈ S ∣ y ∣ ) R=(\sum_{y\in S}|y|^2)/(\sum_{y\in S}|y|) R = ( ∑ y ∈ S ∣ y ∣ 2 ) / ( ∑ y ∈ S ∣ y ∣ ) is the second-moment-to-first-moment ratio of the string lengths in the table S S S . When the table contains even a few very long strings, R R R can be much larger than ∣ x ∣ |x| ∣ x ∣ , causing queries for short strings to pay for the lengths of unrelated long strings. One of the first insights in the current paper, is that by allowing shorter strings to nest within longer ones (via Catalan hashing), we can avoid this kind of cost blow-up for queries to short strings.
Throughout this paper, we assume access to fully random hash functions. There has also been a great deal of work on how to implement linear probing with explicit hash functions [44, 46, 54, 55, 47, 15], as well as, more generally, how to simulate random hash functions for essentially any hash table [52 , 43 , 22 ] (see, e.g., the discussion in [12 ] ).
Preliminaries
Conventions. All logarithms are base 2. In asymptotic cost bounds, we use the convention log s = max { 1 , log 2 s } \log s = \max\{1,\log_{2}s\} log s = max { 1 , log 2 s } for s ≥ 1 s \ge1 s ≥ 1 . For ease of exposition, we ignore divisibility and rounding issues whenever they do not affect the stated asymptotic bounds.
Items, Keys, and Lengths. All lengths in this paper are measured in words . As in the standard word-RAM model, we assume that a word has at least c w log m c_w \log m c w log m bits for some constant c w c_w c w .
The elements stored in our hash table are variable-length items . Each item x = ( k , v ) x=(k,v) x = ( k , v ) consists of a key k k k and a value v v v , both of which may have variable length (the value may also be empty). We store x x x as a sequence of ∣ x ∣ ≥ 1 |x| \ge1 ∣ x ∣ ≥ 1 words, which we call the length (or size ) of x x x , encoded so that the key can be read off from a prefix of x x x (for example, by storing the length of k k k at the start of x x x , or by ending k k k with a terminator, as in a C string). Each key belongs to at most one item in the hash table, and we sometimes write k ( x ) k(x) k ( x ) for the key of x x x and v ( x ) v(x) v ( x ) for the value of x x x .
A query is given only a key k k k ; it returns Found and the value v ( x ) v(x) v ( x ) , where x ∈ S x \in S x ∈ S is the item with key k k k if there is one (a positive query), and Not Found otherwise (a negative query). (If values are empty, a positive query simply returns Found.) In particular, a query does not necessarily know the length of the item it is looking for. In our query bounds, for positive queries, x x x denotes the item found. For negative queries, the bounds hold with ∣ x ∣ = 1 |x|=1 ∣ x ∣ = 1 , independently of the length of the query key. All hash functions are applied to keys, so the hash h ( x ) = h ( k ( x ) ) h(x)=h(k(x)) h ( x ) = h ( k ( x )) of an item depends only on its key, and we write h ( k ) h(k) h ( k ) where convenient.
Hashing, Load Factor, and Runs. In this paper, a hash table H \mathcal{H} H consists of m m m cells, indexed cyclically as { 0 , 1 , … , m − 1 } \{0,1,\ldots,m-1\} { 0 , 1 , … , m − 1 } (or equivalently Z / m Z \mathbb{Z}/m\mathbb{Z} Z / m Z ). We assume access to a fully random hash function h h h that maps each key independently and uniformly to the continuous circle [ 0 , m ) [0,m) [ 0 , m ) . We treat hash values as real numbers, and assume that basic operations on them, such as arithmetic and comparisons, take O ( 1 ) O(1) O ( 1 ) time.3 We also assume that the sequence of operations is fixed in advance, independently of the hash functions, or equivalently, that the input sequence is chosen by an oblivious adversary . The canonical cell of an item x x x is determined by its key k ( x ) k(x) k ( x ) , defined as c ( x ) = c ( k ( x ) ) = ⌊ h ( x ) ⌋ ∈ { 0 , … , m − 1 } c(x)=c(k(x))=\lfloor h(x)\rfloor\in\{0,\ldots,m-1\} c ( x ) = c ( k ( x )) = ⌊ h ( x )⌋ ∈ { 0 , … , m − 1 } . The fractional part of h ( x ) h(x) h ( x ) may be used to break ties between items hashing to the same canonical cell.
Given the set of items S S S stored in an m m m -cell table, the total volume of S S S is Vol ( S ) = ∑ x ∈ S ∣ x ∣ \operatorname{Vol}(S)=\sum_{x\in S}|x| Vol ( S ) = ∑ x ∈ S ∣ x ∣ , and the load factor (or fill ) of the table is load ( S ) = Vol ( S ) / m \operatorname{load}(S)=\operatorname{Vol}(S)/m load ( S ) = Vol ( S ) / m . When load ( S ) ≤ α < 1 \operatorname{load}(S)\le\alpha<1 load ( S ) ≤ α < 1 , we say that the table has load factor at most α \alpha α and slack ε = 1 − α \varepsilon=1-\alpha ε = 1 − α . Any metadata used by the algorithm is not counted in the load factor, but the metadata used in the hash table in this paper only adds a constant factor overhead in the constant-load regimes and is a low-order term in the high-load regimes. In the low-load-factor sections of the paper (§ 5 to § 7 ), we assume α ≤ 1 − Ω ( 1 ) \alpha\le1-\Omega(1) α ≤ 1 − Ω ( 1 ) , i.e., the load factor is bounded away from 1 by a positive constant. In the high-load-factor sections (§ 8 and § 9 ), we parameterize the performance directly in terms of ε \varepsilon ε .
A run (or cluster ) is a maximal contiguous interval of occupied cells in H \mathcal{H} H . Whenever load ( S ) < 1 \operatorname{load}(S)<1 load ( S ) < 1 , at least one cell of H \mathcal{H} H is empty, so every run is preceded immediately by an empty cell and has a well-defined left-to-right linear order obtained by unwrapping the circular array at that empty cell. For a query to the key of an item x x x , the scan length L x L_x L x is the number of contiguous cells inspected by the query starting from c ( x ) c(x) c ( x ) , and the query stretch is L x / ∣ x ∣ L_x/|x| L x /∣ x ∣ .
Scan Cost. Nearly all of our algorithms access a contiguous cyclic interval of the hash table. Following classical linear-probing work [35 , 37 ] , we define the scan cost of an operation on the hash table as the size of the smallest cyclic interval that contains all of its accessed cells. Our primary analysis will report the scan cost of our algorithms, which then can be easily converted into external-memory cost (defined below).
The External-Memory and Cache-Oblivious Models. We use the standard cache-oblivious model of Frigo, Leiserson, Prokop, and Ramachandran [27 ] , building on the external-memory (I/O) model of Aggarwal and Vitter [1 ] . In the external-memory model, memory consists of an internal cache of size M M M words and an external memory partitioned into blocks of B B B consecutive words. An I/O transfers one block of B B B words between cache and external memory. The cost of a sequence of operations is the number of I/Os that it incurs. An algorithm is cache-oblivious if its specification does not depend on the parameters M M M and B B B ; these appear only in the analysis.
We store the table using a uniformly random rotation (chosen at initialization, independently of the hash function). A forward scan of a contiguous interval of L L L cells costs O ( 1 + L / B ) O(1+L/B) O ( 1 + L / B ) I/Os, and 1 + O ( L / B ) 1+O(L/B) 1 + O ( L / B ) in expectation over the rotation. The same bounds hold for an operation making O ( 1 ) O(1) O ( 1 ) forward passes over the interval. We refer to this cost as the scan property .
For ease of exposition, we assume circularity of the array in the remainder of this paper and do not explicitly refer to circular order, cyclic intervals, etc., except where necessary. We will sometimes implicitly or explicitly refer to the hash or other functions as interpreted cyclically as follows. If a run starts at cell a a a , wraps around the end of the array and ends at cell b b b and h ( x ) ∈ [ 0 , b ) h(x) \in[0,b) h ( x ) ∈ [ 0 , b ) , we consider x x x ’s hash to be h ( x ) + m h(x)+m h ( x ) + m , so that within the run, hashes are always monotonic.
We remark that one can also straightforwardly optimize the data structures in this paper to be efficient in the word-RAM model, so that the time per operation is the same as the scan cost (plus O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) ). The only nontrivial optimization needed is to replace the erasure codes introduced in § 6 with codes that supports linear-time operations [2 ] .
Cache-Size Assumptions. We make what we call the working-set assumption , which says that the cache has room for the query key, one other key, the parser state, and a sufficiently large constant number of blocks. This is the only cache-size assumption needed by the static constructions (§ 5 and § 8 ). The dynamic constructions will require moderate additional amounts of memory as stated in their respective sections.
Catalan Hashing
This section introduces the common infrastructure used by the hash tables in this paper, all of which we refer to as types of Catalan hashing . The static constructions in this paper (§ 5 and § 8 ) will make direct use of the framework described here, while the dynamic sections (§ 6 , § 7 and § 9 ) will need to white-box extend it in various ways.
The Nesting Property. In a Catalan hash table, we do not necessarily store each item contiguously. Instead, we allow some items to preempt others. For example, if we have one very large item y y y , say of length 100 with canonical cell c ( y ) = 1 c(y)=1 c ( y ) = 1 , and one very small item x x x , say of length 2 with c ( x ) = 3 c(x)=3 c ( x ) = 3 , then we might place the first two words of y y y in cells 1 and 2, then place the two words of x x x in cells 3 and 4, and then resume placing the remaining 98 words of y y y in cells 5 through 102. This allows the short item x x x to avoid paying any kind of substantial query penalty for being placed near a large item y y y .
Formally, our static constructions will satisfy the following nesting property. For each stored item x x x in a run, let I x = [ a x , b x ] I_x = [a_x,b_x] I x = [ a x , b x ] denote the interval of cells from the first word of x x x (a x a_x a x ) to the last word of x x x (b x b_x b x ).
Definition 3.1 (Catalan Nesting Property). A placement of items into H \mathcal{H} H satisfies the Catalan nesting property if, within every run, for any two distinct items x x x and y y y :
(1) Either I x I_x I x and I y I_y I y are disjoint (I x ∩ I y = ∅ I_x \cap I_y = \varnothing I x ∩ I y = ∅ ), or one is strictly contained in the other (I y ⊊ I x I_y \subsetneq I_x I y ⊊ I x or I x ⊊ I y I_x \subsetneq I_y I x ⊊ I y ); and
(2) Whenever I y ⊊ I x I_y \subsetneq I_x I y ⊊ I x , no cell in I y I_y I y stores a word of x x x (equivalently, the entire interval I y I_y I y lies strictly between two consecutive cells assigned to x x x ).
When describing a given static construction (in § 5 and § 8 ), the key distinguishing feature will be the placement rule that we use within the table. This rule is responsible for deciding which items are nested within which other items, and where the nesting starts and ends. The simplest construction that we consider (simple Catalan hashing in § 5 ) uses a placement rule in which smaller items always take priority over larger items. This, as we will see, leads to good results in hash tables that are not very full (load factor 1 − Ω ( 1 ) 1 - \Omega(1) 1 − Ω ( 1 ) ), but not for hash tables that are much fuller (load factor 1 − ε 1 - \varepsilon 1 − ε ). To obtain hash tables that behave well even under high load, we will end up needing a more sophisticated placement rule (fair Catalan hashing in § 8 ).
The Catalan Representation. The static constructions in § 5 and § 8 use the following basic metadata scheme which we call the Catalan representation .
The Catalan representation augments each cell of H \mathcal{H} H with a 3-bit delimiter tag taking one of five values:
OPEN, MIDDLE, CLOSE, OPEN-CLOSE, EMPTY.
An unoccupied cell is marked EMPTY. If an item x x x has length ∣ x ∣ = 1 |x| = 1 ∣ x ∣ = 1 , the single cell holding its word is marked OPEN-CLOSE. If ∣ x ∣ ≥ 2 |x| \ge2 ∣ x ∣ ≥ 2 , the cell holding the first word of x x x is marked OPEN, the cell holding the last word of x x x is marked CLOSE, and the ∣ x ∣ − 2 |x| - 2 ∣ x ∣ − 2 cells holding the interior words of x x x are marked MIDDLE.
The name Catalan hashing comes from the combinatorial structure of these delimiters: in any run satisfying Definition 3.1 , if we ignore MIDDLE tags and view OPEN as “(”, CLOSE as “)”, and OPEN-CLOSE as “()”, the sequence of delimiters in the run forms a balanced parenthesis string, known as a Catalan sequence [53 ] .
Each cell additionally stores 2 bits called the home and stop bits. The home and stop bits indicate home and stop cells, which are used to limit the scan length of negative queries. A cell which is the canonical cell for some item in the table is called a home cell . Each home cell j j j has a corresponding stop cell , which is the last cell containing a word from an item whose canonical cell is j j j .
Parsing the Nested Structure. For a placement satisfying the Catalan nesting property, any occupied cell belongs to the innermost item interval I x I_x I x that contains it. This makes it possible for a query starting at any cell t t t (including a cell in the middle of a run) to parse the stream of cells using a stack-based pushdown automaton which we will refer to as a Catalan parser . At a high level, the parser maintains a stack of the items that it has seen open (via their OPEN tags) but not yet close (via their CLOSE tags). By the nesting property, every MIDDLE or CLOSE cell that it reads belongs to the item on top of the stack, or, if the stack is empty, to an item that opened before t t t . For each item on the stack, the parser tracks how many of the item’s words it has read and whether they agree with the query key so far, so that it can recognize a matching item as soon as that item’s last word is read. The parser’s behavior is described in more detail in Appendix A.
Query Procedure. With the help of this parser, queries work as follows. A query for a key k k k runs the Catalan parser forward from the canonical cell c ( k ) c(k) c ( k ) , and stops as soon as one of the following occurs:
c ( k ) c(k) c ( k ) is not a home cell (its home bit is unset). Then no item in the table has canonical cell c ( k ) c(k) c ( k ) , so the query returns NOT FOUND without scanning further.
The parser finishes reading an item x x x with k ( x ) = k k(x)=k k ( x ) = k . The query returns FOUND, together with the value of x x x .
The parser finishes reading an item y y y with k ( y ) ≠ k k(y)\ne k k ( y ) = k and c ( y ) = c ( k ) c(y)=c(k) c ( y ) = c ( k ) whose last cell is a stop cell (its stop bit is set). Every item with canonical cell c ( k ) c(k) c ( k ) ends at or before this cell, so the query returns NOT FOUND.
Negative versus Positive Queries. One risk for negative queries is that, if an item y y y is not in the table, but has the same canonical cell c ( y ) = c ( x ) c(y)=c(x) c ( y ) = c ( x ) as a very large item x x x in the table, then the scan cost for y y y will be very high (going at least to the end of x x x ). On the other hand, large items x x x are rare, since the sum of the sizes of all items can be at most m m m . This is formalized by the following lemma, which shows that efficient positive queries immediately imply efficient negative queries, regardless of the placement rule used.
Lemma 3.2 (Negative Queries Versus Positive Queries). Consider a static implementation of Catalan hashing using an arbitrary placement rule. For each item x ∈ S x \in S x ∈ S , let L x L_x L x be the scan cost for querying x x x . Let C C C be a parameter, and suppose that E [ L x ] ≤ C ∣ x ∣ \mathbb{E}[L_x] \le C|x| E [ L x ] ≤ C ∣ x ∣ for each x ∈ S x \in S x ∈ S . Then, for any item y y y not in the table, the expected scan cost of a negative query for y y y is at most 1 + C 1+C 1 + C .
Proof. Let D D D be the scan cost of the negative query for y y y . We first observe that
D ≤ 1 + ∑ x ∈ S L x ⋅ 1 [ c ( x ) = c ( y ) ] . (1) D \le1+\sum_{x\in S} L_x \cdot1[c(x)=c(y)]. \tag*{(1)} D ≤ 1 + x ∈ S ∑ L x ⋅ 1 [ c ( x ) = c ( y )] . (1)
Indeed, if c ( y ) c(y) c ( y ) is not a home cell for any x ∈ S x \in S x ∈ S , then the query stops immediately and D = 1 D=1 D = 1 ; otherwise, D D D is simply max x ∈ S : c ( x ) = c ( y ) L x \max_{x\in S:c(x)=c(y)} L_x max x ∈ S : c ( x ) = c ( y ) L x . Either way, (3.1) holds.
We may therefore conclude that
E [ D ] ≤ 1 + ∑ x ∈ S E [ L x ⋅ 1 [ c ( x ) = c ( y ) ] ] . \mathbb{E}[D] \le1+\sum_{x\in S}\mathbb{E}[L_x\cdot1[c(x)=c(y)]]. E [ D ] ≤ 1 + x ∈ S ∑ E [ L x ⋅ 1 [ c ( x ) = c ( y )]] .
Since c ( y ) c(y) c ( y ) is uniformly random, and independent of the randomness used to determine L x L_x L x (or more generally, to place the items S S S into the table), we have that E [ L x ⋅ 1 [ c ( x ) = c ( y ) ] ] = E [ L x ] / m \mathbb{E}[L_x\cdot1[c(x)=c(y)]]=\mathbb{E}[L_x]/m E [ L x ⋅ 1 [ c ( x ) = c ( y )]] = E [ L x ] / m . Therefore,
E [ D ] ≤ 1 + 1 m ∑ x ∈ S E [ L x ] ≤ 1 + C m ∑ x ∈ S ∣ x ∣ . \mathbb{E}[D] \le1+\frac{1}{m}\sum_{x\in S}\mathbb{E}[L_x] \le1+\frac{C}{m}\sum_{x\in S}|x|. E [ D ] ≤ 1 + m 1 x ∈ S ∑ E [ L x ] ≤ 1 + m C x ∈ S ∑ ∣ x ∣.
Since ∑ x ∈ S ∣ x ∣ ≤ m \sum_{x\in S}|x|\le m ∑ x ∈ S ∣ x ∣ ≤ m , the claimed bound of E [ D ] ≤ 1 + C \mathbb{E}[D]\le1+C E [ D ] ≤ 1 + C follows. □
Priority Rules and Construction. All of the static placement rules in this paper are derived from different priority rules. A priority rule assigns a priority π x \pi_x π x to each item x ∈ S x \in S x ∈ S , which can potentially depend on all the items in S S S and their hash values. When multiple items contend for a given cell, a word from the item of highest priority is placed there. We write x ≻ y x \succ y x ≻ y to denote π x > π y \pi_x > \pi_y π x > π y . A priority rule uniquely determines the placement rule, which we realize with the following equivalent constructions.
Bottom-up: Place the items in decreasing priority order as follows. For each item x x x , place the words of x x x in the first ∣ x ∣ |x| ∣ x ∣ empty cells at or after c ( x ) c(x) c ( x ) . Items are never moved, so the previous placement of higher priority items restricts the empty cells available to x x x .
Left-to-right: Starting from beginning of the array, scan forward, maintaining a max-heap of items by priority order and processing cells one at a time. For each cell c c c , insert all items x ∈ S x \in S x ∈ S with c ( x ) = c c(x)=c c ( x ) = c into the heap, then place the next word of the maximum-priority item into c c c . When all the words of an item in the heap are placed, extract it. At the end of the table, if there are still words in the heap left to place, restart at the beginning. This second pass evicts the word in each cell if it has lower priority than the maximum-priority item in the heap, in which case the evicted word is inserted back into the heap. Because the load of S S S is less than 1, the heap will empty before the end of the second pass.
Arbitrary order: Place the items in S S S in an arbitrary order. Call a cell available for x x x if it is empty or contains a lower-priority item. For each item x x x , place the words of x x x in the first ∣ x ∣ |x| ∣ x ∣ available cells after c ( x ) c(x) c ( x ) . Whenever a word of a lower-priority item y y y is evicted, reinsert that word and the remaining suffix of y y y in logical word order. Starting at the evicted cell, use the first cells that are empty or contain words of y y y or of a lower-priority item, so that the suffix shifts to the right while reusing cells already assigned to y y y . Apply the same rule to any further evictions, continuing until all words are placed. This process must terminate, since the load of the table is less than 1.
A placement rule determined by a priority automatically satisfies the Catalan nesting property.
Lemma 3.3. If the placement of items in H \mathcal{H} H is determined by a priority rule, then it satisfies the Catalan nesting property.
Proof. Let x ≻ y ∈ S x \succ y \in S x ≻ y ∈ S . In the bottom-up construction, all the words of x x x are placed first in the first ∣ x ∣ |x| ∣ x ∣ free cells. Therefore, there are no free cells in I x I_x I x , and so when y y y is placed, no words of y y y will be placed in I x I_x I x .
Note that for a static construction for a Catalan hash table, all we need to specify is the priority rule used. This is captured formally via the following definition, which we invoke in Sections 4 and 7.
Definition 3.4. Given a priority rule π \pi π , the static Catalan hash table H π \mathcal{H}_{\pi} H π is the hash table with placement rule and construction determined by π \pi π , using the Catalan representation, parser and query procedure.
Simple Catalan Hashing (Static, Low Load)
We now present and analyze simple Catalan hashing, a static implementation of Catalan hashing designed to operate at low load factors.
Prioritizing Smaller Items. Simple Catalan hashing prioritizes items according to their length: shorter items have strictly higher priority than longer items. Ties between items of the same length are broken in favor of the item with the smaller hash value. Formally, π ( y ) > π ( z ) \pi(y) > \pi(z) π ( y ) > π ( z ) if ∣ y ∣ < ∣ z ∣ |y| < |z| ∣ y ∣ < ∣ z ∣ , or if ∣ y ∣ = ∣ z ∣ |y| = |z| ∣ y ∣ = ∣ z ∣ and h ( y ) < h ( z ) h(y) < h(z) h ( y ) < h ( z ) .
Given S S S with load ( S ) < 1 \operatorname{load}(S) < 1 load ( S ) < 1 , we define the simple Catalan hash table to be H π \mathcal{H}_{\pi} H π as in Definition 3.4 . Following § 4 , this defines a placement rule and construction, and uses the Catalan representation and query algorithm.
Analysis. A key property of simple Catalan hashing is that, to analyze the cost of a positive query for an item x x x of length ℓ = ∣ x ∣ \ell= |x| ℓ = ∣ x ∣ , we can ignore all items in S S S of length greater than ℓ \ell ℓ . Formally, let Y = { y ∈ S ∣ ∣ y ∣ ≤ ℓ } Y = \{y \in S \mid|y| \le\ell\} Y = { y ∈ S ∣ ∣ y ∣ ≤ ℓ } be the subset of items of length at most ℓ \ell ℓ , and imagine forming a Simple Catalan hash table H Y \mathcal{H}_Y H Y containing only the items in Y Y Y . In H Y \mathcal{H}_Y H Y , every cell that is empty corresponds in the full table to either to an empty cell or to a cell occupied by some item z ∈ S ∖ Y z \in S \setminus Y z ∈ S ∖ Y of length ∣ z ∣ ≥ ℓ + 1 |z| \ge\ell+ 1 ∣ z ∣ ≥ ℓ + 1 .
Lemma 4.1. The cost to query x x x in H Y \mathcal{H}_Y H Y is the same as the cost to query x x x in H \mathcal{H} H .
Proof. In the bottom-up construction, the items of Y Y Y are placed before every item of S ∖ Y S \setminus Y S ∖ Y , so each item of Y Y Y occupies exactly the same cells in H \mathcal{H} H as in H Y \mathcal{H}_Y H Y . Since x ∈ Y x \in Y x ∈ Y , the lemma follows.
We now bound the length of the run containing c ( x ) c(x) c ( x ) in H Y \mathcal{H}_Y H Y . We also prove a bound on the second moment of the run length, which we will use later in the paper.
Lemma 4.2. Let W x W_x W x be the length of the run in H Y \mathcal{H}_Y H Y containing c ( x ) c(x) c ( x ) . Then
E [ W x ] = O ( ∣ x ∣ ) and E [ W x 2 ] = O ( ∣ x ∣ 2 ) . \mathbb{E}[W_x] = O(|x|) \quad\text{and} \quad\mathbb{E}[W_x^2] = O(|x|^2). E [ W x ] = O ( ∣ x ∣ ) and E [ W x 2 ] = O ( ∣ x ∣ 2 ) . Proof. Condition on c ( x ) c(x) c ( x ) , and partition the table into blocks of ∣ x ∣ |x| ∣ x ∣ cells. Let I I I be the set of blocks that intersect the run containing c ( x ) c(x) c ( x ) , and let Q = ∣ I ∣ Q = |I| Q = ∣ I ∣ . Every word in the run belongs to an item hashing into the run. All of the blocks in I I I except for the two end blocks are entirely covered by the run, and x x x itself accounts for only ∣ x ∣ |x| ∣ x ∣ words, so the items of Y ∖ { x } Y \setminus\{x\} Y ∖ { x } hashing into I I I have total length at least
max { ( Q − 3 ) ∣ x ∣ , 0 } . \max\{(Q - 3)|x|, 0\}. max {( Q − 3 ) ∣ x ∣ , 0 } . For a fixed interval J J J of q q q blocks, let T ( J ) T(J) T ( J ) be the total length of the items of Y ∖ { x } Y \setminus\{x\} Y ∖ { x } hashing into J J J . Then T ( J ) T(J) T ( J ) is a sum of independent random variables, one per item of Y ∖ { x } Y \setminus\{x\} Y ∖ { x } , each taking values in [ 0 , ∣ x ∣ ] [0, |x|] [ 0 , ∣ x ∣ ] , and
E [ T ( J ) ] ≤ ∑ y ∈ S ∣ y ∣ ⋅ q ∣ x ∣ m ≤ α q ∣ x ∣ . \mathbb{E}[T(J)] \le\sum_{y \in S} |y| \cdot\frac{q|x|}{m} \le\alpha q|x|. E [ T ( J )] ≤ y ∈ S ∑ ∣ y ∣ ⋅ m q ∣ x ∣ ≤ α q ∣ x ∣. Since α < 1 \alpha< 1 α < 1 is fixed, applying a Chernoff bound to T ( J ) / ∣ x ∣ T(J)/|x| T ( J ) /∣ x ∣ (a sum of independent random variables in [ 0 , 1 ] [0, 1] [ 0 , 1 ] with mean at most α q \alpha q α q ) gives, for all sufficiently large q q q ,
Pr [ T ( J ) ≥ ( q − 3 ) ∣ x ∣ ] ≤ e − Ω ( q ) . \Pr\left[T(J) \ge(q - 3)|x|\right] \le e^{-\Omega(q)}. Pr [ T ( J ) ≥ ( q − 3 ) ∣ x ∣ ] ≤ e − Ω ( q ) . If Q = q Q = q Q = q , then I I I is one of the at most q q q intervals of q q q blocks that contain the block of c ( x ) c(x) c ( x ) , and T ( I ) ≥ ( q − 3 ) ∣ x ∣ T(I) \ge(q - 3)|x| T ( I ) ≥ ( q − 3 ) ∣ x ∣ . Taking a union bound over these intervals,
Pr [ Q = q ] ≤ q e − Ω ( q ) ≤ e − Ω ( q ) , \Pr[Q = q] \le qe^{-\Omega(q)} \le e^{-\Omega(q)}, Pr [ Q = q ] ≤ q e − Ω ( q ) ≤ e − Ω ( q ) , and therefore both E [ Q ] = O ( 1 ) \mathbb{E}[Q] = O(1) E [ Q ] = O ( 1 ) and E [ Q 2 ] = O ( 1 ) \mathbb{E}[Q^2] = O(1) E [ Q 2 ] = O ( 1 ) . Finally, W x ≤ ∣ x ∣ Q W_x \le|x|Q W x ≤ ∣ x ∣ Q , so E [ W x ] = O ( ∣ x ∣ ) \mathbb{E}[W_x] = O(|x|) E [ W x ] = O ( ∣ x ∣ ) and E [ W x 2 ] = O ( ∣ x ∣ 2 ) \mathbb{E}[W_x^2] = O(|x|^2) E [ W x 2 ] = O ( ∣ x ∣ 2 ) .
Theorem 1.1 (Simple Catalan Hashing). Let S S S be a static set of variable-length items stored in an m m m -cell table at load factor load ( S ) = 1 m ∑ y ∈ S ∣ y ∣ ≤ 1 − Ω ( 1 ) \operatorname{load}(S) = \frac{1}{m} \sum_{y \in S} |y| \le1 - \Omega(1) load ( S ) = m 1 ∑ y ∈ S ∣ y ∣ ≤ 1 − Ω ( 1 ) . Simple Catalan hashing uses O ( 1 ) O(1) O ( 1 ) bits of metadata per cell, answers positive queries for any item x ∈ S x \in S x ∈ S with expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) , and answers negative queries with expected scan cost O ( 1 ) O(1) O ( 1 ) .
Proof. Each cell stores a 3-bit tag, a home bit, and a stop bit. Consider a positive query for an item x ∈ S x \in S x ∈ S , and define Y Y Y and H Y H_Y H Y as above. By Lemma 4.1 , the query has the same scan cost in H H H as in H Y H_Y H Y . In H Y H_Y H Y , the query scans from c ( x ) c(x) c ( x ) through the last word of x x x , all of which lies in the run containing c ( x ) c(x) c ( x ) , so its scan cost is at most W x W_x W x . Thus, by Lemma 4.2 , there is a constant C C C (depending only on the load factor) such that a positive query for any item x ∈ S x \in S x ∈ S has expected scan cost at most C ∣ x ∣ C|x| C ∣ x ∣ . By Lemma 3.2 , it follows that any negative query also has expected scan cost at most 1 + C = O ( 1 ) 1 + C = O(1) 1 + C = O ( 1 ) .
Corollary 4.3 (Simple Catalan Hashing in External Memory). Under the working-set assumption of § 3 , the hash table of Theorem 1.1 is cache-oblivious and answers positive queries for any item x ∈ S x \in S x ∈ S in expected 1 + O ( ∣ x ∣ / B ) 1 + O(|x|/B) 1 + O ( ∣ x ∣/ B ) I/Os and negative queries in expected 1 + O ( 1 / B ) 1 + O(1/B) 1 + O ( 1/ B ) I/Os.
It is worth remarking on why the analysis in this section does not extend to get an O ( ε − 1 ∣ x ∣ ) O(\varepsilon^{-1}|x|) O ( ε − 1 ∣ x ∣ ) bound at high load factors 1 − ε 1-\varepsilon 1 − ε . Recall that, in a standard linear probing hash table (with unit-sized items), the expected length of the run containing a given cell is Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) . Now, consider a table in which all but one item has size 1, and one item x x x has size 2. The item x x x has lower priority than any other item, and will therefore go at the end of the run formed by elements of size 1. But this forces a scan length of Ω ( ε − 2 ∣ x ∣ ) \Omega(\varepsilon^{-2}|x|) Ω ( ε − 2 ∣ x ∣ ) , which has a quadratic dependence on the load factor. We will see in § 8 that this can be avoided by using a more sophisticated placement rule.
Write Optimization via Erasure Codes (Dynamic, Low Load)
Next we turn to the question of how to support insertions in the Catalan hash table. In this section and the next we focus on the insertion-only regime and defer deletions to § 9 and Appendix § 14 . Additionally, for this section only, we streamline our presentation by considering only positive queries (not negative ones).
Why Naive Dynamization Fails. At first glance, it might seem simple to dynamize simple Catalan hashing: whenever a new item x x x is inserted, we could simply update the table so that its layout matches the static simple Catalan table on the current set of items.
The most straightforward way to do so would be when inserting an item x x x , to insert it and evict all cells with words from lower priority items that block its placement. These evicted words are then shifted and placed, possibly triggering further evictions of even lower priority items, and so on. However, this naive dynamization incurs substantial overhead. To see why, suppose the table currently stores a long item y y y of length ∣ y ∣ = L ≫ B |y| = L \gg B ∣ y ∣ = L ≫ B in cells 1 , 2 , … , L 1,2,\ldots,L 1 , 2 , … , L . Now suppose we insert a tiny item x x x of length ∣ x ∣ = 1 |x| = 1 ∣ x ∣ = 1 with hash c ( x ) = 1 c(x) = 1 c ( x ) = 1 . Because ∣ x ∣ < ∣ y ∣ |x| < |y| ∣ x ∣ < ∣ y ∣ , x x x has higher priority than y y y and must be placed in cell 1. This forces all L L L words of y y y to shift, incurring a scan cost of L + 1 L+1 L + 1 and an I/O cost of 1 + Ω ( L ) / B 1+\Omega(L)/B 1 + Ω ( L ) / B .
One could try to improve this cost by allowing the words of y y y to be stored out of order. Then in the prior example, the first word of y y y could be evicted to the next available cell, while the other words of y y y stay in place. However, this doesn’t change the scan cost, since the evicted word of y y y has to be placed after the other L L L words. Similarly, in the external-memory model this incurs a random access, which in general adds an I/O. In the external-memory literature, this phenomenon in which a small logical update triggers a much larger physical reorganization of memory is known as write amplification . The standard way to combat write amplification in external-memory search trees (such as B ε B^\varepsilon B ε -trees [18 ] , LSM-trees [42 ] , and cache-oblivious streaming B-trees [9 ] ) is to introduce buffers at the nodes of a tree, which collect small updates until enough have accumulated to amortize the cost of moving them down a level.
In a linear-probing hash table, however, there is no tree hierarchy, and we cannot redirect insertions to an external buffer without breaking the core guarantee that queries execute in a single contiguous scan of 1 + O ( ∣ x ∣ / B ) 1 + O(|x|/B) 1 + O ( ∣ x ∣/ B ) I/Os. How, then, can we perform write optimization inside a flat, inline array?
Using Large Items as Write Buffers With Erasure Coding
Our key insight is that we can use erasure codes to turn the large items themselves into in-place write buffers for smaller items.
Specifically, we use a standard maximum distance separable (MDS) erasure code (such as a Reed-Solomon code) as a black-box primitive [49 ] . The MDS code generates, for each item x x x , 2 ∣ x ∣ 2|x| 2∣ x ∣ codewords x ~ = { x ~ 1 , … , x ~ 2 ∣ x ∣ } \tilde{x} = \{\tilde{x}_1, \ldots, \tilde{x}_{2|x|}\} x ~ = { x ~ 1 , … , x ~ 2∣ x ∣ } with the property that we can decode x x x from any ∣ x ∣ |x| ∣ x ∣ of the x ~ i \tilde{x}_i x ~ i . We then store x ~ \tilde{x} x ~ as the representation of x x x in the table, and allow items of higher priority to simply overwrite the codewords. The MDS property ensures that as long as at most ∣ x ∣ |x| ∣ x ∣ words are overwritten, queries can still decode x x x . The basic idea is that, until ∣ x ∣ |x| ∣ x ∣ words are overwritten, the item x x x does not need to be moved at all; and once ∣ x ∣ |x| ∣ x ∣ words have been overwritten, we can afford the cost of “reinflating” x x x , that is, of reinserting x x x from scratch to once again have a codeword of length 2 ∣ x ∣ 2|x| 2∣ x ∣ .
One issue that proves (surprisingly!) challenging is how to detect when more than ∣ x ∣ |x| ∣ x ∣ words have been overwritten. A natural approach would be to keep a counter (perhaps at the start of x x x ) keeping track of how many codewords have been rewritten. But if a small item overwrites a few codewords in the middle of x x x , it cannot afford to update such a counter, as that would dramatically increase the current operation’s scan cost. To separate this issue (determining when more than ∣ x ∣ |x| ∣ x ∣ words have been overwritten) from the other algorithmic issues involved, we assume in this section that we have an oracle that simply tells us when more than half of the codewords for a given element x x x have been overwritten (we will describe this oracle in more detail shortly). We then devote Section 6 to removing the oracle assumption.
Note that the erasure-coding approach fundamentally requires operating at a low load factor, since we are effectively doubling the size of every item. Moreover, the metadata scheme we use will add an additional O ( 1 ) O(1) O ( 1 ) words to each cell to enable parsers to distinguish words of different items. Additionally, in Section 6, we will further add an additional constant-factor in space overhead due to the backup-table technique that we introduce. Thus, the constructions in this section and the next are fundamentally limited to low constant load factors. We will, however, be able to get analogous results at high load factors using the additional techniques introduced in Sections 7 and 8.
Warmup: Using an Erasure Oracle
The rest of this section will be a warmup that uses an erasure oracle to sidestep the challenge of dealing with too many overwritten codewords. The erasure oracle notifies the algorithm when the number of surviving codewords of a stored item x x x drops to ∣ x ∣ |x| ∣ x ∣ and provides a copy of the original item. The algorithm incurs no cost from the oracle. Section 6 removes the oracle assumption. As noted earlier, we focus here on insertions and positive queries only (no deletions or negative queries).
Metadata. Overwriting cells in place can break the parser for the static Catalan representation (§ 4 ). A higher-priority insertion x x x can overwrite any cell of a lower-priority item y y y , including y y y ’s first (OPEN) or last (CLOSE) cell, which would orphan y y y ’s remaining MIDDLE cells. Moreover, MDS erasure decoding requires knowing which codeword each surviving cell holds.
We therefore use a different metadata scheme that encodes for each cell containing a codeword y ~ j \tilde{y}_j y ~ j of y y y :
y y y ’s item length ∣ y ∣ |y| ∣ y ∣ ,
the codeword index j ∈ { 1 , … , 2 ∣ y ∣ } j \in\{1,\ldots,2|y|\} j ∈ { 1 , … , 2∣ y ∣ } , and
a unique owner identifier, I D ( y ) \mathrm{ID}(y) ID ( y ) .
For this warmup, we take I D ( y ) \mathrm{ID}(y) ID ( y ) to simply be the number of insertions performed before y y y , which is less than m m m . Thus the metadata for each cell is O ( 1 ) O(1) O ( 1 ) words. The ID is used to distinguish the items corresponding to different cells, so we cannot simply use the canonical cell (or a truncated hash) because then collisions would make the resulting cells indistinguishable. We also change the tie-breaking mechanism for items of the same length to use the ID rather than the hash (as in § 5 ). This is because we want to be able to determine the relative priority of items directly from the metadata.
Insertions and queries use a simple parser that separates words of items using their IDs and tracks them with a stack.
Insertions and Reinflations. To perform Insert ( x ) \operatorname{Insert}(x) Insert ( x ) , first encode the item x x x into x ~ \tilde{x} x ~ . Let A ≻ x A_{\succ x} A ≻ x denote the set of cells holding codewords of items y ≻ x y \succ x y ≻ x . We say a cell is available to x x x if it is not in A ≻ x A_{\succ x} A ≻ x , that is, if it is empty or holds a codeword of x x x or of a lower-priority item. Starting at c ( x ) c(x) c ( x ) , scan forward and write x ~ 1 , … , x ~ 2 ∣ x ∣ \tilde{x}_1,\ldots,\tilde{x}_{2|x|} x ~ 1 , … , x ~ 2∣ x ∣ into the first 2 ∣ x ∣ 2|x| 2∣ x ∣ cells available to x x x . Whenever a cell containing a word of a lower-priority item y y y is overwritten, the algorithm asks the oracle if only ∣ y ∣ |y| ∣ y ∣ codewords of y ~ \tilde{y} y ~ are still present. If so, this triggers an immediate reinflation of y y y .
To perform Reinflate ( y ) \operatorname{Reinflate}(y) Reinflate ( y ) , we simply run Insert ( y ) \operatorname{Insert}(y) Insert ( y ) again using the copy of y ~ \tilde{y} y ~ obtained from the erasure oracle, keeping the same identifier I D ( y ) \mathrm{ID}(y) ID ( y ) (so y y y ’s priority does not change). Since cells holding codewords of y y y are available to y y y , we will see in Lemma 5.2 that this overwrites all remaining codewords of y y y from before the reinflation.
Queries. As noted earlier, in this section, we only describe positive queries. The function Query ( k ) \operatorname{Query}(k) Query ( k ) starts scanning at c ( k ) c(k) c ( k ) , parsing cells and collecting codewords for the items that it reads. Whenever it obtains ∣ y ∣ |y| ∣ y ∣ codewords for an item y y y , it decodes it and compares k k k with y ( k ) y(k) y ( k ) until it finds a matching item.
Analysis
In this section we prove results in the slightly more general case that H \mathcal{H} H operates as above, but may decide to perform Reinflate ( x ) \operatorname{Reinflate}(x) Reinflate ( x ) at any time when there are at most ∣ x ∣ |x| ∣ x ∣ surviving codewords (this will be helpful for extending the analysis in later sections). We will also slightly abuse notation and define the query cost of x x x to be the scan cost of reading to the cell that contained the last codeword of x x x when x x x was most recently inserted or reinflated. Using the erasure oracle guarantees that Reinflate ( x ) \operatorname{Reinflate}(x) Reinflate ( x ) is performed any time there are exactly ∣ x ∣ |x| ∣ x ∣ surviving codewords, and positive queries always terminate at or before the above cell.
Recall that a cell is said to be available to x x x if it is not in A ≻ x A_{\succ x} A ≻ x (i.e., it does not contain a codeword of an item y y y with higher priority than x x x ). Furthermore, define the priority of a cell to be the priority of the item whose word it holds, and 0 0 0 if the cell is empty. The following two lemmas state basic structural invariants in terms of these definitions.
When interpreting the following lemma, note that the algorithm never clears a cell; it only writes codewords. We will see in Lemma 5.2 that a reinflation of x x x overwrites all remaining codewords of x x x anyway, but by convention, any old codeword that a reinflation did not overwrite would simply remain in place.
Lemma 5.1. The priority of each cell never decreases over time.
Proof. Since cells are never cleared, a cell changes only when an item w w w writes to it. The cell is then available to w w w , so its priority was no higher than that of w w w . (Equality occurs when a reinflation of w w w overwrites an old codeword of w w w .)
Lemma 5.2. At a fixed point in time, if there are s s s surviving codewords of x x x in H \mathcal{H} H , then they occupy the first s s s cells available to x x x at or after c ( x ) c(x) c ( x ) . In particular, a reinflation of x x x overwrites all remaining codewords of x x x , so the table never holds stale codewords.
Proof. Consider the most recent insertion or reinflation of x x x , and let r r r be the cell where it writes x ~ 2 ∣ x ∣ \widetilde{x}_{2|x|} x 2∣ x ∣ . By construction, it writes the codewords of x x x into the first 2 ∣ x ∣ 2|x| 2∣ x ∣ cells available to x x x at or after c ( x ) c(x) c ( x ) , so afterward every cell in [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] is either in A ≻ x A_{\succ x} A ≻ x or holds a codeword of x x x .
Now consider what has happened to the cells in [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] since x x x was last inserted or reinflated. A cell containing a codeword for x x x can only be overwritten by an item to which it is available. Thus an item of lower priority than x x x cannot overwrite any of x x x ’s codewords, and an item of higher priority than x x x that overwrites a codeword of x x x moves that cell into A ≻ x A_{\succ x} A ≻ x . Once a cell enters A ≻ x A_{\succ x} A ≻ x , it stays there by Lemma 5.1. Thus, at the current time, the cells in [ c ( x ) , r ] ∖ A ≻ x [c(x),r] \setminus A_{\succ x} [ c ( x ) , r ] ∖ A ≻ x are exactly those containing surviving codewords for x x x .
Finally, we must argue that when the next reinflation occurs, all of x x x ’s surviving codewords get overwritten (no stale codewords are left behind). We have already shown, however, that x x x ’s surviving codewords from last time it was inserted or reinflated are among the first 2 ∣ x ∣ 2|x| 2∣ x ∣ cells available to x x x ; since the reinflation overwrites all of those cells, it must overwrite all of x x x ’s surviving codewords.
Next we turn to analyzing queries. We argue that we can directly bound the scan cost for a query by what the scan cost would be in a static table if every item x x x was resized to have size 2 ∣ x ∣ 2|x| 2∣ x ∣ . To formalize this, we introduce the following notation. Let S S S be the set of items present at the end of the operation sequence; since there are no deletions, every item that is ever inserted is in S S S . For an item x ∈ S x \in S x ∈ S , let Y = { y ∈ S ∣ ∣ y ∣ ≤ ∣ x ∣ } Y = \{y \in S \mid|y| \le|x|\} Y = { y ∈ S ∣ ∣ y ∣ ≤ ∣ x ∣ } , which includes x x x and every item of higher priority than x x x . Let Y ~ = { y ~ ∣ y ∈ Y } \widetilde{Y} = \{\widetilde{y} \mid y \in Y\} Y = { y ∣ y ∈ Y } , where we view y ~ \widetilde{y} y as an item of length 2 ∣ y ∣ 2|y| 2∣ y ∣ with hash h ( y ) h(y) h ( y ) . Let H Y ~ \mathcal{H}_{\widetilde{Y}} H Y be a static Catalan hash table on Y ~ \widetilde{Y} Y , and let L x ~ L_{\widetilde{x}} L x be the length of the run in H Y ~ \mathcal{H}_{\widetilde{Y}} H Y containing c ( x ) c(x) c ( x ) . Since load ( Y ~ ) ≤ 2 load ( S ) \operatorname{load}(\widetilde{Y}) \le2\operatorname{load}(S) load ( Y ) ≤ 2 load ( S ) , if load ( S ) ≤ 1 / 2 − Ω ( 1 ) \operatorname{load}(S) \le1/2 - \Omega(1) load ( S ) ≤ 1/2 − Ω ( 1 ) then Lemma 4.2 (applied to x ~ \widetilde{x} x ) gives E [ L x ~ ] = O ( ∣ x ∣ ) \mathbb{E}[L_{\widetilde{x}}] = O(|x|) E [ L x ] = O ( ∣ x ∣ ) and E [ L x ~ 2 ] = O ( ∣ x ∣ 2 ) \mathbb{E}[L_{\widetilde{x}}^2] = O(|x|^2) E [ L x 2 ] = O ( ∣ x ∣ 2 ) .
Lemma 5.3. Let x ∈ S x \in S x ∈ S . The query scan cost of x x x , and the scan cost of an insertion or reinflation of x x x (excluding the reinflations that it triggers), are at most L x ~ L_{\widetilde{x}} L x .
Proof. Consider an insertion or reinflation of x x x , and let r r r be the cell where it writes its last codeword. To prove the lemma, it suffices to show that [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] lies within the run of H Y ~ \mathcal{H}_{\widetilde{Y}} H Y containing c ( x ) c(x) c ( x ) .
At the moment the insertion or reinflation finishes, every cell of [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] is either in A ≻ x A_{\succ x} A ≻ x or holds a codeword of x x x . For each item y ≻ x y \succ x y ≻ x , let y ~ \widetilde{y} y be the item formed from the surviving codewords of y y y at that moment, with hash h ( y ) h(y) h ( y ) , and let Y ~ \widetilde{Y} Y be the set of these items together with x ~ \widetilde{x} x . By Lemma 5.2, each item of Y ~ \widetilde{Y} Y occupies the first cells at or after its canonical cell that are not occupied by items of higher priority. So the cells of A ≻ x A_{\succ x} A ≻ x , together with the cells of x x x , are exactly the occupied cells of the bottom-up placement of Y ~ \widetilde{Y} Y (as in Section 4) in the priority order ≻ \succ ≻ . In any such placement, a cell c c c is empty if and only if, for every j j j , the items hashing into [ c − j , c ] [c-j,c] [ c − j , c ] have total length at most j j j ; in particular, the set of occupied cells does not depend on the priority order. Each item of Y ‾ \overline{Y} Y is no longer than the corresponding item of Y ~ \widetilde{Y} Y and has the same hash, since every item y ≻ x y \succ x y ≻ x satisfies ∣ y ∣ ≤ ∣ x ∣ |y| \le|x| ∣ y ∣ ≤ ∣ x ∣ . So every cell occupied in the placement of Y ‾ \overline{Y} Y is also occupied in H Y ~ \mathcal{H}_{\widetilde{Y}} H Y . In particular, every cell of [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] is occupied in H Y ~ \mathcal{H}_{\widetilde{Y}} H Y , so [ c ( x ) , r ] [c(x),r] [ c ( x ) , r ] lies within the run containing c ( x ) c(x) c ( x ) .
Amortizing Reinflations
Finally, we now give a simple argument that bounds the total cost of all reinflations of a given item x x x . We also prove a second-moment bound on the number of reinflations that we will be helpful later in the paper (in the the proof of Theorem 1.2).
Lemma 5.4. Suppose load ( S ) ≤ 1 / 2 − Ω ( 1 ) \operatorname{load}(S) \le1/2 - \Omega(1) load ( S ) ≤ 1/2 − Ω ( 1 ) . For an element x x x , let R x R_x R x denote the number of times x x x is reinflated over the entire input sequence of H \mathcal{H} H and F x F_x F x be the query scan cost in H \mathcal{H} H immediately after the last reinflation of x x x .
Then
E [ R x 2 ] = O ( 1 ) and E [ R x F x ] = O ( ∣ x ∣ ) . \mathbb{E}[R_x^2] = O(1) \qquad\text{and} \qquad\mathbb{E}[R_xF_x] = O(|x|). E [ R x 2 ] = O ( 1 ) and E [ R x F x ] = O ( ∣ x ∣ ) .
Proof. Let F x ( t ) F_x(t) F x ( t ) denote the query scan cost of x x x after the t t t -th reinflation. Since ∣ x ∣ |x| ∣ x ∣ cells belonging to x x x must be overwritten between reinflations, Lemmas 5.1 and 5.2 imply F x ( t + 1 ) − F x ( t ) ≥ ∣ x ∣ F_x(t+1)-F_x(t) \ge|x| F x ( t + 1 ) − F x ( t ) ≥ ∣ x ∣ for all t ≤ R x t \le R_x t ≤ R x . Therefore R x ≤ F x / ∣ x ∣ R_x \le F_x/|x| R x ≤ F x /∣ x ∣ .
By Lemma 5.3, F x ≤ L x ~ F_x \le L_{\widetilde{x}} F x ≤ L x . Therefore, by Lemma 4.2,
E [ R x 2 ] ≤ E [ F x 2 ] / ∣ x ∣ 2 ≤ E [ L x ~ 2 ] / ∣ x ∣ 2 = O ( 1 ) , \mathbb{E}[R_x^2] \le\mathbb{E}[F_x^2]/|x|^2 \le\mathbb{E}[L_{\widetilde{x}}^2]/|x|^2 = O(1), E [ R x 2 ] ≤ E [ F x 2 ] /∣ x ∣ 2 ≤ E [ L x 2 ] /∣ x ∣ 2 = O ( 1 ) ,
and
E [ R x F x ] ≤ E [ F x 2 ] / ∣ x ∣ ≤ E [ L x ~ 2 ] / ∣ x ∣ = O ( ∣ x ∣ ) . \mathbb{E}[R_xF_x] \le\mathbb{E}[F_x^2]/|x| \le\mathbb{E}[L_{\widetilde{x}}^2]/|x| = O(|x|). E [ R x F x ] ≤ E [ F x 2 ] /∣ x ∣ ≤ E [ L x 2 ] /∣ x ∣ = O ( ∣ x ∣ ) .
:::
Since R x ⋅ F x R_x \cdot F_x R x ⋅ F x in the lemma above is a bound on the total scan cost of all reinflations of x x x , we can now complete the analysis of the data structure:
Theorem 5.5 (Dynamic Catalan Hashing in the Erasure Oracle Model). In the erasure oracle model at load factor α ≤ 1 / 2 − Ω ( 1 ) \alpha\le1/2 - \Omega(1) α ≤ 1/2 − Ω ( 1 ) , there is an insertion-only dynamic Catalan hash table in which a positive query for an item x x x has expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) , and an insertion of x x x has amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) .
Proof. By Lemma 5.3 and the bound E [ L x ~ ] = O ( ∣ x ∣ ) \mathbb{E}[L_{\widetilde{x}}] = O(|x|) E [ L x ] = O ( ∣ x ∣ ) , a positive query for x x x scans O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) cells in expectation. We now bound the expected amortized cost of reinflations. Let R x R_x R x , F x ( t ) F_x(t) F x ( t ) , and F x F_x F x be as in the preceding lemma. The algorithm performs R x R_x R x reinflations, each of which has scan cost at most F x F_x F x . Therefore the expected amortized cost of reinflations of x x x is E [ R x F x ] = O ( ∣ x ∣ ) \mathbb{E}[R_xF_x] = O(|x|) E [ R x F x ] = O ( ∣ x ∣ ) by Lemma 5.4. We charge the cost of these reinflations to the initial insertion of x x x . Ignoring the cost of reinflations that an insertion triggers, insertions have expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) by Lemma 5.3. Thus the overall amortized expected scan cost of an insertion (including the costs of reinflations) is O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) .
Removing the Oracle via Lazy Reinflation from a Backup Table
In Section 5, we constructed a dynamic Catalan hash table that used an erasure oracle to prevent items from being overwritten too much. In this section, we show how to remove the oracle. Rather than trying to simulate the oracle directly (which is difficult to do without increasing scan cost, as explained in § 6.1 ), we remove the oracle by taking a different approach.
When we insert items, we insert them both into the actual table, and into a second backup table that is interleaved with the actual table. The backup table is less efficient than the full table (incurring roughly log 1 + δ s \log^{1+\delta} s log 1 + δ s factor overhead on all operations), and is used by queries only when query cannot recover enough codewords in the main table to reconstruct the element x x x it is looking for. When this happens, we recover x x x from the backup table, and reinflate x x x . We call this a lazy reinflation , since the reinflation is only performed when necessary when a query actually fails to be able to reconstruct x x x . Critically, we show that we can charge the cost of lazy reinflations to the original insertion of x x x ; this allows us to charge the inefficient-ness of the backup table (which is inefficient for both insertions and queries) to insertions only, without affecting the amortized cost of queries.
To make the above construction work with negative queries we also introduce another interleaved table that stores fingerprints of present items. This enables queries to determine membership (whether the element queried is present) efficiently, and therefore allows negative queries to (most likely) avoid having to go into the backup table at all.
High-Level Construction: How to use the Membership and Backup Tables
We now formalize the high-level construction described above. For now, we treat the backup table as a black box, and focus on how to use it within the larger data structure.
Our starting point is the dynamic Catalan hash table H \mathcal{H} H from § 6 with the same priority order, metadata, and insertion and reinflation procedures as defined in that section. (Below we do change the unique IDs used to index items, but they function in the same way.)
We add two secondary tables interleaved evenly with H \mathcal{H} H : a backup table B \mathcal{B} B , which stores full items (that cannot be overwritten), and a membership table M \mathcal{M} M , which stores compact fingerprints for keys that are present. All three tables store items using the same hash function h h h , so that for each key k k k , c ( k ) c(k) c ( k ) refers to the three adjacent cells with one cell from each table. We abuse notation and refer to this triple of cells as c ( k ) c(k) c ( k ) .
Interleaving the three tables is key to maintaining the property that all data is stored in a shared physical array, thus avoiding a blowup in scan costs or I/Os from the addition of these auxiliary tables. Operations will scan forward starting from c ( k ) c(k) c ( k ) , parsing cells from each table, which we think of as running concurrent operations on all three tables (some of which may be no-ops).
We will assume throughout this section that the load factor is at most a small positive constant of our choice. We refer to the combined interleaved table as C ( H , M , B ) \mathcal{C}(\mathcal{H},\mathcal{M},\mathcal{B}) C ( H , M , B ) ; we will frequently write simply C \mathcal{C} C when the component tables are clear from context.
The Membership Table and Negative Queries. The membership table M \mathcal{M} M is a standard unordered fixed-length linear-probing hash table. For each item y ∈ S y \in S y ∈ S , M \mathcal{M} M stores a triple ( ϕ ( k ( y ) ) , F y , ∣ y ∣ ) (\phi(k(y)), F_y, |y|) ( ϕ ( k ( y )) , F y , ∣ y ∣ ) , where ϕ ( k ( y ) ) \phi(k(y)) ϕ ( k ( y )) is a fingerprint, and F y F_y F y is the index of the last cell occupied by a codeword of y y y at the time of its insertion or last reinflation. The fingerprints are computed with an independent hash function ϕ \phi ϕ and are C log m C \log m C log m bits in length, for a sufficiently large constant C C C .
A query for a key k k k begins by scanning M \mathcal{M} M from c ( k ) c(k) c ( k ) to the first empty cell of M \mathcal{M} M , collecting the entries whose fingerprint is ϕ ( k ) \phi(k) ϕ ( k ) . If there are none, the query returns NOT FOUND. Otherwise, it runs the positive-query procedure described below for each matching entry. Since the tables are interleaved, scanning j j j cells of M \mathcal{M} M has scan cost O ( j ) O(j) O ( j ) in C \mathcal{C} C . The stored length tells the query how many codeword symbols suffice to decode a candidate; we will also use it to support deletions in
We also redefine in this section the unique ID of each item x ∈ S x \in S x ∈ S to be the index of its position in M \mathcal{M} M . Unordered (and insertion-only) linear-probing hash tables are stable (items do not move after insertion), so the IDs do not change over time. As we will discuss shortly, this re-definition of ID is helpful to positive queries for determining which codewords in the main table H \mathcal{H} H correspond to the item x x x being queried.
The following lemma shows that negative queries are efficient in this construction.
Lemma 6.1. A negative query to C \mathcal{C} C has expected scan cost O ( 1 ) O(1) O ( 1 ) .
Proof. Let k k k be a key not in the table. Since M \mathcal{M} M holds one entry per item of S S S , it is a unit-size linear-probing table at load factor at most load ( S ) < 1 − Ω ( 1 ) \operatorname{load}(S) < 1 - \Omega(1) load ( S ) < 1 − Ω ( 1 ) . So the run of M \mathcal{M} M containing c ( k ) c(k) c ( k ) , and hence the scan of M \mathcal{M} M from c ( k ) c(k) c ( k ) to the first empty cell, has expected length O ( 1 ) O(1) O ( 1 ) by Lemma 4.2 (applied with unit-length items, with k k k added as one of them). The query returns Not Found at the end of this scan unless some item y ∈ S y \in S y ∈ S has ϕ ( k ( y ) ) = ϕ ( k ) \phi(k(y)) = \phi(k) ϕ ( k ( y )) = ϕ ( k ) . Each item collides with k k k with probability m − C m^{-C} m − C , so by a union bound a collision occurs with probability at most m 1 − C m^{1-C} m 1 − C . Every operation has scan cost at most polynomial in m m m , so for a sufficiently large constant C C C , collisions contribute O ( 1 ) O(1) O ( 1 ) to the expected scan cost. □
The Backup Table and Lazy Reinflation. The backup table B \mathcal{B} B stores a second copy of each item x ∈ S x \in S x ∈ S that cannot be overwritten, together with its ID. It will have potentially worse performance guarantees, incurring expected scan cost O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) O(|x| \cdot q(|x|)) O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) for some function q q q for both insertions and queries. However, in C \mathcal{C} C , we will only pay this cost on insertions. For now, we’ll treat B \mathcal{B} B as a black box and defer its construction to § 7.3 .
When an item x x x is inserted, we insert x ~ \tilde{x} x ~ into H \mathcal{H} H , x x x into B \mathcal{B} B , and ( ϕ ( x ) , F x , ∣ x ∣ ) (\phi(x), F_x, |x|) ( ϕ ( x ) , F x , ∣ x ∣ ) into M \mathcal{M} M . When a subsequent insertion overwrites cells of x ~ \tilde{x} x ~ , we do nothing. We then lazily reinflate during queries as follows. A query for x x x finds ϕ ( x ) \phi(x) ϕ ( x ) and F x F_x F x in M \mathcal{M} M , and computes ID ( x ) \operatorname{ID}(x) ID ( x ) . If the query finds ∣ x ∣ |x| ∣ x ∣ codewords in C \mathcal{C} C while scanning from c ( x ) c(x) c ( x ) to F x F_x F x , then it can decode x x x and return Found together with v ( x ) v(x) v ( x ) . Otherwise, this means that more than ∣ x ∣ |x| ∣ x ∣ codewords have been overwritten, so x x x requires reinflation. The query finds x x x in B \mathcal{B} B , then performs Reinflate ( x ) \operatorname{Reinflate}(x) Reinflate ( x ) in C \mathcal{C} C . Then it updates F x F_x F x in M \mathcal{M} M .
Handling False Positives in M \mathcal{M} M . For a matching membership entry at position p p p , the query only collects codewords from the primary table with owner identifier p p p . If too few survive, it searches the backup table for the item with identifier p p p . In either case, it compares the recovered key with k k k before returning the item or reinflating it; a mismatch rejects this entry. A backup search stops once it has read the complete candidate item, allowing at most two laps of the table so that a chunk crossing the starting point can be read in full. For the true candidate, the search starts at its canonical cell and has the usual positive-query scan bound. A false candidate may require a full-table scan, but this occurs only on a fingerprint collision. This happens rarely enough that it does not affect the expected scan bounds (as in the proof of Lemma 6.1 , we can even afford to scan the entire table in the event of a fingerprint collision, which happens with probability m − ( C − 1 ) m^{-(C-1)} m − ( C − 1 ) ).
Amortizing Reinflations Against Insertions
When a query for x x x triggers a reinflation, the query must retrieve x x x from the backup table B \mathcal{B} B incurring a scan cost of O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) O(|x| \cdot q(|x|)) O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) , then execute Reinflate ( x ) \operatorname{Reinflate}(x) Reinflate ( x ) in C \mathcal{C} C . At first glance, this appears to make queries as expensive as backup-table queries.
The key observation is that we can amortize the cost of these reinflations against the insertions. Then in this amortized sense, a query for x x x still incurs a scan cost of O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) while x x x ’s initial insertion pays for the backup-table queries and reinflations. The reason this works is that once x x x has been reinflated, subsequent queries for x x x will be answered from the main table C \mathcal{C} C rather than requiring the expensive reinflation. Only when sufficiently many higher priority items again over-overwritten x x x ’s codewords in C \mathcal{C} C will a query for x x x need to reinflate again.
Lemma 6.2 (Backup-Table Reduction). Let α 0 ≤ 1 / 2 − Ω ( 1 ) \alpha_0 \le1/2 - \Omega(1) α 0 ≤ 1/2 − Ω ( 1 ) be a positive constant, and suppose that load ( S ) ≤ α 0 \operatorname{load}(S) \le\alpha_0 load ( S ) ≤ α 0 , where the load factor is measured relative to the m m m cells of H \mathcal{H} H . Let B \mathcal{B} B be a dynamic hash table with m m m cells that supports insertions and positive queries (queries for keys in S S S ). For x ∈ S x \in S x ∈ S , let Λ x \Lambda_x Λ x be the random variable of the maximum scan cost of any insertion or query of x x x in B \mathcal{B} B over the entire operation sequence. Suppose E [ Λ x ] = O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) \mathbb{E}[\Lambda_x] = O(|x| \cdot q(|x|)) E [ Λ x ] = O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) and E [ ( Λ x ) 2 ] = O ( ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) 2 ) \mathbb{E}[(\Lambda_x)^2] = O((|x| \cdot q(|x|))^2) E [( Λ x ) 2 ] = O (( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) 2 ) for all x ∈ S x \in S x ∈ S .
Let C = C ( H , M , B ) \mathcal{C} = \mathcal{C}(\mathcal{H}, \mathcal{M}, \mathcal{B}) C = C ( H , M , B ) . Then
If x ∈ S x \in S x ∈ S , Query ( k ( x ) ) \operatorname{Query}(k(x)) Query ( k ( x )) in C \mathcal{C} C has amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) ;
If k ∉ S k \notin S k ∈ / S , Query ( k ) \operatorname{Query}(k) Query ( k ) in C \mathcal{C} C has expected scan cost O ( 1 ) O(1) O ( 1 ) ; and
Insert ( x ) \operatorname{Insert}(x) Insert ( x ) has amortized expected scan cost O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) O(|x| \cdot q(|x|)) O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) .
Proof. The negative-query bound is Lemma 6.1. Since load ( S ) ≤ α 0 ≤ 1 / 2 − Ω ( 1 ) \operatorname{load}(S) \le\alpha_0 \le1/2 - \Omega(1) load ( S ) ≤ α 0 ≤ 1/2 − Ω ( 1 ) , Lemma 5.3 and the bound E [ L ~ x ] = O ( ∣ x ∣ ) \mathbb{E}[\widetilde{L}_x] = O(|x|) E [ L x ] = O ( ∣ x ∣ ) from § 6 apply to H \mathcal{H} H . Therefore, the scan cost of a positive query that doesn’t trigger a reinflation is O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) .
Let R x R_x R x be the number of reinflations and F x F_x F x be the query cost of x x x in H \mathcal{H} H after the last reinflation. The total amortized cost of reinflation is at most R x ( Λ x + F x ) R_x(\Lambda_x + F_x) R x ( Λ x + F x ) . Taking expectations and applying Cauchy-Schwarz, we have
E [ R x ( Λ x + F x ) ] ≤ E [ R x 2 ] E [ Λ x 2 ] + E [ R x F x ] . (2) \mathbb{E}[R_x(\Lambda_x + F_x)] \le\sqrt{\mathbb{E}[R_x^2]\mathbb{E}[\Lambda_x^2]} + \mathbb{E}[R_xF_x]. \tag*{(2)} E [ R x ( Λ x + F x )] ≤ E [ R x 2 ] E [ Λ x 2 ] + E [ R x F x ] . (2) H \mathcal{H} H satisfies the hypotheses of Lemma 5.4, so E [ R x 2 ] = O ( 1 ) \mathbb{E}[R_x^2] = O(1) E [ R x 2 ] = O ( 1 ) and E [ R x F x ] = O ( ∣ x ∣ ) \mathbb{E}[R_xF_x] = O(|x|) E [ R x F x ] = O ( ∣ x ∣ ) . Therefore also using the hypothesis on Λ x \Lambda_x Λ x , the RHS of Equation (2) is O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) O(|x| \cdot q(|x|)) O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) . We charge this cost to the insertion of x x x , which itself has expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) in H \mathcal{H} H , O ( 1 ) O(1) O ( 1 ) in M \mathcal{M} M , and E [ Λ x ] = O ( ∣ x ∣ ⋅ q ( ∣ x ∣ ) ) \mathbb{E}[\Lambda_x] = O(|x| \cdot q(|x|)) E [ Λ x ] = O ( ∣ x ∣ ⋅ q ( ∣ x ∣ )) in B \mathcal{B} B .
Constructing the Density-Regulated Backup Table
With Lemma 6.2 in place, the only remaining piece of Theorem 1.2 is the backup table itself. The goal of this subsection is to construct a backup table B \mathcal{B} B , which we call the density-regulated backup table , that satisfies the hypotheses of Lemma 6.2 with q ( s ) = log 1 + δ s q(s) = \log^{1+\delta} s q ( s ) = log 1 + δ s . That is, B \mathcal{B} B needs to support only insertions and positive queries, and for each item x x x of length s s s , the insertion of x x x and every query for x x x should incur scan cost roughly O ( s log 1 + δ s ) O(s\log^{1+\delta}s) O ( s log 1 + δ s ) , in the sense that the maximum Λ x \Lambda_x Λ x of these scan costs satisfies E [ Λ x 2 ] = O ( ( s log 1 + δ s ) 2 ) \mathbb{E}[\Lambda_x^2] = O((s\log^{1+\delta}s)^2) E [ Λ x 2 ] = O (( s log 1 + δ s ) 2 ) . (Throughout this subsection, log s \log s log s denotes max { 1 , log 2 s } \max\{1,\log_2 s\} max { 1 , log 2 s } .)
What makes this task easier than the one we faced in the primary table is that B \mathcal{B} B is allowed to lose a log 1 + δ s \log^{1+\delta}s log 1 + δ s factor on both insertions and queries. This, it turns out, allows for a completely different (and somewhat simpler) construction than those in previous sections, one in which items never overwrite or move one another.
The Basic Idea. The basic idea is to separate items by size, and to divide B \mathcal{B} B among the resulting size classes in proportion to their volumes. We partition the items into size classes C i = { x ∣ 2 i ≤ ∣ x ∣ < 2 i + 1 } C_i = \{x \mid2^i \le|x| < 2^{i+1}\} C i = { x ∣ 2 i ≤ ∣ x ∣ < 2 i + 1 } , and we give each class a few strided regions of B \mathcal{B} B , which we call subarrays : a subarray of density 2 − k 2^{-k} 2 − k consists of every 2 k 2^k 2 k -th cell of B \mathcal{B} B . When a class C i C_i C i receives its first item, it is given a subarray of density roughly 1 / ( i + 1 ) 1 + δ 1/(i+1)^{1+\delta} 1/ ( i + 1 ) 1 + δ , and whenever the class fills half of its newest subarray, it is given a new subarray of twice the density. Since ∑ i 1 / ( i + 1 ) 1 + δ \sum_i 1/(i+1)^{1+\delta} ∑ i 1/ ( i + 1 ) 1 + δ converges, the first subarrays of all of the classes together take up only a small constant fraction of B \mathcal{B} B . Since additional subarrays are allocated on a per-demand basis (only when a size class has already received a large volume of elements), we will be able to ensure that the total density of all subarrays ever allocated is less than 1. This will be the critical point that makes the algorithm possible.
Within each subarray, a class stores its items using standard unit-size linear probing. Since the items of C i C_i C i have the same length up to a factor of 2, we can divide the subarray into fixed-size chunks of 2 i + 2 2^{i+2} 2 i + 2 consecutive subarray cells, each of which holds at most one item. Note that, even though the item is stored contiguously within the subarray , it is not stored physically contiguously: a chunk in a subarray of density 1 / D 1/D 1/ D is physically spread across Θ ( D 2 i ) \Theta(D2^i) Θ ( D 2 i ) cells, which inflates the scan cost by a factor of Θ ( D ) \Theta(D) Θ ( D ) . This is the main thing we need to be careful about when bounding scan cost.
For the first subarray of class C i C_i C i , the stride (i.e., the physical distance between consecutive cells of the subarray) is roughly ( i + 1 ) 1 + δ = O ( log 1 + δ s ) (i+1)^{1+\delta}=O(\log^{1+\delta}s) ( i + 1 ) 1 + δ = O ( log 1 + δ s ) , and the later subarrays of the class are only denser, so this ends up being the source of the log 1 + δ s \log^{1+\delta}s log 1 + δ s factor when analyzing the scan costs of both insertions and queries.
In the rest of the subsection, we fill in the details for the data structure described above.
Allocating Subarrays. We begin by defining the strided subarrays in which classes store their items, and by defining the simple allocator that we use to hand these strided subarrays out to different classes.
Let m B m_B m B denote the number of cells of B \mathcal{B} B ; with the interleaving above, m B = m m_B=m m B = m . Throughout this subsection, we assume that Vol ( S ) ≤ m B / 64 \operatorname{Vol}(S)\le m_B/64 Vol ( S ) ≤ m B /64 , that is, load ( S ) ≤ 1 / 64 \operatorname{load}(S)\le1/64 load ( S ) ≤ 1/64 .
For 0 ≤ k ≤ log m B 0\le k\le\log m_B 0 ≤ k ≤ log m B and 0 ≤ a < 2 k 0\le a<2^k 0 ≤ a < 2 k , we define subarray B k , a B_{k,a} B k , a to consist of the m B / 2 k m_B/2^k m B / 2 k cells a a a , a + 2 k a+2^k a + 2 k , a + 2 ⋅ 2 k a+2\cdot2^k a + 2 ⋅ 2 k , … \ldots … of B \mathcal{B} B , in this order. Its density is 2 − k 2^{-k} 2 − k . The subarrays form a complete binary tree: the two children B k + 1 , a B_{k+1,a} B k + 1 , a and B k + 1 , a + 2 k B_{k+1,a+2^k} B k + 1 , a + 2 k of B k , a B_{k,a} B k , a partition it, so two subarrays are either disjoint or nested.
We allocate subarrays with the following simple allocator (essentially a buddy allocator [34 , 36 ] ). To allocate a subarray of density 2 − k 2^{-k} 2 − k , let j ≤ k j\le k j ≤ k be the largest depth at which some free subarray exists, and let Q Q Q be any free subarray at depth j j j . We allocate one of the depth-k k k subarrays contained in Q Q Q , and the rest of Q Q Q , which consists of one subarray at each depth j + 1 , … , k j+1,\ldots,k j + 1 , … , k , remains free. The next lemma shows that this allocator succeeds so long as the total allocated density stays at most 1.
Lemma 6.3. An allocation of density 2 − k 2^{-k} 2 − k succeeds whenever the total density of all allocated subarrays, including the new one, is at most 1.
Proof. The key invariant is that, at any moment, there is at most one free subarray at each depth. To see that allocations preserve this, consider an allocation of density 2 − k 2^{-k} 2 − k that splits a free subarray at depth j j j . The allocation creates one new free subarray at each depth j + 1 , … , k j+1,\ldots,k j + 1 , … , k , and there were no free subarrays at these depths beforehand, since otherwise the allocator would have chosen a larger j j j .
Now suppose that an allocation of density 2 − k 2^{-k} 2 − k fails, meaning that no free subarray has depth at most k k k . By the invariant, there is at most one free subarray at each depth d > k d>k d > k , so the free subarrays have total density less than ∑ d > k 2 − d = 2 − k \sum_{d>k}2^{-d}=2^{-k} ∑ d > k 2 − d = 2 − k . Since the free subarrays cover all of the unallocated cells, the allocated density is more than 1 − 2 − k 1-2^{-k} 1 − 2 − k , so the allocated density plus 2 − k 2^{-k} 2 − k exceeds 1. □
Levels and Chunks. We now describe how each class C i C_i C i stores its items. The class is stored in a sequence of subarrays, which we call levels , whose densities double from each level to the next. Within each level, the class is simply a unit-size linear-probing table whose entries are chunks of 2 i + 2 2^{i+2} 2 i + 2 cells of B \mathcal{B} B . Within a given size class C i C_i C i , new items are only ever inserted into the newest level, which is never more than half full. (The doubling of densities is not needed to bound scan costs; it just keeps the number of levels of each class at O ( log m B ) O(\log m_{\mathcal{B}}) O ( log m B ) .)
Fix a constant c 0 > 0 c_0 > 0 c 0 > 0 such that ∑ i ≥ 0 c 0 / ( i + 1 ) 1 + δ ≤ 1 / 8 \sum_{i\ge0} c_0/(i+1)^{1+\delta} \le1/8 ∑ i ≥ 0 c 0 / ( i + 1 ) 1 + δ ≤ 1/8 , and let d i d_i d i be the largest power of two that is at most c 0 / ( i + 1 ) 1 + δ c_0/(i+1)^{1+\delta} c 0 / ( i + 1 ) 1 + δ . Level ℓ \ell ℓ of class C i C_i C i is a subarray of density D i , ℓ = 2 ℓ D i , 0 D_{i,\ell}=2^\ell D_{i,0} D i , ℓ = 2 ℓ D i , 0 , where D i , 0 = max { d i , 2 i + 3 / m B } D_{i,0}=\max\{d_i,2^{i+3}/m_{\mathcal{B}}\} D i , 0 = max { d i , 2 i + 3 / m B } (the second term just ensures that every level has at least two chunks). It is divided into n i , ℓ = D i , ℓ m B / 2 i + 2 n_{i,\ell}=D_{i,\ell}m_{\mathcal{B}}/2^{i+2} n i , ℓ = D i , ℓ m B / 2 i + 2 chunks of 2 i + 2 2^{i+2} 2 i + 2 consecutive cells, and an item stored in a chunk occupies a prefix of it. This prefix starts with a two-word header containing the item’s identifier and length. The header and item fit in the chunk, since ∣ x ∣ + 2 ≤ 2 i + 1 + 1 ≤ 2 i + 2 |x|+2\le2^{i+1}+1\le2^{i+2} ∣ x ∣ + 2 ≤ 2 i + 1 + 1 ≤ 2 i + 2 for x ∈ C i x\in C_i x ∈ C i . Queries can therefore recognize items by identifier as well as by key, without changing their placement or scan bounds.
Let γ ( x ) \gamma(x) γ ( x ) be the cell of B \mathcal{B} B that x x x hashes to, namely the cell of B \mathcal{B} B in c ( x ) c(x) c ( x ) , which is uniformly random and independent across items. In each level, the home chunk of x x x is the first chunk (cyclically) whose first cell is at or after γ ( x ) \gamma(x) γ ( x ) . Since the first cells of the chunks are evenly spaced, home chunks are uniformly random and independent across items, and x x x ’s home chunk in level ℓ \ell ℓ begins fewer than 2 i + 2 / D i , ℓ 2^{i+2}/D_{i,\ell} 2 i + 2 / D i , ℓ cells after γ ( x ) \gamma(x) γ ( x ) .
With these definitions in place, the operations are implemented as follows.
Insert ( x ) (x) ( x ) , for x ∈ C i x\in C_i x ∈ C i : if C i C_i C i has no levels, or its newest level L L L already holds n i , L / 2 n_{i,L}/2 n i , L /2 items, allocate a new level. Then write x x x into the first empty chunk of the newest level at or after x x x ’s home chunk.
Query ( x ) (x) ( x ) , for x ∈ S x\in S x ∈ S : scan forward from γ ( x ) \gamma(x) γ ( x ) , parsing the items in the chunks that the scan passes through, until finding x x x .
The bookkeeping for the allocator and the levels takes O ( log 2 m B ) O(\log^2 m_{\mathcal{B}}) O ( log 2 m B ) words of working space, as used in the I/O analysis below.
Analysis. It remains to show that the allocator never runs out of space, and to bound the scan costs of insertions and queries. We begin with space. The key observation is that, beyond level 0, the levels of a class are paid for by the volume of the class: a class only receives level L ≥ 1 L\ge1 L ≥ 1 once level L − 1 L-1 L − 1 is half full, at which point the volume of the class is proportional to the volume of the level being allocated.
Lemma 6.4 (Density Never Exceeds 7/8). At all times, the total density of the allocated subarrays is at most 7 / 8 7/8 7/8 . Hence, by Lemma 6.3, every allocation succeeds.
Proof. We bound the density used by the classes in two groups, depending on whether or not a class has advanced beyond level 0.
First consider the classes whose newest level is level 0. Such a class C i C_i C i uses density D i , 0 ≤ d i + 2 i + 3 / m B D_{i,0}\le d_i+2^{i+3}/m_{\mathcal{B}} D i , 0 ≤ d i + 2 i + 3 / m B . Since every item has ∣ x ∣ ≤ Vol ( S ) ≤ m B / 64 |x|\le\operatorname{Vol}(S)\le m_{\mathcal{B}}/64 ∣ x ∣ ≤ Vol ( S ) ≤ m B /64 , every class satisfies 2 i ≤ m B / 64 2^i\le m_{\mathcal{B}}/64 2 i ≤ m B /64 . Summing over all such classes, the total density is therefore at most
∑ i d i + ∑ i : 2 i ≤ m B / 64 2 i + 3 m B ≤ 1 8 + 16 ⋅ m B / 64 m B = 3 8 . \sum_i d_i+\sum_{i:2^i\le m_{\mathcal{B}}/64}\frac{2^{i+3}}{m_{\mathcal{B}}}\le\frac18+\frac{16\cdot m_{\mathcal{B}}/64}{m_{\mathcal{B}}}=\frac38. i ∑ d i + i : 2 i ≤ m B /64 ∑ m B 2 i + 3 ≤ 8 1 + m B 16 ⋅ m B /64 = 8 3 .
Next consider a class C i C_i C i whose newest level is L ≥ 1 L\ge1 L ≥ 1 . At the moment when level L L L was allocated, level L − 1 L-1 L − 1 held n i , L − 1 / 2 n_{i,L-1}/2 n i , L − 1 /2 items, each of length at least 2 i 2^i 2 i . Thus the class has volume
Vol ( C i ) ≥ n i , L − 1 2 i − 1 = D i , L − 1 m B / 8 = D i , L m B / 16. (3) \operatorname{Vol}(C_i)\ge n_{i,L-1}2^{i-1}=D_{i,L-1}m_{\mathcal{B}}/8=D_{i,L}m_{\mathcal{B}}/16. \tag*{(3)} Vol ( C i ) ≥ n i , L − 1 2 i − 1 = D i , L − 1 m B /8 = D i , L m B /16. (3)
On the other hand, since the densities of the levels double from each level to the next, the total density of the levels of C i C_i C i is ∑ ℓ ≤ L 2 ℓ D i , 0 < 2 D i , L \sum_{\ell\le L} 2^{\ell}D_{i,0}<2D_{i,L} ∑ ℓ ≤ L 2 ℓ D i , 0 < 2 D i , L , which by the above is at most 32 Vol ( C i ) / m B 32\operatorname{Vol}(C_i)/m_B 32 Vol ( C i ) / m B . Summing over all such classes, the total density is at most 32 Vol ( S ) / m B ≤ 32 ( m B / 64 ) / m B = 1 / 2 32\operatorname{Vol}(S)/m_B \le32(m_B/64)/m_B = 1/2 32 Vol ( S ) / m B ≤ 32 ( m B /64 ) / m B = 1/2 . Adding together the two groups, the total density is at most 3 / 8 + 1 / 2 = 7 / 8 3/8+1/2=7/8 3/8 + 1/2 = 7/8 .
Finally, we bound the scan costs. A key observation is that querying x x x costs no more than inserting x x x did: x x x never moves, and a query for x x x scans forward from γ ( x ) \gamma(x) γ ( x ) only until it reaches x x x , which is exactly where the insertion of x x x stopped. So it suffices to bound insertions. An insertion scans its level, which is a unit-size linear-probing table with load factor at most 1 / 2 1/2 1/2 , from x x x ’s home chunk to the first empty chunk. This is O ( 1 ) O(1) O ( 1 ) chunks in expectation, each spanning at most 2 i + 2 / d i 2^{i+2}/d_i 2 i + 2 / d i cells of B \mathcal{B} B , for a total of O ( 2 i / d i ) = O ( s log 1 + δ s ) O(2^i/d_i)=O(s\log^{1+\delta}s) O ( 2 i / d i ) = O ( s log 1 + δ s ) .
Lemma 6.5 (Performance of the Density-Regulated Backup Table). Let x ∈ C i x\in C_i x ∈ C i have length s = ∣ x ∣ s=|x| s = ∣ x ∣ . There is a random variable Λ x \Lambda_x Λ x with E [ Λ x 2 ] = O ( ( s log 1 + δ s ) 2 ) \mathbb{E}[\Lambda_x^2]=O((s\log^{1+\delta}s)^2) E [ Λ x 2 ] = O (( s log 1 + δ s ) 2 ) such that the insertion of x x x , and every query for x x x over the entire operation sequence, scans a contiguous interval of B \mathcal{B} B starting at γ ( x ) \gamma(x) γ ( x ) of length at most Λ x \Lambda_x Λ x . In particular, B \mathcal{B} B satisfies the hypotheses of Lemma 6.2 with q ( s ) = log 1 + δ s q(s)=\log^{1+\delta}s q ( s ) = log 1 + δ s and α 0 = 1 / 64 \alpha_0=1/64 α 0 = 1/64 .
Proof. Let ℓ \ell ℓ be the level into which x x x is inserted, and let J ℓ J_\ell J ℓ be the number of chunks from x x x ’s home chunk through the first empty chunk of level ℓ \ell ℓ at the time of the insertion; x x x is written into that empty chunk. The home chunk begins fewer than 2 i + 2 / D i , ℓ 2^{i+2}/D_{i,\ell} 2 i + 2 / D i , ℓ cells after γ ( x ) \gamma(x) γ ( x ) , and each chunk spans at most 2 i + 2 / D i , ℓ 2^{i+2}/D_{i,\ell} 2 i + 2 / D i , ℓ cells of B \mathcal{B} B , so the insertion of x x x scans at most
Λ x = ( J ℓ + 1 ) 2 i + 2 D i , ℓ ≤ ( J ℓ + 1 ) 2 i + 2 d i \Lambda_x=(J_\ell+1)\frac{2^{i+2}}{D_{i,\ell}}\le(J_\ell+1)\frac{2^{i+2}}{d_i} Λ x = ( J ℓ + 1 ) D i , ℓ 2 i + 2 ≤ ( J ℓ + 1 ) d i 2 i + 2
cells starting at γ ( x ) \gamma(x) γ ( x ) . Since x x x is never moved, and a query for x x x stops once it has read x x x , every query for x x x also scans at most Λ x \Lambda_x Λ x cells.
It remains to show that E [ J ℓ 2 ] = O ( 1 ) \mathbb{E}[J_\ell^2]=O(1) E [ J ℓ 2 ] = O ( 1 ) . View level ℓ \ell ℓ as a unit-size linear-probing table whose cells are its chunks, in which each item hashes to its home chunk. Which items are placed in level ℓ \ell ℓ (including whether x x x is) depends only on the operation sequence, and not on the hashes, since the level that receives an insertion is determined by the number of earlier insertions into C i C_i C i . The level holds fewer than n i , ℓ / 2 n_{i,\ell}/2 n i , ℓ /2 items before x x x is inserted, so its load factor is at most 1 / 2 1/2 1/2 . If R R R is the length of the run of occupied chunks containing x x x ’s home chunk at the time of the insertion (with R = 0 R=0 R = 0 if that chunk is empty), then J ℓ ≤ 1 + R J_\ell\le1+R J ℓ ≤ 1 + R . Applying Lemma 4.2 with all items having length 1 gives E [ R 2 ] = O ( 1 ) \mathbb{E}[R^2]=O(1) E [ R 2 ] = O ( 1 ) . (The proof of that lemma uses only two properties, both of which hold here: that every word of a run belongs to an item whose canonical cell lies in the run, and that the items other than x x x have independent hashes.) Hence E [ J ℓ 2 ] = O ( 1 ) \mathbb{E}[J_\ell^2]=O(1) E [ J ℓ 2 ] = O ( 1 ) , and so E [ Λ x 2 ] = O ( ( 2 i / d i ) 2 ) \mathbb{E}[\Lambda_x^2]=O((2^i/d_i)^2) E [ Λ x 2 ] = O (( 2 i / d i ) 2 ) .
Finally, 2 i ≤ s 2^i\le s 2 i ≤ s and 1 / d i ≤ 2 ( i + 1 ) 1 + δ / c 0 ≤ 2 ( 2 log s ) 1 + δ / c 0 1/d_i\le2(i+1)^{1+\delta}/c_0\le2(2\log s)^{1+\delta}/c_0 1/ d i ≤ 2 ( i + 1 ) 1 + δ / c 0 ≤ 2 ( 2 log s ) 1 + δ / c 0 , so 2 i / d i = O ( s log 1 + δ s ) 2^i/d_i=O(s\log^{1+\delta}s) 2 i / d i = O ( s log 1 + δ s ) . This establishes the bound on E [ Λ x ] \mathbb{E}[\Lambda_x] E [ Λ x ] , and by Jensen’s inequality, E [ Λ x ] ≤ E [ Λ x 2 ] 1 / 2 = O ( s log 1 + δ s ) \mathbb{E}[\Lambda_x]\le\mathbb{E}[\Lambda_x^2]^{1/2}=O(s\log^{1+\delta}s) E [ Λ x ] ≤ E [ Λ x 2 ] 1/2 = O ( s log 1 + δ s ) as well. Thus the hypotheses of Lemma 6.2 hold.
Finally, plugging Lemma 6.5 into Lemma 6.2 proves Theorem 1.2, which we restate here for convenience:
Theorem 1.2 (Dynamic Catalan Hashing at Low Load Factor). Fix any constant δ > 0 \delta>0 δ > 0 . There is an insertion-only dynamic Catalan hash table that supports some constant load factor Ω ( 1 ) \Omega(1) Ω ( 1 ) , and that answers positive queries for any item x x x with amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) , answers negative queries with expected scan cost O ( 1 ) O(1) O ( 1 ) , and supports insertions of any item x x x with amortized expected scan cost O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) .
Proof. Choose m m m so that load ( S ) ≤ 1 / 64 \operatorname{load}(S) \le1/64 load ( S ) ≤ 1/64 , and apply Lemma 6.2 with α 0 = 1 / 64 \alpha_0 = 1/64 α 0 = 1/64 and B B B instantiated as the density-regulated backup table on m B = m m_B = m m B = m cells. Then Vol ( S ) ≤ m B / 64 \operatorname{Vol}(S) \le m_B/64 Vol ( S ) ≤ m B /64 , so by Lemma 6.5, the hypotheses of Lemma 6.2 hold with q ( s ) = log 1 + δ s q(s) = \log^{1+\delta} s q ( s ) = log 1 + δ s , which gives the claimed bounds. The three interleaved tables use 3 m 3m 3 m cells in total, so the load factor is a positive constant.
Corollary 6.6 (Dynamic Low-Load Catalan Hashing in External Memory). Suppose M ≥ c M ( s max + B + log 2 m ) M \ge c_M(s_{\max} + B + \log^2 m) M ≥ c M ( s m a x + B + log 2 m ) for a sufficiently large constant c M c_M c M . The hash table of Theorem 1.2 is cache-oblivious and answers positive queries for any item x x x in amortized expected 1 + O ( ∣ x ∣ / B ) 1 + O(|x|/B) 1 + O ( ∣ x ∣/ B ) I/Os, answers negative queries in expected 1 + O ( 1 / B ) 1 + O(1/B) 1 + O ( 1/ B ) I/Os, and supports insertions of any item x x x in amortized expected
1 + O ( ∣ x ∣ log 1 + δ ∣ x ∣ B ) 1 + O\left(\frac{|x|\log^{1+\delta}|x|}{B}\right) 1 + O ( B ∣ x ∣ log 1 + δ ∣ x ∣ ) I/Os.
Proof. We use the working-set assumption of § 3 , together with the stated cache-size bound. The additional working space is O ( s max + log 2 m ) O(s_{\max} + \log^2 m) O ( s m a x + log 2 m ) words: we collect the codeword symbols read during a scan to encode or decode the item there, and retain the allocator’s O ( log 2 m ) O(\log^2 m) O ( log 2 m ) words of global bookkeeping. Thus encoding and decoding incur no I/Os beyond those of the scan. Each operation makes O ( 1 ) O(1) O ( 1 ) forward passes over its scan interval, so the scan property converts the scan bounds of Theorem 1.2 into the stated I/O bounds, with the same amortization. The algorithms do not use M M M or B B B .
Fair Catalan Hashing (Static, High Load)
Up to this point, we have focused on the low-load-factor regime, where the load factor α = load ( S ) \alpha= \operatorname{load}(S) α = load ( S ) is 1 − Ω ( 1 ) 1 - \Omega(1) 1 − Ω ( 1 ) . We now turn to the high-load-factor regime , where the table is filled to load factor
α = 1 − ε \alpha= 1 - \varepsilon α = 1 − ε
for a small parameter ε ∈ ( 0 , 1 ) \varepsilon\in(0, 1) ε ∈ ( 0 , 1 ) , and prove Theorem 1.3.
In classical unit-size linear probing, standard (unordered) placement suffers from primary clustering (where runs of items merge together), resulting in an expected query cost of Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) [37 ] . By contrast, ordered linear probing [3 ] (more popularly known as Robin Hood hashing [19 ] ), which gives priority to items with smaller hash values, reduces the worst-case expected query cost for any item to Θ ( ε − 1 ) \Theta(\varepsilon^{-1}) Θ ( ε − 1 ) .
When items have variable lengths, however, simple Catalan hashing gets stuck at Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) . To see this, consider an m m m -cell table containing ( 1 − ε ) m − 2 (1 - \varepsilon)m - 2 ( 1 − ε ) m − 2 items of length 1 and a single item x x x of length 2. Because simple Catalan hashing prioritizes shorter items over longer ones, every unit-length item has higher priority than x x x . Consequently, when x x x is placed, it must skip over all unit-length items in its run and place its words into the first two empty cells left behind by the unit-length items. By the standard analysis of unit-length linear probing at load factor ( 1 − ε ) (1 - \varepsilon) ( 1 − ε ) [37 ] , the expected distance from a random cell c ( x ) c(x) c ( x ) to the first empty cell is Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) , so querying x x x requires scanning Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) cells in expectation.
At the same time, we cannot simply switch to Robin Hood priority (giving priority to items with smaller hash values), because in the variable-length setting, a single long item y y y of length L L L would have higher priority than all items hashing after h ( y ) h(y) h ( y ) , forcing short items of length 1 to scan past all L L L words of y y y . Thus, neither prioritizing by length (simple Catalan hashing) nor prioritizing by hash value (Robin Hood hashing) can achieve a linear O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) dependence on the inverse slack.
Hash-Table Queues: Variable-Length Linear Probing as Preemptive Queuing
The reason that simple Catalan hashing and Robin Hood hashing don’t achieve the query bounds we would like in the high-load case is that they are unfair : some jobs unfairly take priority over others. Robin Hood hashing gives too much priority to large jobs, so small jobs have high expected query cost. On the other hand, simple Catalan hashing gives too much priority to small jobs, so large jobs have high expected query cost. This is analogous to fairness problems in queuing theory, where a long job can block short jobs, while strict priority for short jobs can penalize long ones. We show that there is a surprisingly direct connection from variable-length linear probing to fair queuing and develop fair Catalan hash tables based on fair queuing policies.
We can reinterpret variable-length linear probing as a preemptive single-server scheduling problem , where we view the hash table [ 0 , m ) ⊂ R [0,m) \subset\mathbb{R} [ 0 , m ) ⊂ R as a continuous time axis. We define the following:
Jobs: Each job is an item x ∈ S x \in S x ∈ S .
Arrival times: A job x x x ’s arrival time A x A_x A x is h ( x ) h(x) h ( x ) .
Service requirements: A job x x x ’s service requirement (or size) is ∣ x ∣ |x| ∣ x ∣ .
Schedule: A job x x x ’s schedule is its placement in the table. The words of x x x correspond to units of time x x x is serviced (with preemption allowed).
Departure times: A job x x x ’s departure time D x D_x D x is the position of its last cell.
Sojourn times: A job x x x ’s sojourn time (or response time) is the query scan cost L x L_x L x . Its stretch (or slowdown) is L x / ∣ x ∣ L_x/|x| L x /∣ x ∣ .
We call the resulting queuing model the hash-table queue . In this model, the set of jobs and their sizes are fixed, and the arrival times are independent and uniform on a circle of circumference m m m . Service in a hash-table queue is interpreted cyclically: since the total service requirement is α m < m \alpha m < m α m < m , the schedule has idle time at which we can unwrap the circle.
For comparison, the classical M / G / 1 M/G/1 M / G /1 queue has a Poisson stream of arrivals and independent, identically distributed service requirements, independent of the arrival process [32 , 28 ] . The notation refers to memoryless (exponentially distributed) interarrival times (M M M ), a general distribution of service requirements (G G G ), and a single server (1). The server supplies one unit of service per unit of time, and a scheduling policy specifies how that service is allocated among the jobs present. The load ρ \rho ρ is the arrival rate times the mean service requirement; the stable regime ρ < 1 \rho< 1 ρ < 1 is analogous to α < 1 \alpha< 1 α < 1 for hash-table queues.
Interpreting linear probing as a hash-table queue, different linear-probing priority rules correspond directly to different scheduling policies:
Simple Catalan hashing is Preemptive Static-Size Priority: Jobs with smaller original size ∣ x ∣ |x| ∣ x ∣ have strict preemptive priority over jobs with larger original size. This is unfair to large jobs, which get starved until the very end of the busy period (run), incurring Θ ( ε − 2 ) \Theta(\varepsilon^{-2}) Θ ( ε − 2 ) stretch.
What we want is a scheduling policy that is fair across all job sizes—guaranteeing expected stretch O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) for every item x x x , regardless of ∣ x ∣ |x| ∣ x ∣ —while still producing a table that satisfies the Catalan nesting property (Definition 3.1 ).
For this we look to fair queues. We first consider a policy that gives the desired bound for each item’s expected stretch, and then a policy based on processor sharing that also bounds the expected maximum stretch over the table.
Anti-Robin-Hood Catalan Hashing (ARCH)
In queuing theory, the simplest policy whose expected slowdown is O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) independent of job size is Last-In-First-Out with Preemption (LIFO-PR) , where newly arriving jobs immediately preempt whatever job is currently in service. In our hash-table setting, LIFO-PR corresponds to what we call anti-Robin-Hood Catalan hashing (ARCH) , so named because its priority order is the exact opposite of that of Robin Hood hashing. Formally, the ARCH hash table is the static Catalan hash table of Definition 3.4 in which an item x x x has priority over an item y y y if h ( x ) > h ( y ) h(x)>h(y) h ( x ) > h ( y ) , where hashes are compared cyclically.
We remark that, in the context of Catalan hashing, it may seem counterintuitive that the anti-Robin-Hood order is in some sense “fairer” than the Robin Hood order. After all, Robin Hood hashing is named for “giving back to the poor”: it prioritizes items that are already farther from their hashes. One might expect ARCH to give a small item x x x very large expected stretch, since a very large item y y y with h ( y ) > h ( x ) h(y)>h(x) h ( y ) > h ( x ) can preempt x x x and blow up its stretch. However, the smaller x x x is, the less likely it is that any other item y y y hashes into the region where it would preempt x x x . As we will see in the proof of Theorem 1.3, this is why ARCH achieves worst-case expected stretch ε − 1 \varepsilon^{-1} ε − 1 .
Analysis. We now give a simple, self-contained proof that the ARCH table achieves worst-case expected stretch O ( ε − 1 ) O(\varepsilon^{-1}) O ( ε − 1 ) . The result mirrors the standard results in queuing theory for LIFO-PR schedules on M/G/1 queues [32 , 56 ] . We remark, however, that the proof itself is necessarily different, because the standard approaches rely heavily on the Poisson arrivals of the M/G/1 model in order to perform an optional-stopping-time argument; to get the same result in the hash-table setting, we use an alternative approach in which we reason about x x x ’s query cost by considering the impact for each item y y y of deleting and reinserting y y y into the table.
Theorem 1.3 (Anti-Robin-Hood Catalan Hashing). Let ε ∈ ( 0 , 1 ) \varepsilon\in(0,1) ε ∈ ( 0 , 1 ) , and let S S S be a static set of items stored in an m m m -cell table with load factor load ( S ) ≤ 1 − ε \operatorname{load}(S) \le1-\varepsilon load ( S ) ≤ 1 − ε . Anti-Robin-Hood Catalan hashing (ARCH) uses O ( 1 ) O(1) O ( 1 ) bits of metadata per cell, answers positive queries for any item x ∈ S x \in S x ∈ S with expected scan cost at most ∣ x ∣ / ε |x|/\varepsilon ∣ x ∣/ ε , and answers negative queries with expected scan cost at most 1 + 1 / ε 1+1/\varepsilon 1 + 1/ ε .
Proof. Fix x ∈ S x \in S x ∈ S . For T ⊆ S T \subseteq S T ⊆ S containing x x x , let L x ( T ) L_x(T) L x ( T ) denote the scan cost of x x x in the ARCH table on T T T , let e T ( x ) e_T(x) e T ( x ) be the last cell of x x x in that table, and let J x ( T ) = ( h ( x ) , e T ( x ) + 1 ) J_x(T)=(h(x),e_T(x)+1) J x ( T ) = ( h ( x ) , e T ( x ) + 1 ) . We say that an item y ∈ T y \in T y ∈ T preempts x x x in T T T if h ( y ) ∈ J x ( T ) h(y) \in J_x(T) h ( y ) ∈ J x ( T ) . Then L x ( T ) L_x(T) L x ( T ) equals ∣ x ∣ |x| ∣ x ∣ plus the total length of the other items that preempt x x x in T T T .
We prove the positive-query bound by induction on ∣ S ∣ |S| ∣ S ∣ . If S = { x } S=\{x\} S = { x } , then L x ( { x } ) = ∣ x ∣ L_x(\{x\})=|x| L x ({ x }) = ∣ x ∣ . In general, for y ≠ x y \ne x y = x , consider removing y y y and then reinserting it. Then y y y preempts x x x in S S S exactly when h ( y ) ∈ J x ( S ∖ { y } ) h(y) \in J_x(S\setminus\{y\}) h ( y ) ∈ J x ( S ∖ { y }) . The interval J x ( S ∖ { y } ) J_x(S\setminus\{y\}) J x ( S ∖ { y }) has length at most L x ( S ∖ { y } ) L_x(S\setminus\{y\}) L x ( S ∖ { y }) and depends only on the hashes of S ∖ { y } S\setminus\{y\} S ∖ { y } . Independence of hashes therefore gives
Pr [ h ( y ) ∈ J x ( S ∖ { y } ) ] = E [ ∣ J x ( S ∖ { y } ) ∣ ] m ≤ E [ L x ( S ∖ { y } ) ] m . \Pr[h(y) \in J_x(S\setminus\{y\})] = \frac{\mathbb{E}[|J_x(S\setminus\{y\})|]}{m} \le\frac{\mathbb{E}[L_x(S\setminus\{y\})]}{m}. Pr [ h ( y ) ∈ J x ( S ∖ { y })] = m E [ ∣ J x ( S ∖ { y }) ∣ ] ≤ m E [ L x ( S ∖ { y })] .
Each set S ∖ { y } S\setminus\{y\} S ∖ { y } has load at most 1 − ε 1-\varepsilon 1 − ε , so by the induction hypothesis, E [ L x ( S ∖ { y } ) ] ≤ ∣ x ∣ / ε \mathbb{E}[L_x(S\setminus\{y\})] \le|x|/\varepsilon E [ L x ( S ∖ { y })] ≤ ∣ x ∣/ ε . Therefore,
E [ L x ( S ) ] = ∣ x ∣ + ∑ y ∈ S ∖ { x } ∣ y ∣ ⋅ Pr [ h ( y ) ∈ J x ( S ∖ { y } ) ] ≤ ∣ x ∣ + 1 m ∑ y ∈ S ∖ { x } ∣ y ∣ ⋅ E [ L x ( S ∖ { y } ) ] ≤ ∣ x ∣ + ( 1 − ε ) ∣ x ∣ ε = ∣ x ∣ ε . \begin{aligned}
\mathbb{E}[L_x(S)] = |x| + \sum_{y \in S \setminus\{x\}} |y| \cdot\Pr[h(y) \in J_x(S \setminus\{y\})] \\
&\le|x| + \frac{1}{m} \sum_{y \in S \setminus\{x\}} |y| \cdot\mathbb{E}[L_x(S \setminus\{y\})] \\
&\le|x| + (1-\varepsilon)\frac{|x|}{\varepsilon} = \frac{|x|}{\varepsilon}.
\end{aligned} E [ L x ( S )] = ∣ x ∣ + y ∈ S ∖ { x } ∑ ∣ y ∣ ⋅ Pr [ h ( y ) ∈ J x ( S ∖ { y })] ≤ ∣ x ∣ + m 1 y ∈ S ∖ { x } ∑ ∣ y ∣ ⋅ E [ L x ( S ∖ { y })] ≤ ∣ x ∣ + ( 1 − ε ) ε ∣ x ∣ = ε ∣ x ∣ .
For negative queries, since E [ L y ] ≤ ∣ y ∣ / ε \mathbb{E}[L_y] \le|y|/\varepsilon E [ L y ] ≤ ∣ y ∣/ ε for every y ∈ S y \in S y ∈ S , Lemma 3.2 with C = 1 / ε C = 1/\varepsilon C = 1/ ε bounds the expected scan cost by 1 + 1 / ε 1 + 1/\varepsilon 1 + 1/ ε .
Corollary 7.1 (Anti-Robin-Hood Catalan Hashing in External Memory). Under the working-set assumption (see § 3 ), the hash table of Theorem 1.3 is cache-oblivious and answers positive queries for any item x ∈ S x \in S x ∈ S in expected 1 + O ( ∣ x ∣ / ( ε B ) ) 1 + O(|x|/(\varepsilon B)) 1 + O ( ∣ x ∣/ ( εB )) I/Os and negative queries in expected 1 + O ( 1 / ( ε B ) ) 1 + O(1/(\varepsilon B)) 1 + O ( 1/ ( εB )) I/Os.
The ARCH hash table is already good enough for the guarantees required by the dynamic version in § 9 . In particular, we bound the expected query cost of any item in terms of its size, or equivalently, the maximum expected query stretch , where the maximum is taken over all items. Theorem 1.3 shows that the maximum expected query stretch in an ARCH table is at most 1 / ε 1/\varepsilon 1/ ε .
In the static case, we can also consider the expected maximum stretch . It turns out that this quantity can be quite large in ARCH tables. If the table contains Θ ( n ) \Theta(n) Θ ( n ) items of length 2 and one large item y y y of size L = Θ ( δ n ) L = \Theta(\delta n) L = Θ ( δ n ) , then with constant probability, c ( y ) c(y) c ( y ) is the cell holding the second word of some length-2 item x x x (a constant fraction of all cells are such cells), so y y y preempts x x x after its first word and x x x has query stretch at least L / 2 = Ω ( δ n ) L/2 = \Omega(\delta n) L /2 = Ω ( δ n ) .
To bound both per-item expected query cost and expected maximum stretch across the entire table, we turn to a different fair queuing protocol, the fair-sojourn protocol, based on processor sharing.
Fair-Sojourn Catalan Hashing (FSCH)
Processor Sharing (PS) [33 ] is a service schedule which can be thought of as an idealized version of round-robin scheduling. In processor sharing, if j j j jobs are present in the system, they all share the server equally, each receiving service at instantaneous rate 1 / j 1/j 1/ j . Processor sharing is the canonical fair scheduling policy in queuing theory, avoiding both the starvation of large jobs (as in simple Catalan hashing) and the preemption of small jobs by newly arriving large jobs (as in ARCH) [56 ] .
Processor sharing cannot be used directly in practice, because jobs cannot truly share a server. Similarly, we cannot write fractions of words from multiple different items in a single hash-table cell. In practice, processor sharing is approximated in a number of different ways, the simplest of which is round-robin scheduling, which converges to PS as the size of the time quantum tends to 0 [33 ] . However, in our case, if we alternated words round-robin among active items, the resulting interleaved words would violate the Catalan nesting property (Definition 3.1 ) and could not be processed using our parser.
We will instead use the Fair-Sojourn Protocol (FSP) [26 ] , which is a schedule that realizes the benefits of PS while running a single job on the server at each point in time. FSP runs a virtual simulation of PS and assigns the processor to the remaining job that would finish first in this simulation. We use the following discrete-time version, which we show approximates FSP, which in turn dominates PS.
The virtual PS schedule. Let S S S be a set of items with arrivals A x = h ( x ) A_x = h(x) A x = h ( x ) and service requirements s x = ∣ x ∣ s_x = |x| s x = ∣ x ∣ , such that α = 1 m ∑ x ∈ S s x = 1 − ε \alpha= \frac{1}{m}\sum_{x\in S}s_x = 1-\varepsilon α = m 1 ∑ x ∈ S s x = 1 − ε . We run a continuous processor sharing schedule: every item x ∈ S x\in S x ∈ S arrives at A x A_x A x and departs at a well-defined PS departure time D x P S D_x^{\mathrm{PS}} D x PS , satisfying
∫ A x D x P S d t N S ( t ) = ∣ x ∣ , (4) \int_{A_x}^{D_x^{\mathrm{PS}}} \frac{dt}{N_S(t)} = |x|, \tag*{(4)} ∫ A x D x PS N S ( t ) d t = ∣ x ∣ , (4)
where the integral is interpreted cyclically and N S ( t ) N_S(t) N S ( t ) denotes the number of jobs of S S S alive in the virtual PS schedule at time t t t . (4) expresses that between A x A_x A x and D x P S D_x^{\mathrm{PS}} D x PS , x x x receives service at rate 1 / N S ( t ) 1/N_S(t) 1/ N S ( t ) at time t t t , which is the defining property of PS. Given S S S and its hashes, we can compute these departure times by sorting the arrivals and simulating the schedule from an idle point. Between consecutive arrivals and departures, N S ( t ) N_S(t) N S ( t ) is constant, so every active item’s remaining service decreases linearly at rate 1 / N S ( t ) 1/N_S(t) 1/ N S ( t ) .
FSCH Hash Tables. We define the FSCH priority π F S C H \pi^{\mathrm{FSCH}} π FSCH by giving higher priority to items with earlier departure times in the virtual PS schedule, interpreted cyclically. We define the FSCH table to be H π F S C H \mathcal{H}_{\pi_{\mathrm{FSCH}}} H π FSCH , as in Definition 3.4.
The FSCH table defines a hash-table-queue schedule which resembles a discrete-time version of FSP. Continuous FSP uses the arrival times h ( x ) h(x) h ( x ) and may preempt a job at a nonintegral time. FSCH instead makes x x x available at c ( x ) = ⌊ h ( x ) ⌋ c(x)=\lfloor h(x)\rfloor c ( x ) = ⌊ h ( x )⌋ and assigns each occupied cell entirely to one item, so preemptions occur only between words (i.e. at integral times). Both continuous FSP and FSCH priorities are computed from the virtual PS schedule with the original, unrounded arrivals. As shown in Lemma 7.2, this increases sojourn time by at most one unit.
FSCH Analysis
We first show that the FSCH sojourn time exceeds the continuous FSP sojourn time by at most one unit.
Lemma 7.2. In a hash-table queue for S S S , let D x F S P D_x^{\mathrm{FSP}} D x FSP and D x F S C H D_x^{\mathrm{FSCH}} D x FSCH be the departure times of an item x ∈ S x\in S x ∈ S under continuous FSP and FSCH, respectively, with cell j j j representing service during [ j , j + 1 ) [j,j+1) [ j , j + 1 ) . Then
[ D x F S C H − c ( x ) ] − [ D x F S P − h ( x ) ] ≤ 1 , [D_x^{\mathrm{FSCH}}-c(x)]-[D_x^{\mathrm{FSP}}-h(x)]\le1, [ D x FSCH − c ( x )] − [ D x FSP − h ( x )] ≤ 1 , where times are interpreted cyclically.
Proof. Fix x x x , write T = D x F S P T=D_x^{\mathrm{FSP}} T = D x FSP , and let P x P_x P x consist of x x x and all higher-priority items arriving before T T T . FSP has completed every item of P x P_x P x by T T T , since otherwise a higher-priority item would prevent x x x from finishing.
Rounding the arrivals of P x P_x P x down preserves this deadline. A schedule serving P x P_x P x whenever any of its work is pending therefore also finishes by T T T . Its arrivals and service requirements are integers, so it finishes by ⌊ T ⌋ \lfloor T\rfloor ⌊ T ⌋ . In the full FSCH schedule, lower-priority items never delay P x P_x P x , and every higher-priority item outside P x P_x P x has rounded arrival at least ⌊ T ⌋ \lfloor T\rfloor ⌊ T ⌋ . Thus D x F S C H ≤ ⌊ T ⌋ ≤ T D_x^{\mathrm{FSCH}}\le\lfloor T\rfloor\le T D x FSCH ≤ ⌊ T ⌋ ≤ T , and hence
[ D x F S C H − c ( x ) ] − [ D x F S P − h ( x ) ] ≤ h ( x ) − ⌊ h ( x ) ⌋ < 1. [D_x^{\mathrm{FSCH}}-c(x)]-[D_x^{\mathrm{FSP}}-h(x)]\le h(x)-\lfloor h(x)\rfloor<1. [ D x FSCH − c ( x )] − [ D x FSP − h ( x )] ≤ h ( x ) − ⌊ h ( x )⌋ < 1.
Next, a classical theorem of Fridman and Henderson [26 ] shows that FSP completes each item no later than PS.
Theorem 7.3 [26 ] . For any fixed arrival times and service requirements, continuous FSP and processor sharing satisfy
D x F S P ≤ D x P S for every x ∈ S , D^{\mathrm{FSP}}_x \le D^{\mathrm{PS}}_x \qquad\text{for every } x \in S, D x FSP ≤ D x PS for every x ∈ S , where times are interpreted cyclically.
Using Lemma 7.2 and Theorem 7.3 , any result on continuous PS for hash-table queues that bounds the sojourn time of an item x x x produces the same bound on the query scan cost of x x x in the FSCH table (up to rounding). We want to bound both the expected stretch of each item and the expected maximum stretch. Our results are analogues of similar results in the M/G/1 setting. There, each item’s expected stretch is 1 / ( 1 − ρ ) 1/(1-\rho) 1/ ( 1 − ρ ) , where ρ \rho ρ is the load; see e.g. Kleinrock [32 ] or Harchol-Balter [28 Section 30.1] . Cheung, van den Berg, and Boucherie [20 ] give bounds on all moments of the conditional sojourn time that are insensitive to the job-size distribution, yielding exponential tail bounds for stretch. These techniques do not immediately transfer to hash-table queues: an M/G/1 queue has Poisson arrivals and independently sampled service requirements, whereas hash-table queues have fixed finite sets of jobs with arbitrary sizes and independent uniform arrival times. In particular, the numbers of arrivals in disjoint intervals are dependent, so we cannot directly apply results that assume Poisson arrivals.
Our analysis instead establishes analogous arrival and deletion properties for hash-table queues. An arriving job sees the system with that job deleted at a uniformly random time, since its hash is independent of the other hashes. We show that PS departure times are also independent and uniform: reversing time gives a valid PS schedule, and the map from arrivals to departures preserves measure. Now consider the instant at which a job x x x has received a fixed amount v ∈ ( 0 , ∣ x ∣ ) v \in(0, |x|) v ∈ ( 0 , ∣ x ∣ ) of service. Truncating x x x to size v v v makes this instant its departure time, allowing us to show that the number of other jobs present has the same distribution as the population of the system without x x x at a uniformly random time. Integrating over v v v gives the service-deletion identity (Lemma B.5 ).
This identity lets us bound the queue population recursively, by deleting one job at a time. By doing so, we obtain a geometric tail bound on the number of jobs present at a random time u u u :
Pr [ N S ( u ) ≥ k ] ≤ α k \Pr[N_S(u) \ge k] \le\alpha^k Pr [ N S ( u ) ≥ k ] ≤ α k
(Lemma B.6 ). The service-deletion identity also expresses x x x ’s expected stretch as one plus the expected population of the system without x x x , giving a bound of 1 / ( 1 − α ) ≤ 1 / ε 1/(1-\alpha) \le1/\varepsilon 1/ ( 1 − α ) ≤ 1/ ε . Finally, every item’s stretch is at most the maximum queue population, since a job receives service at rate 1 / j 1/j 1/ j whenever j j j jobs are present. The population increases only at arrivals, so a union bound over the n n n arrivals gives the logarithmic factor in the expected maximum stretch. We defer the full analysis to Appendix § 13 , whose main result we state here.
Theorem 7.4. Let S S S be a nonempty set of n n n items with load ( S ) = α ≤ 1 − ε \operatorname{load}(S)=\alpha\le1-\varepsilon load ( S ) = α ≤ 1 − ε . In the PS schedule for S S S in the hash-table queue, each item has expected stretch at most 1 / ε 1/\varepsilon 1/ ε , and the expected maximum stretch is at most ( ln n + 1 ) / ε (\ln n+1)/\varepsilon ( ln n + 1 ) / ε .
We now combine Theorems 7.3 and 7.4 and Lemmas 3.2 and 7.2 to establish the bounds for FSCH:
Theorem 7.5 (Fair-Sojourn Catalan Hashing). Let ε ∈ ( 0 , 1 ) \varepsilon\in(0,1) ε ∈ ( 0 , 1 ) , and let S S S be a static set of n n n items stored in an m m m -cell table with load factor load ( S ) ≤ 1 − ε \operatorname{load}(S) \le1-\varepsilon load ( S ) ≤ 1 − ε . Fair-sojourn Catalan hashing (FSCH) uses O ( 1 ) O(1) O ( 1 ) bits of metadata per cell, answers positive queries for any item x ∈ S x \in S x ∈ S with expected scan cost at most 1 + ∣ x ∣ / ε 1+|x|/\varepsilon 1 + ∣ x ∣/ ε , and answers negative queries with expected scan cost at most 2 + 1 / ε 2+1/\varepsilon 2 + 1/ ε . Moreover, if L x L_x L x denotes the scan cost for x ∈ S x \in S x ∈ S , the expected maximum stretch across the entire table satisfies
E [ max x ∈ S L x ∣ x ∣ ] ≤ 1 + ln n + 1 ε . \mathbb{E}\left[\max_{x \in S}\frac{L_x}{|x|}\right] \le1+\frac{\ln n+1}{\varepsilon}. E [ x ∈ S max ∣ x ∣ L x ] ≤ 1 + ε ln n + 1 .
Proof. Each cell uses the O ( 1 ) O(1) O ( 1 ) bits of the Catalan representation. By Lemma 7.2 and Theorem 7.3 , the positive query scan cost satisfies L x ≤ T x + 1 L_x \le T_x+1 L x ≤ T x + 1 , where T x T_x T x is x x x ’s PS sojourn time. Thus Theorem 7.4 gives E [ L x ] ≤ 1 + ∣ x ∣ / ε ≤ ( 1 + 1 / ε ) ∣ x ∣ \mathbb{E}[L_x] \le1 + |x|/\varepsilon\le(1 + 1/\varepsilon)|x| E [ L x ] ≤ 1 + ∣ x ∣/ ε ≤ ( 1 + 1/ ε ) ∣ x ∣ , since ∣ x ∣ ≥ 1 |x| \ge1 ∣ x ∣ ≥ 1 . Applying Lemma 3.2 with C = 1 + 1 / ε C = 1 + 1/\varepsilon C = 1 + 1/ ε gives expected scan cost at most 2 + 1 / ε 2 + 1/\varepsilon 2 + 1/ ε for negative queries. Since ∣ x ∣ ≥ 1 |x| \ge1 ∣ x ∣ ≥ 1 , the expected maximum query stretch is at most
E [ max x ∈ S L x ∣ x ∣ ] ≤ 1 + E [ max x ∈ S T x ∣ x ∣ ] ≤ 1 + ln n + 1 ε . \mathbb{E}\left[\max_{x\in S}\frac{L_x}{|x|}\right] \le1+\mathbb{E}\left[\max_{x\in S}\frac{T_x}{|x|}\right] \le1+\frac{\ln n+1}{\varepsilon}. E [ x ∈ S max ∣ x ∣ L x ] ≤ 1 + E [ x ∈ S max ∣ x ∣ T x ] ≤ 1 + ε ln n + 1 .
Graveyard Catalan Hashing (Dynamic, High Load)
In this section, we combine the two constructions developed in the preceding sections. The hash tables in § 8 achieve load factor 1 − ε 1-\varepsilon 1 − ε and expected query scan cost O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) , but are static. The dynamic table of § 7 supports efficient updates, but its erasure codes and backup copies require a constant factor of extra space. In the following, we will construct a fully dynamic Catalan hash table at high load factor 1 − ε 1-\varepsilon 1 − ε , proving Theorem 1.4 .
The basic idea is to keep almost all items in a static table, and reserve Θ ( ε m ) \Theta(\varepsilon m) Θ ( ε m ) interleaved cells for a dynamic table that receives new insertions. After Θ ( ε m ) \Theta(\varepsilon m) Θ ( ε m ) words of updates, we rebuild the static table on the current live set. The dynamic table’s constant-factor space overhead then uses only O ( ε m ) O(\varepsilon m) O ( ε m ) cells, and a rebuild with scan cost O ( m ) O(m) O ( m ) has amortized cost only O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) per word of updates.
Our construction takes inspiration from graveyard hashing [14 ] , which achieves dynamic high-load operations in unit-size linear probing by maintaining tombstones from deletions (and inserting artificial tombstones during periodic rebuilds) so that new insertions quickly find tombstones without scanning to the end of a long cluster. In our variable-length external-memory setting, graveyard hashing would be subject to the same write amplification issue we described in § 6 . Moreover, both the ARCH and FSCH schemes in § 8 use an inherently static scheduling-based approach.
Instead, we implement the graveyard hashing framework in a more heavy-handed structural manner: we explicitly decompose the hash table into interleaved tables that share the same physical array. Into a large static table, we sparsely interleave a small dynamic table that serves the same function as the tombstones in graveyard hashing: it efficiently accepts new insertions. Also as in graveyard hashing, periodically we rebuild to refresh the dynamic table.
In the next sections, we describe the construction and its operation bounds; we later show how to rebuild in a constant number of sequential passes (Appendix § 15 ).
A Static Table with a Small Dynamic Buffer
We divide the operation sequence into rebuild windows . At the start of a window, all live items are stored in a static table H f u l l \mathcal{H}_{\mathrm{full}} H full with m m m cells. During the window, insertions go into a small dynamic table C s m a l l \mathcal{C}_{\mathrm{small}} C small , and deletions from H f u l l \mathcal{H}_{\mathrm{full}} H full merely set a flag. Thus the static layout remains fixed until the next rebuild.
The Three Tables. Let ε ′ = ε / C \varepsilon'=\varepsilon/C ε ′ = ε / C , where C C C is a sufficiently large constant, and set
P = ⌊ 1 / ε ′ ⌋ . P=\left\lfloor1/\varepsilon'\right\rfloor. P = ⌊ 1/ ε ′ ⌋ .
Let m s m a l l m_{\mathrm{small}} m small be the smallest power of two at least ⌈ m / P ⌉ \lceil m/P\rceil ⌈ m / P ⌉ , so that the auxiliary tables satisfy the size convention of § 7.3 . The construction consists of the following tables.
(1) The static table H f u l l \mathcal{H}_{\mathrm{full}} H full . This is an m m m -cell ARCH table built on the live set T T T at the start of the window. We keep T T T fixed throughout the window, including items subsequently marked as deleted. (We fix the use of ARCH for ease of exposition. Since FSCH guarantees the same expected query scan bounds, all of the claimed bounds in this section would hold with a FSCH-based static table as well.) We add two additional metadata bits to each cell: a deletion bit , which marks the first cell of an item to indicate it’s deleted, and a collision bit , defined below, which enables queries for deleted items to resolve fingerprint collisions. Deleted items’ words remain in place. The static table’s placement rule and remaining metadata are unchanged.
(2) The insertion table C s m a l l \mathcal{C}_{\mathrm{small}} C small . This is the dynamic low-load structure
C s m a l l = C ( H s m a l l , M s m a l l , B s m a l l ) \mathcal{C}_{\mathrm{small}}=\mathcal{C}(\mathcal{H}_{\mathrm{small}},\mathcal{M}_{\mathrm{small}},\mathcal{B}_{\mathrm{small}}) C small = C ( H small , M small , B small )
of Section 6, extended to support deletions by Lemma 8.1 below. Each of its three constituent tables has m s m a l l m_{\mathrm{small}} m small cells. It starts empty and stores the items inserted during the window that are still live. As in Section 6, we sometimes abuse notation and refer to a triple as a cell, for example, the canonical cell c s m a l l ( x ) c_{\mathrm{small}}(x) c small ( x ) of an item x x x .
(3) The deletion table D \mathcal{D} D . This is an ordinary insertion-only unordered linear-probing table with m s m a l l m_{\mathrm{small}} m small cells. It starts empty. Whenever an item of H f u l l \mathcal{H}_{\mathrm{full}} H full is deleted, we insert a 1-word descriptor d ( k ) d(k) d ( k ) into D \mathcal{D} D , starting at c s m a l l ( k ) c_{\mathrm{small}}(k) c small ( k ) . d ( k ) = ( c ( k ) , ψ ( k ) ) d(k)=(c(k),\psi(k)) d ( k ) = ( c ( k ) , ψ ( k )) consists of k k k ’s canonical cell together with a 4 log m 4\log m 4 log m -bit fingerprint from an independent hash function. During rebuilding, we set the collision bit at a static cell j j j exactly when two distinct keys in T T T with canonical cell j j j have the same fingerprint. These bits remain fixed throughout the window. Thus, when the collision bit at c ( k ) c(k) c ( k ) is clear, at most one key in T T T has descriptor d ( k ) d(k) d ( k ) . We always insert one record per deleted static item, even in the above case where two descriptors are the same, allowing equal descriptors to occupy separate cells. Records stay in D \mathcal{D} D until the next rebuild, even if their keys are subsequently reinserted into C s m a l l \mathcal{C}_{\mathrm{small}} C small .
At all times, each live item occurs either as an unflagged item of H f u l l \mathcal{H}_{\mathrm{full}} H full or as a live item of C s m a l l \mathcal{C}_{\mathrm{small}} C small , and D \mathcal{D} D contains one descriptor for each flagged item of H f u l l \mathcal{H}_{\mathrm{full}} H full .
The deletion table is structured using descriptors with fingerprints to handle a complication that can arise with deletions. A negative query for a deleted key could still scan the entire old item, which may be much longer than the key itself. The static negative-query bound does not apply, because this key’s hash is correlated with the contents of the static table. Storing the deleted key in a second table would reduce this cost only to one proportional to the key’s length, but we reduce this further to O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) in expectation by using constant-size descriptors. We check for collisions among resident keys during rebuilding, so that a fingerprint match can certify absence without reading the deleted key.
Interleaving and Space. As in Section 6.1, we interleave all constituent tables in a single physical array. When interleaving the cell s s s (technically three cells) of C s m a l l \mathcal{C}_{\mathrm{small}} C small , we follow it with cell s s s from D \mathcal{D} D . We refer to this collection of cells as a single small cell s s s . For 0 ≤ s < m s m a l l 0 \le s < m_{\mathrm{small}} 0 ≤ s < m small , place small cell s s s immediately after cell ⌈ ( s + 1 ) m / m s m a l l ⌉ − 1 \left\lceil(s+1)m/m_{\mathrm{small}}\right\rceil-1 ⌈ ( s + 1 ) m / m small ⌉ − 1 of H f u l l \mathcal{H}_{\mathrm{full}} H full . Thus consecutive small cells are separated by at most ⌈ m / m s m a l l ⌉ ≤ P \lceil m/m_{\mathrm{small}}\rceil\le P ⌈ m / m small ⌉ ≤ P static cells and four component cells of the small cell.
Assume for now that ε ′ m ≥ 17 \varepsilon' m \ge17 ε ′ m ≥ 17 . Since ε ′ < 1 / 2 \varepsilon' < 1/2 ε ′ < 1/2 , we have m s m a l l ≥ ε ′ m m_{\mathrm{small}} \ge\varepsilon' m m small ≥ ε ′ m and m s m a l l < 2 ⌈ m / P ⌉ ≤ 2 ( 2 ε ′ m + 1 ) m_{\mathrm{small}} < 2\lceil m/P\rceil\le2(2\varepsilon' m+1) m small < 2 ⌈ m / P ⌉ ≤ 2 ( 2 ε ′ m + 1 ) , so
m t o t a l = m + 4 m s m a l l ≤ ( 1 + 28 ε ′ ) m . m_{\mathrm{total}}=m+4m_{\mathrm{small}}\le(1+28\varepsilon')m. m total = m + 4 m small ≤ ( 1 + 28 ε ′ ) m .
Choosing C ≥ 56 C \ge56 C ≥ 56 ensures that a live volume of at most ( 1 − ε ) m t o t a l (1-\varepsilon)m_{\mathrm{total}} ( 1 − ε ) m total is at most ( 1 − ε / 2 ) m (1-\varepsilon/2)m ( 1 − ε /2 ) m . In particular, each rebuilt static table has slack at least ε / 2 \varepsilon/2 ε /2 . Consecutive cells of any component of the small table are at most σ = P + 4 = O ( 1 / ε ) \sigma=P+4=O(1/\varepsilon) σ = P + 4 = O ( 1/ ε ) physical cells apart.
All tables use the same hash, rescaled to their lengths. A key k k k has canonical cell c ( k ) = ⌊ h ( k ) ⌋ c(k)=\lfloor h(k)\rfloor c ( k ) = ⌊ h ( k )⌋ in the static table and
c s m a l l ( k ) = ⌊ m s m a l l m h ( k ) ⌋ c_{\mathrm{small}}(k)=\left\lfloor\frac{m_{\mathrm{small}}}{m}h(k)\right\rfloor c small ( k ) = ⌊ m m small h ( k ) ⌋
in the small table. c s m a l l ( k ) c_{\mathrm{small}}(k) c small ( k ) lies within O ( P ) O(P) O ( P ) physical cells after c ( k ) c(k) c ( k ) . Consequently, scans in all three structures start near the same physical location, which preserves the I/O bounds.
The Update Budget. We keep two counters: the total volume inserted into C s m a l l C_{\mathrm{small}} C small , including items subsequently deleted, and the total length of keys deleted from H f u l l H_{\mathrm{full}} H full . The latter also bounds the number of records inserted into D D D . We ensure that neither counter ever exceeds b = ε ′ m / 64 ≤ m s m a l l / 64 b=\varepsilon' m/64\leq m_{\mathrm{small}}/64 b = ε ′ m /64 ≤ m small /64 . Before an operation would exceed either budget, we rebuild and then perform the operation in the new window. An insertion of length at least b / 2 b/2 b /2 , or a deletion from H f u l l H_{\mathrm{full}} H full whose key has length at least b / 2 b/2 b /2 , is instead incorporated directly into a rebuild. We call these long updates . This rule keeps the insertion table at the low load required by Section 6, and keeps D D D at load at most 1 / 64 1/64 1/64 . It also ensures that a budget-triggered rebuild follows at least b / 2 = Ω ( ε m ) b/2=\Omega(\varepsilon m) b /2 = Ω ( ε m ) words charged to one of the counters.
If ε ′ m < 17 \varepsilon' m<17 ε ′ m < 17 , we use just the static table and rebuild after every update. Then m = O ( 1 / ε ) m=O(1/\varepsilon) m = O ( 1/ ε ) , so a rebuild has scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) ; under the cache-size assumption in Section 8.3, the whole table fits in cache. The bounds below therefore also cover this case.
Operations Within a Rebuild Window
We use the low-load dynamic table from Theorem 1.2 as a black box, except we extend it to support deletions.
Lemma 8.1 (Low-Load Deletions). The structure C ( H , M , B ) C(\mathcal{H},\mathcal{M},\mathcal{B}) C ( H , M , B ) of Section 6, with m m m cells in each constituent table and initially empty, supports any operation sequence whose insertions have total volume at most m / 64 m/64 m /64 . Its scan costs are:
O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) in amortized expectation for a positive query for an item x x x ;
O ( 1 ) O(1) O ( 1 ) in expectation for a negative query; and
O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) in amortized expectation for an insertion or deletion of an item x x x .
Each operation on a key k k k makes O ( 1 ) O(1) O ( 1 ) forward passes over a contiguous physical interval starting at c ( k ) c(k) c ( k ) . Under the cache assumptions of Section 6, the corresponding I/O bounds are 1 + O ( L / B ) 1+O(L/B) 1 + O ( L / B ) , where L L L is the stated scan bound.
The proof is given in Appendix C. The main issue is repeated deletion and reinsertion of one key: its successive values share a hash, so they cannot be treated as independent items. The construction reuses a deleted copy when that key is reinserted into the same size class. We use the budget m / 64 m/64 m /64 so that the density bound for the backup table continues to apply.
Lemma 8.2 (Interleaving Preserves Scan Bounds). An operation in C s m a l l C_{\mathrm{small}} C small or D D D with logical scan bound L L L has physical scan bound O ( σ ( L + 1 ) ) O(\sigma(L+1)) O ( σ ( L + 1 )) . Its accesses make O ( 1 ) O(1) O ( 1 ) forward passes over an interval beginning within O ( σ ) O(\sigma) O ( σ ) cells after c ( k ) c(k) c ( k ) in H f u l l H_{\mathrm{full}} H full . The same statement holds for expected and amortized expected bounds.
Proof. An interval of L L L cells in any auxiliary constituent table spans at most σ ( L + 1 ) \sigma(L+1) σ ( L + 1 ) physical cells. All constituent scans start at their canonical cells. For g = c s m a l l ( k ) g=c_{\mathrm{small}}(k) g = c small ( k ) , the boundary ( g + 1 ) m / m s m a l l (g+1)m/m_{\mathrm{small}} ( g + 1 ) m / m small lies in ( h ( k ) , h ( k ) + m / m s m a l l ] (h(k),h(k)+m/m_{\mathrm{small}}] ( h ( k ) , h ( k ) + m / m small ] . Rounding this boundary changes its position by less than one static cell, and m / m s m a l l ≤ P m/m_{\mathrm{small}} \le P m / m small ≤ P . Thus these starting points lie within O ( P ) O(P) O ( P ) physical cells after c ( k ) c(k) c ( k ) . The encoding, decoding, and allocator bookkeeping use the same scratch region as before, so interleaving changes only the lengths of the scans.
Deletion-Table Scans. A search in D \mathcal{D} D starts at c s m a l l ( k ) c_{\mathrm{small}}(k) c small ( k ) and scans to a matching descriptor or the first empty cell. An insertion scans to the first empty cell and writes its record there. Each static item is deleted at most once per window. Moreover, the window boundaries and the sequence of deleted static keys are determined by the operation sequence, independently of the hashes. Thus the records have independent uniform home small cells, even if their descriptors coincide. Since the load is at most 1 / 64 1/64 1/64 , ordinary unit-size linear probing gives expected logical scan cost O ( 1 ) O(1) O ( 1 ) for either operation, whether or not the query key has a record. By Lemma 8.2, the physical scan cost is O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) in expectation.
Queries. A query for k k k reads the collision bit at c ( k ) c(k) c ( k ) and scans forward from that cell, running the following searches together:
Search C s m a l l \mathcal{C}_{\mathrm{small}} C small for a live item with key k k k .
If the collision bit is clear, search D \mathcal{D} D for d ( k ) d(k) d ( k ) .
Search H f u l l \mathcal{H}_{\mathrm{full}} H full with the Catalan parser, treating flagged items as absent. Abandon this search as soon as the deletion-table search finds d ( k ) d(k) d ( k ) .
Return the live item if one is found, and otherwise return Not Found once the insertion-table search and either the static-table search or the deletion-table search establish absence. The searches advance together through the interleaved array; any backup lookup or reinflation uses the forward passes of the low-load construction.
To see why a deletion-table match certifies absence from the live static set, let z ∈ T z \in T z ∈ T be the deleted item that supplied the matching descriptor. If k k k were also the key of a live static item x x x , then d ( k ( x ) ) = d ( k ( z ) ) d(k(x)) = d(k(z)) d ( k ( x )) = d ( k ( z )) . Since the collision bit at c ( k ) c(k) c ( k ) is clear, x = z x = z x = z , a contradiction. A collision with a key outside T T T is harmless: that key is absent from the static table anyway. The query must still search the insertion table, since a deleted static key may have been reinserted there.
Lemma 8.3 (Query Bounds Within a Window). A *positive query for an item x x x has amortized expected scan cost O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) . Every negative query has expected scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) , independently of the key length. Each query makes O ( 1 ) O(1) O ( 1 ) forward passes over a contiguous physical interval starting at c ( k ) c(k) c ( k ) .
Proof. If the live item x x x is in the insertion table, its search finishes with amortized expected scan cost O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) by Lemmas 8.1 and 8.2; the concurrent searches stop no later. If x x x is an unflagged static item, the static search has expected scan cost O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) , while the insertion-table search is negative and costs O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) in expectation.
Now consider a negative query for k k k . The insertion-table search has expected scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) . If k k k is not a key of T T T , its hash is independent of the static contents, so the static search has expected scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) by Theorem 1.3; an early stop from D \mathcal{D} D can only shorten it.
Otherwise, k k k belongs to a flagged static item. Let E \mathcal{E} E be the event that two distinct keys of T T T have equal fingerprints. Since ∣ T ∣ ≤ m |T| \le m ∣ T ∣ ≤ m ,
Pr [ E ] ≤ ( ∣ T ∣ 2 ) m − 4 = O ( m − 2 ) . \Pr[\mathcal{E}] \le\binom{|T|}{2}m^{-4} = O(m^{-2}). Pr [ E ] ≤ ( 2 ∣ T ∣ ) m − 4 = O ( m − 2 ) .
Outside E \mathcal{E} E , the collision bit at c ( k ) c(k) c ( k ) is clear and the query stops the static search upon finding d ( k ) d(k) d ( k ) in D D D . Let L D L_D L D be the physical distance from c ( k ) c(k) c ( k ) through this record, with E [ L D ] = O ( 1 / ε ) \mathbb{E}[L_D] = O(1/\varepsilon) E [ L D ] = O ( 1/ ε ) by the deletion-table scan bound. The static and deletion-table searches together cost at most
O ( L D ) + O ( m ) 1 [ E ] . O(L_D) + O(m)1[\mathcal{E}]. O ( L D ) + O ( m ) 1 [ E ] .
Taking expectations gives O ( 1 / ε ) + O ( m ) Pr [ E ] = O ( 1 / ε ) O(1/\varepsilon) + O(m)\Pr[\mathcal{E}] = O(1/\varepsilon) O ( 1/ ε ) + O ( m ) Pr [ E ] = O ( 1/ ε ) , without conditioning the deletion-table bound on the collision bits. This estimate applies separately to every query, including repeated queries for the same deleted key; no query cost is charged to its deletion.
Insertions. To insert an item x x x whose key is absent, first apply the budget rule. A long insertion is incorporated directly into a rebuild; otherwise, rebuild first if necessary, and then insert x x x into C s m a l l C_{\mathrm{small}} C small . Its key may already have a dead copy there; Lemma 8.1 handles this case. Apart from rebuilding, Lemmas 8.1 and 8.2 give amortized expected scan cost O ( ∣ x ∣ ε − 1 log 1 + δ ∣ x ∣ ) O(|x|\varepsilon^{-1}\log^{1+\delta}|x|) O ( ∣ x ∣ ε − 1 log 1 + δ ∣ x ∣ ) .
Deletions. To delete a present key k k k , first locate its item x x x with a query. If x x x is in C s m a l l C_{\mathrm{small}} C small , delete it using Lemma 8.1 . Otherwise, check the budget rule before changing the table. A long deletion is incorporated directly into a rebuild. If the deletion is short but the key budget would be exceeded, rebuild first and locate x x x in the rebuilt static table. Then set the deleted flag at its opening cell and insert d ( k ) d(k) d ( k ) into D D D . The static table’s delimiters, home bits, stop marks, and collision bits remain unchanged, so its parser continues to operate on exactly the layout built at the start of the window. The auxiliary update has expected scan cost O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) , which is within the claimed update bound. Thus both insertions and deletions have the desired update bound within a window. It remains to pay for rebuilding; we defer the analysis of the amortized expected cost of rebuilds to Appendix § 15 .
Putting the Bounds Together
Cache Size for I/O Bounds. Our I/O bounds use the working-set assumption of § 3 , together with M ≥ C M ( B + s max ε − 2 log 3 m ) M \ge C_M(B+s_{\max}\varepsilon^{-2}\log^3 m) M ≥ C M ( B + s m a x ε − 2 log 3 m ) for a sufficiently large constant C M C_M C M . This accommodates the encoding, decoding, and bookkeeping space of § 7 , as well as O ( s max ε − 2 log 3 m ) O(s_{\max}\varepsilon^{-2}\log^3 m) O ( s m a x ε − 2 log 3 m ) words of workspace for rebuilding one run at a time in cache (Appendix § 15 ). We remark that our assumption on M M M is quite mild in the sense that it only requires memory to be slightly larger than what is required to fit the largest string.
The following proposition collects the guarantees established by the construction, proving Theorem 1.4 and Corollary 1.5.
Proposition 8.4 (Dynamic High-Load Bounds). For ε ∈ ( 0 , 1 / 2 ) \varepsilon\in(0,1/2) ε ∈ ( 0 , 1/2 ) and fixed δ > 0 \delta> 0 δ > 0 , the construction operates at load factor 1 − ε 1-\varepsilon 1 − ε and has the following amortized expected scan costs:
(1) O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) for a positive query for an item x x x ;
(2) O ( 1 / ε ) O(1/\varepsilon) O ( 1/ ε ) for a negative query, independently of the key length; and
(3) O ( ∣ x ∣ log 1 + δ ∣ x ∣ / ε ) O(|x|\log^{1+\delta}|x|/\varepsilon) O ( ∣ x ∣ log 1 + δ ∣ x ∣/ ε ) for an insertion or deletion of an item x x x .
Under the cache-size assumption above, the construction is cache-oblivious and the corresponding amortized expected I/O bounds are
positive query: 1 + O ( ∣ x ∣ / ( ε B ) ) , negative query: 1 + O ( 1 / ( ε B ) ) , insertion or deletion: 1 + O ( ∣ x ∣ log 1 + δ ∣ x ∣ / ( ε B ) ) . \begin{aligned}
\text{positive query:} &\quad1 + O(|x|/(\varepsilon B)),\\
\text{negative query:} &\quad1 + O(1/(\varepsilon B)),\\
\text{insertion or deletion:} &\quad1 + O(|x|\log^{1+\delta}|x|/(\varepsilon B)).
\end{aligned} positive query: negative query: insertion or deletion: 1 + O ( ∣ x ∣/ ( εB )) , 1 + O ( 1/ ( εB )) , 1 + O ( ∣ x ∣ log 1 + δ ∣ x ∣/ ( εB )) .
Proof. The space calculation in § 9.1 leaves slack at least ε / 2 \varepsilon/2 ε /2 in the static table. The update budgets keep the insertion table within the hypotheses of Lemma 8.1 and keep the deletion table at load at most 1 / 64 1/64 1/64 . Thus Lemma 8.3 and the update analysis give all within-window scan bounds.
Each budget-triggered rebuild follows Ω ( ε m ) \Omega(\varepsilon m) Ω ( ε m ) words of insertions or deleted keys. Charge its O ( m ) O(m) O ( m ) expected scan cost to those words. A long update has length Ω ( ε m ) \Omega(\varepsilon m) Ω ( ε m ) and pays for its own rebuild. Since a deleted key is no longer than its item, rebuilding adds only O ( ∣ x ∣ / ε ) O(|x|/\varepsilon) O ( ∣ x ∣/ ε ) to each update’s amortized expected scan cost.
For I/Os, all within-window accesses make O ( 1 ) O(1) O ( 1 ) forward passes over a contiguous interval starting at c ( k ) c(k) c ( k ) . The cache-size assumption above converts a scan bound L L L to 1 + O ( L / B ) 1 + O(L/B) 1 + O ( L / B ) expected I/Os, with a single leading 1 1 1 shared by the interleaved searches. By Lemma D.4 , rebuilding adds O ( ∣ x ∣ / ( ε B ) ) O(|x|/(\varepsilon B)) O ( ∣ x ∣/ ( εB )) amortized expected I/Os per update. None of the algorithms uses M M M or B B B . □ \square □
Acknowledgments
This work is partially supported by NSF grants 2504470, 2504471, 2551615, and 2542165, as well as by a CMU / Jane Street collaboration grant.
AI Disclosure
All of the main technical ideas in this paper were generated by the authors over the duration of around a year; during that time, the authors used AI tools (all models of GPT, Claude, and Gemini during that time span) to aid in literature review and to explore different ways of structuring the proofs. AI tools were also used in the writing process to help write the starting draft for the paper, which was then almost entirely rewritten by the authors. Finally, AI tools were used to polish text throughout, and to perform edits to the paper.
We reviewed and revised the AI-assisted material and take full responsibility for the content, correctness, and originality of the paper.
The Static Catalan Parser
State. The parser maintains a stack S S S of active item frames for items whose OPEN tag has been seen at or after cell t t t , along with a single integer counter f ≥ 0 f \ge0 f ≥ 0 (initially 0) that counts observed words belonging to an “unmatched suffix” of an item whose OPEN tag occurred before cell t t t . Each frame on S S S buffers the key, maintaining a word counter c n t cnt c n t (the number of words of that item seen so far) and a boolean match flag indicating whether the key words seen so far match the corresponding prefix of the queried key k k k . When the buffer reaches the end of the key, it computes and retains its canonical cell before discarding the buffer. The match flag tests equality of the complete keys, including their lengths. If the key matches k k k , the frame buffers the subsequent value in order to return it. These key and value buffers are part of the parser state assumed to reside in cache in Section 2.
Transitions on Reading a Cell j j j .
(i) OPEN: Push a new frame onto S S S with c n t = 1 cnt = 1 c n t = 1 , and compare the cell’s word with the first word of k k k .
(ii) OPEN-CLOSE: This cell is a complete 1-word item; if its key is k k k , return FOUND (we assume a 1-word item has no value). Otherwise, compute its canonical cell and apply the stop-cell rule of Section 3. The stack S S S and counter f f f are unchanged.
(iii) MIDDLE with S S S non-empty: Increment c n t cnt c n t of the top frame of S S S , and, if the c n t cnt c n t -th word of the item is part of its key, compare it with the c n t cnt c n t -th word of k k k .
(iv) CLOSE with S S S non-empty: Increment c n t cnt c n t of the top frame of S S S , compare the cell’s word with k k k as in (iii), and pop the frame. If the completed item’s key is k k k , return FOUND, together with its buffered value. Otherwise, use its retained canonical cell and the stop bit of cell j j j to apply the stop-cell rule of Section 3.
(v) MIDDLE with S S S empty: Increment the unmatched-suffix counter f ← f + 1 f \leftarrow f + 1 f ← f + 1 .
(vi) CLOSE with S S S empty: Increment f ← f + 1 f \leftarrow f + 1 f ← f + 1 (completing the unmatched suffix of some item that opened before t t t ), and reset f ← 0 f \leftarrow0 f ← 0 . No stop-cell test is needed for this item: when t = c ( k ) t = c(k) t = c ( k ) , an item that opened before t t t cannot have canonical cell c ( k ) c(k) c ( k ) .
Lemma A.1 (Correctness of the Catalan Parser). Suppose a table satisfies the Catalan nesting property (Definition 3.1 ), and a scan starts at cell t t t . Then the Catalan parser correctly assembles, in logical word order, every item y y y whose entire interval I y I_y I y lies at or after t t t . Furthermore, at every step of the scan:
(1) Each frame on the stack S S S tracks a single item y y y that opened at or after t t t , and its counter c n t cnt c n t equals the number of words of y y y seen so far (so c n t ≤ ∣ y ∣ cnt \le|y| c n t ≤ ∣ y ∣ ).
(2) Whenever f > 0 f > 0 f > 0 , the unmatched-suffix counter f f f counts words belonging to a single item w w w that opened before t t t (namely, the innermost such item that has not yet closed), and never combines words from two different items (so f ≤ ∣ w ∣ f \le|w| f ≤ ∣ w ∣ ).
Proof. By Definition 3.1 , whenever an item z z z is nested inside another item y y y (I z ⊆ I y I_z \subseteq I_y I z ⊆ I y ), I z I_z I z lies strictly between two consecutive cells of y y y . If y y y opened at or after t t t , then y y y ’s frame is on the stack S S S when z z z ’s OPEN is encountered; z z z ’s frame is pushed above y y y , absorbs all words of z z z (and any items nested inside z z z ), and is popped on z z z ’s CLOSE before the next word of y y y is encountered. Thus y y y ’s frame receives precisely the words of y y y in order. Similarly, when S S S is empty, any MIDDLE or CLOSE cell belongs to the innermost enclosing item w w w that opened before t t t . Any item z z z nested between two words of w w w after t t t pushes a frame onto S S S and pops it before the next word of w w w , leaving f f f untouched during I z I_z I z ; when w w w ’s CLOSE is reached with S S S empty, f f f is reset to 0 0 0 before any outer enclosing item’s words can be seen. Thus f f f never conflates words from two different items.
Analysis of Processor Sharing for Hash-Table Queues
In this appendix, we prove Theorem 7.4, which bounds the expected stretch of each item and the expected maximum stretch in the virtual processor-sharing (PS) schedule used by FSCH. Recall the hash-table-queue model of § 8.1 : the set S S S of n n n items and their sizes are fixed, while the arrival times A x = h ( x ) A_x = h(x) A x = h ( x ) are independent and uniform on a circle of circumference m m m . The load is α = 1 m ∑ x ∈ S ∣ x ∣ ≤ 1 − ε \alpha= \frac{1}{m}\sum_{x \in S}|x| \le1 - \varepsilon α = m 1 ∑ x ∈ S ∣ x ∣ ≤ 1 − ε . We write D x D_x D x for the PS departure time of x x x and N ( t ) = N S ( t ) N(t) = N_S(t) N ( t ) = N S ( t ) for the number of jobs present at time t t t . Each active job receives service at rate 1 / N ( t ) 1/N(t) 1/ N ( t ) , and its stretch is its sojourn time divided by its size. All times and intervals below are interpreted cyclically.
Our main tool is the service-deletion identity (Lemma B.5), which relates the population seen by a job during its service to the population of the system with that job removed. We first establish the arrival and departure properties needed to prove this identity, using time reversal and a measure-preserving map from arrivals to departures. The identity then yields a geometric tail bound on the queue population and the expected stretch bound for each item. Finally, a union bound over arrivals controls the maximum population, giving the bound on the expected maximum stretch.
We begin by considering what an arriving job sees. In the M/G/1 setting, PASTA (“Poisson Arrivals See Time Averages”) equates the distribution seen by arrivals with the stationary time-average distribution [28 Section 13.3] . Here A x A_x A x is uniform and independent of the hashes of S ∖ { x } S \setminus\{x\} S ∖ { x } , so the corresponding comparison is with the system in which x x x is absent.
Observation B.1. In a hash-table queue, the state of the system immediately before an item x x x arrives has the same distribution as the state of the system without x x x at a uniformly random time.
We would like a similar deletion property at departures, so that it can later be applied at intermediate stages of a job’s service. Our first ingredient is time reversal, which is classical in queuing theory [31 ] . We observe that this holds immediately for processor sharing in hash-table queues.
Observation B.2. In a hash-table queue, given a PS schedule with arrivals ( A x ) x ∈ S (A_x)_{x\in S} ( A x ) x ∈ S and departures ( D x ) x ∈ S (D_x)_{x\in S} ( D x ) x ∈ S , the reverse schedule defined by A ~ x = m − D x \widetilde{A}_x=m-D_x A x = m − D x and D ~ x = m − A x \widetilde{D}_x=m-A_x D x = m − A x is a valid PS schedule.
To invoke Observation B.1 in the reversed schedule, we also need its arrivals (departures in the forward schedule) to be independent and uniform. This is the analogue of the Poisson-output property of stationary M/G/1-PS queues: in both models, departures retain the distributional form of arrivals [28 Chapter 22] .
Lemma B.3. In a hash-table queue, the departures in a PS schedule are uniformly and independently distributed.
Proof. Each arrival A x A_x A x and departure D x D_x D x lives in R / m Z \mathbb{R}/m\mathbb{Z} R / m Z . Therefore the collection of all arrivals (resp. all departures) lives on ( R / m Z ) ∣ S ∣ (\mathbb{R}/m\mathbb{Z})^{|S|} ( R / m Z ) ∣ S ∣ . The proof will analyze the map Φ : ( A x ) x ∈ S ↦ ( D x ) x ∈ S \Phi:(A_x)_{x\in S}\mapsto(D_x)_{x\in S} Φ : ( A x ) x ∈ S ↦ ( D x ) x ∈ S . We will show that Φ \Phi Φ is bijective and measure-preserving.
By Observation B.2, the reverse schedule is a valid PS schedule. Thus departures determine arrivals uniquely by reversing time and running PS again, so Φ \Phi Φ is a bijection.
Now, outside the measure-zero set where two or more arrivals or departures coincide, the number of jobs N ( t ) N(t) N ( t ) is locally constant except at the points { A x , D x } x ∈ S \{A_x,D_x\}_{x\in S} { A x , D x } x ∈ S . We differentiate the service equations (Equation (4) )
∣ x ∣ = ∫ A x D x d t N ( t ) , x ∈ S , (5) |x|=\int_{A_x}^{D_x}\frac{\mathrm{d}t}{N(t)},\qquad x\in S, \tag*{(5)} ∣ x ∣ = ∫ A x D x N ( t ) d t , x ∈ S , (5)
yielding Γ A d A + Γ D d D = 0 \Gamma^A\mathrm{d}A+\Gamma^D\mathrm{d}D=0 Γ A d A + Γ D d D = 0 , where Γ A \Gamma^A Γ A and Γ D \Gamma^D Γ D are locally constant matrices (except at { A x , D y } x ∈ S \{A_x,D_y\}_{x\in S} { A x , D y } x ∈ S ). Starting at an idle point, we order the jobs by arrival time. Then, for y ≠ x y\ne x y = x , an infinitesimal perturbation of A y A_y A y only affects the integral in Equation (B.1) if A y A_y A y is in the interval ( A x , D x ) (A_x,D_x) ( A x , D x ) (interpreted cyclically). Therefore Γ A \Gamma^A Γ A is upper triangular with diagonal entries Γ x x A = − 1 N ( A x + ) \Gamma^A_{xx}=-\frac{1}{N(A_x+)} Γ xx A = − N ( A x + ) 1 . Similarly, ordering the rows and columns by departure time makes Γ D \Gamma^D Γ D lower triangular with diagonal entries Γ x x D = 1 / N ( D x − ) \Gamma^D_{xx}=1/N(D_x-) Γ xx D = 1/ N ( D x − ) . This reordering does not change its determinant. Therefore, the Jacobian determinant of Φ \Phi Φ is
∣ det ∂ D ∂ A ∣ = ∣ det Γ A ∣ ∣ det Γ D ∣ = ∏ x ∈ S N ( D x − ) ∏ x ∈ S N ( A x + ) . \left|\det\frac{\partial D}{\partial A}\right|=\frac{|\det\Gamma^A|}{|\det\Gamma^D|}=\frac{\prod_{x\in S}N(D_x-)}{\prod_{x\in S}N(A_x+)}. det ∂ A ∂ D = ∣ det Γ D ∣ ∣ det Γ A ∣ = ∏ x ∈ S N ( A x + ) ∏ x ∈ S N ( D x − ) .
Every arrival where N ( A x + ) = r N(A_x+)=r N ( A x + ) = r is an upcrossing from r − 1 r-1 r − 1 to r r r , and every departure where N ( D x − ) = r N(D_x-)=r N ( D x − ) = r is a downcrossing from r r r to r − 1 r-1 r − 1 . The number of upcrossings must equal the number of downcrossings, so the factors of the numerator and denominator are the same. Hence, ∣ det ( ∂ D / ∂ A ) ∣ = 1 |\det(\partial D/\partial A)|=1 ∣ det ( ∂ D / ∂ A ) ∣ = 1 almost everywhere, and so Φ \Phi Φ is measure-preserving. Because the arrivals are u.i.d. and this is preserved by Φ \Phi Φ , the departures are as well.
Combining time reversal with Lemma B.3 now turns the arrival observation into a departure observation.
Corollary B.4. In a PS schedule for a hash-table queue, the number of other jobs present immediately before a job x x x departs has the same distribution as the number of jobs in the system without x x x at a uniformly random time.
Proof. This follows immediately from Lemma B.3 and Observations B.1 and B.2.
To bound sojourn times, we need to relate the population throughout a job’s service to the population of the system without that job. For PS, truncating a job x x x to size v v v leaves its service history unchanged until it has received v v v units of service, turning that instant into a departure. Applying Corollary B.4 to this truncated job and integrating over v v v gives the following identity.
Lemma B.5 (Service-Deletion Identity). Let x x x be a tagged job in a hash-table queue with a PS schedule. Let χ x ( t ) \chi_x(t) χ x ( t ) be the indicator that x x x is in service at time t t t and M x ( t ) = M x S ( t ) = N ( t ) − χ x ( t ) M_x(t) = M_x^S(t) = N(t) - \chi_x(t) M x ( t ) = M x S ( t ) = N ( t ) − χ x ( t ) be the number of jobs present other than x x x . For every function g : N → R g : \mathbb{N} \to\mathbb{R} g : N → R we have
E [ ∫ 0 m χ x ( t ) g ( M x ( t ) ) N ( t ) d t ] = ∣ x ∣ m E [ ∫ 0 m g ( N S ∖ { x } ( t ) ) d t ] . (6) \mathbb{E}\left[\int_0^m \chi_x(t)\frac{g(M_x(t))}{N(t)}\,dt\right] = \frac{|x|}{m}\mathbb{E}\left[\int_0^m g\left(N^{S\setminus\{x\}}(t)\right)\,dt\right]. \tag*{(6)} E [ ∫ 0 m χ x ( t ) N ( t ) g ( M x ( t )) d t ] = m ∣ x ∣ E [ ∫ 0 m g ( N S ∖ { x } ( t ) ) d t ] . (6)
We use Lemma B.5 in two ways. Taking g ( i ) = i + 1 g(i) = i + 1 g ( i ) = i + 1 makes the left-hand side of Equation (B.2) equal to the expected sojourn time of x x x . Taking g ( i ) = 1 { i ≥ k } g(i) = \mathbb{1}\{i \ge k\} g ( i ) = 1 { i ≥ k } makes it the expected amount of service that x x x receives while at least k k k other jobs are present. Summing this second identity over x x x will give a tail bound on the number of jobs in the system.
Proof. We will change variables in the integral so that we integrate against the service time attained by x x x . Define the service time
C x ( t ) = ∫ A x t d u N ( u ) . C_x(t) = \int_{A_x}^{t} \frac{du}{N(u)}. C x ( t ) = ∫ A x t N ( u ) d u .
For v ∈ ( 0 , ∣ x ∣ ) v \in(0, |x|) v ∈ ( 0 , ∣ x ∣ ) , define T x ( v ) T_x(v) T x ( v ) to be the unique time at which C x ( T x ( v ) ) = v C_x(T_x(v)) = v C x ( T x ( v )) = v . We make the change of variables v = C x ( t ) v = C_x(t) v = C x ( t ) , for which d v = d t N ( t ) dv = \frac{dt}{N(t)} d v = N ( t ) d t . Then we have
∫ 0 m χ x ( t ) g ( M x ( t ) ) N ( t ) d t = ∫ 0 ∣ x ∣ g ( M x ( T x ( v ) ) ) d v . \int_0^m \chi_x(t)\frac{g(M_x(t))}{N(t)}\,dt = \int_0^{|x|} g\left(M_x(T_x(v))\right)\,dv. ∫ 0 m χ x ( t ) N ( t ) g ( M x ( t )) d t = ∫ 0 ∣ x ∣ g ( M x ( T x ( v )) ) d v .
Now fix any v ∈ ( 0 , ∣ x ∣ ) v \in(0, |x|) v ∈ ( 0 , ∣ x ∣ ) and consider a new system S v S_v S v where x x x is replaced with the truncated job x v x_v x v of size v v v . The systems are identical up until T x ( v ) T_x(v) T x ( v ) , when x v x_v x v departs from S v S_v S v . Therefore,
M x S ( T x ( v ) − ) = M x v S v ( D x v − ) . M_x^S(T_x(v)-) = M_{x_v}^{S_v}(D_{x_v}-). M x S ( T x ( v ) − ) = M x v S v ( D x v − ) .
Now, applying Corollary B.4 to x v x_v x v in S v S_v S v shows that M x v S v ( D x v − ) M_{x_v}^{S_v}(D_{x_v}-) M x v S v ( D x v − ) has the same distribution as N S ∖ { x } ( u ) N^{S\setminus\{x\}}(u) N S ∖ { x } ( u ) where u u u is a uniformly random time. Taking expectations and integrating gives
E [ ∫ 0 m χ x ( t ) g ( M x ( t ) ) N ( t ) d t ] = ∫ 0 ∣ x ∣ E [ g ( N S ∖ { x } ( u ) ) ] d v = ∣ x ∣ m E [ ∫ 0 m g ( N S ∖ { x } ( t ) ) d t ] . \mathbb{E}\left[\int_0^m \chi_x(t)\frac{g(M_x(t))}{N(t)}\,dt\right] = \int_0^{|x|}\mathbb{E}\left[g\left(N^{S\setminus\{x\}}(u)\right)\right]\,dv = \frac{|x|}{m}\mathbb{E}\left[\int_0^m g\left(N^{S\setminus\{x\}}(t)\right)\,dt\right]. E [ ∫ 0 m χ x ( t ) N ( t ) g ( M x ( t )) d t ] = ∫ 0 ∣ x ∣ E [ g ( N S ∖ { x } ( u ) ) ] d v = m ∣ x ∣ E [ ∫ 0 m g ( N S ∖ { x } ( t ) ) d t ] .
We now can prove bounds on probability there are at least a given number of jobs present at a random time with a recursion derived from summing the service-deletion identity over jobs. The resulting upper bound mirrors the geometric stationary population law of M/G/1-PS queues, whose tail at load ρ \rho ρ is ρ k \rho^k ρ k , independently of the service-size distribution [28 Chapter 22] .
Lemma B.6. Consider a PS schedule for hash-table queue for a set S S S with load ( S ) ≤ α \operatorname{load}(S) \le\alpha load ( S ) ≤ α . For a uniformly random time u u u , let H S ( k ) = Pr [ N S ( u ) ≥ k ] H_S(k) = \Pr[N^S(u) \ge k] H S ( k ) = Pr [ N S ( u ) ≥ k ] . Then, for every integer k ≥ 0 k \ge0 k ≥ 0 ,
H S ( k ) ≤ α k and E [ N S ( u ) ] ≤ α 1 − α . H_S(k) \le\alpha^k \qquad\text{and} \qquad\mathbb{E}[N^S(u)] \le\frac{\alpha}{1-\alpha}. H S ( k ) ≤ α k and E [ N S ( u )] ≤ 1 − α α .
Proof. For each subset X ⊆ S X \subseteq S X ⊆ S , write α X = 1 m ∑ x ∈ X ∣ x ∣ \alpha_X = \frac{1}{m}\sum_{x\in X}|x| α X = m 1 ∑ x ∈ X ∣ x ∣ . We prove H X ( k ) ≤ α X k H_X(k) \le\alpha_X^k H X ( k ) ≤ α X k simultaneously for all X ⊆ S X \subseteq S X ⊆ S by induction on k k k . The base case k = 0 k=0 k = 0 holds since H X ( 0 ) = 1 H_X(0)=1 H X ( 0 ) = 1 . Whenever N X ( t ) ≥ k + 1 N_X(t) \ge k+1 N X ( t ) ≥ k + 1 , each active job contributes 1 / N X ( t ) 1/N_X(t) 1/ N X ( t ) to the indicator of this event. Thus
1 { N X ( t ) ≥ k + 1 } = ∑ x ∈ X χ x X ( t ) 1 { M x X ( t ) ≥ k } N X ( t ) . \mathbf{1}\{N_X(t) \ge k+1\} = \sum_{x\in X}\chi_x^X(t)\frac{\mathbf{1}\{M_x^X(t)\ge k\}}{N_X(t)}. 1 { N X ( t ) ≥ k + 1 } = x ∈ X ∑ χ x X ( t ) N X ( t ) 1 { M x X ( t ) ≥ k } .
where a summand is taken to be zero when x x x is inactive. Integrating, applying Lemma B.5 with g ( i ) = 1 { i ≥ k } g(i)=\mathbf{1}\{i\ge k\} g ( i ) = 1 { i ≥ k } , and using the induction hypothesis gives
H X ( k + 1 ) = ∑ x ∈ X ∣ x ∣ m H X ∖ { x } ( k ) ≤ ∑ x ∈ X ∣ x ∣ m α X ∖ { x } k ≤ α X k + 1 . H_X(k+1)=\sum_{x\in X}\frac{|x|}{m}H_{X\setminus\{x\}}(k)\le\sum_{x\in X}\frac{|x|}{m}\alpha_{X\setminus\{x\}}^k\le\alpha_X^{k+1}. H X ( k + 1 ) = x ∈ X ∑ m ∣ x ∣ H X ∖ { x } ( k ) ≤ x ∈ X ∑ m ∣ x ∣ α X ∖ { x } k ≤ α X k + 1 .
This completes the induction. Finally,
E [ N S ( u ) ] = ∑ k ≥ 1 H S ( k ) ≤ ∑ k ≥ 1 α k = α 1 − α . \mathbb{E}[N_S(u)]=\sum_{k\ge1}H_S(k)\le\sum_{k\ge1}\alpha^k=\frac{\alpha}{1-\alpha}. E [ N S ( u )] = k ≥ 1 ∑ H S ( k ) ≤ k ≥ 1 ∑ α k = 1 − α α .
Next, we translate the bounds of the number of jobs present into stretch bounds. The service-deletion identity makes the expected stretch of a tagged item equal to one plus the expected population without that item, yielding the same 1 / ( 1 − ρ ) 1/(1-\rho) 1/ ( 1 − ρ ) dependence on load as in M/G/1-PS [28 Section 30.1] . For the maximum stretch, every job’s service rate is at least the reciprocal of the maximum population. The population can increase only at arrivals, so a union bound over the n n n arrivals supplies the additional logarithmic factor.
Theorem 7.4. Let S S S be a nonempty set of n n n items with load ( S ) = α ≤ 1 − ε \operatorname{load}(S)=\alpha\le1-\varepsilon load ( S ) = α ≤ 1 − ε . In the PS schedule for S S S in the hash-table queue, each item has expected stretch at most 1 / ε 1/\varepsilon 1/ ε , and the expected maximum stretch is at most ( ln n + 1 ) / ε (\ln n+1)/\varepsilon ( ln n + 1 ) / ε .
Proof. Write T x = D x P S − A x T_x=D_x^{\mathrm{PS}}-A_x T x = D x PS − A x for the sojourn time of x x x . Applying Lemma B.5 with g ( i ) = i + 1 g(i)=i+1 g ( i ) = i + 1 and then Lemma B.6, we obtain
E [ T x ] = ∣ x ∣ ⋅ E [ N S ∖ { x } ( u ) + 1 ] ≤ ∣ x ∣ ( α 1 − α + 1 ) ≤ ∣ x ∣ ε . \mathbb{E}[T_x]=|x|\cdot\mathbb{E}[N_{S\setminus\{x\}}(u)+1]\le|x|\left(\frac{\alpha}{1-\alpha}+1\right)\le\frac{|x|}{\varepsilon}. E [ T x ] = ∣ x ∣ ⋅ E [ N S ∖ { x } ( u ) + 1 ] ≤ ∣ x ∣ ( 1 − α α + 1 ) ≤ ε ∣ x ∣ .
Let K = max t N S ( t ) K=\max_t N_S(t) K = max t N S ( t ) . Since every active item receives service at rate at least 1 / K 1/K 1/ K , we have T x / ∣ x ∣ ≤ K T_x/|x|\le K T x /∣ x ∣ ≤ K for every x x x . The population increases only at arrivals. By Observation B.1 and Lemma B.6, for every integer k ≥ 0 k\ge0 k ≥ 0 ,
Pr [ K ≥ k + 1 ] ≤ ∑ x ∈ S Pr [ N S ∖ { x } ( u ) ≥ k ] ≤ n α k ≤ n e − ε k . \Pr[K\ge k+1]\le\sum_{x\in S}\Pr[N_{S\setminus\{x\}}(u)\ge k]\le n\alpha^k\le ne^{-\varepsilon k}. Pr [ K ≥ k + 1 ] ≤ x ∈ S ∑ Pr [ N S ∖ { x } ( u ) ≥ k ] ≤ n α k ≤ n e − ε k .
Since u ↦ min { n , 1 / u } u\mapsto\min\{n,1/u\} u ↦ min { n , 1/ u } is nonincreasing, integration over the intervals [ α k + 1 , α k ] [\alpha^{k+1},\alpha^k] [ α k + 1 , α k ] gives
( 1 − α ) ∑ k ≥ 0 min { 1 , n α k } ≤ ∫ 0 1 min { n , 1 / u } d u = ln n + 1. (1-\alpha)\sum_{k\ge0}\min\{1,n\alpha^k\}\le\int_0^1\min\{n,1/u\}\,du=\ln n+1. ( 1 − α ) k ≥ 0 ∑ min { 1 , n α k } ≤ ∫ 0 1 min { n , 1/ u } d u = ln n + 1.
Consequently,
E [ max x ∈ S T x ∣ x ∣ ] ≤ E [ K ] ≤ ∑ k ≥ 0 min { 1 , n α k } ≤ ln n + 1 1 − α ≤ ln n + 1 ε . \mathbb{E}\left[\max_{x\in S}\frac{T_x}{|x|}\right]\le\mathbb{E}[K]\le\sum_{k\ge0}\min\{1,n\alpha^k\}\le\frac{\ln n+1}{1-\alpha}\le\frac{\ln n+1}{\varepsilon}. E [ x ∈ S max ∣ x ∣ T x ] ≤ E [ K ] ≤ k ≥ 0 ∑ min { 1 , n α k } ≤ 1 − α ln n + 1 ≤ ε ln n + 1 .
Supporting Deletions in the Low-Load Structure
In this appendix, we prove Lemma 8.1, extending the low-load structure of Section 6 to support deletions. The goal is to preserve its scan bounds: O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) for positive queries, O ( 1 ) O(1) O ( 1 ) for negative queries, and O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) for insertions and deletions, with positive queries and updates analyzed in amortized expectation. As in the main text, the primary table H \mathcal{H} H , membership table M \mathcal{M} M , and backup table B \mathcal{B} B each have m m m cells and are interleaved in a single physical array. They are initially empty, and the total volume of insertions is at most m / 64 m/64 m /64 , including items subsequently deleted.
Why Deletion Marks Alone Do Not Suffice. The simplest approach would be to mark an item as deleted and otherwise leave it in place. This preserves the placement machinery, but repeated deletions and reinsertions of one key would create many items with the same hash. For example, repeatedly inserting key k k k with values of slightly increasing lengths could leave many copies near c ( k ) c(k) c ( k ) , even within a single size class. The analyses of Sections 5 and 6 rely on independence across keys and cannot treat these copies as independent items.
Instead, we keep at most one copy of each key in each size class, and reuse that copy whenever the key is reinserted into the same class. As we describe below, the insertion algorithm will enforce this property by searching all levels of the relevant size class for an existing copy before creating a new one. To make this possible, we also replace the exact-length priority order by an order on size classes. A copy’s stored length may then increase within its class without changing its priority.
Copies, Size Classes, and Metadata. The size class of a length s ≥ 1 s \ge1 s ≥ 1 is i ( s ) = ⌊ log s ⌋ i(s)=\lfloor\log s\rfloor i ( s ) = ⌊ log s ⌋ . Each copy y y y has a key k ( y ) k(y) k ( y ) and a stored length ∣ y ∣ |y| ∣ y ∣ , and holds an item of that key padded to length ∣ y ∣ |y| ∣ y ∣ . We assume that the original item length can be read from its encoding, so padding can be removed after decoding. A copy is either live or dead ; the live copies represent exactly the current set S S S .
Each key k k k ever inserted has one permanent membership record, created on its first insertion. Its position in M \mathcal{M} M is the identifier ID ( k ) \operatorname{ID}(k) ID ( k ) shared by all copies of k k k . We write ID ( y ) = ID ( k ( y ) ) \operatorname{ID}(y)=\operatorname{ID}(k(y)) ID ( y ) = ID ( k ( y )) and use the priority order
y ≻ z ⟺ i ( ∣ y ∣ ) < i ( ∣ z ∣ ) , or i ( ∣ y ∣ ) = i ( ∣ z ∣ ) and ID ( y ) < ID ( z ) . y \succ z \Longleftrightarrow i(|y|)<i(|z|),\qquad\text{or}\qquad i(|y|)=i(|z|)\text{ and }\operatorname{ID}(y)<\operatorname{ID}(z). y ≻ z ⟺ i ( ∣ y ∣ ) < i ( ∣ z ∣ ) , or i ( ∣ y ∣ ) = i ( ∣ z ∣ ) and ID ( y ) < ID ( z ) .
Since there is at most one copy per key and size class, this is a strict total order on copies. Identifiers and size classes never change, so neither do priorities.
Primary-cell metadata is as in Section 5: the stored length, codeword index, and owner identifier. A cell’s identifier and stored length together identify its copy and determine its priority. As in Section 6.3, the backup table stores items in chunks. Deleting a copy does not free its chunk, allowing the chunk to be reused if the copy is later resurrected. Each occupied chunk also records its copy’s identifier, stored length, and a live/dead bit, using O ( 1 ) O(1) O ( 1 ) words of metadata. A membership record stores ( ϕ ( k ) , F , s ) (\phi(k),F,s) ( ϕ ( k ) , F , s ) , as in Section 6.1, together with a live/dead bit. Here s s s is the stored length of the most recently live copy of k k k , and F F F is the endpoint of its most recent placement in H \mathcal{H} H . Deletion does not clear cells or free chunks. Dead copies continue to participate in the placement rules, but are never reinflated.
Searching the Backup Table. Unlike the insertion-only structure, we sometimes need to search for a copy that does not exist. To search class i i i for key k k k , search each level of that class from the home chunk of k k k until finding the desired copy or an empty chunk. These searches proceed together in a single forward pass from γ ( k ) \gamma(k) γ ( k ) , the cell of B \mathcal{B} B in c ( k ) c(k) c ( k ) . Dead chunks count as occupied. Within a class, keys are distinct and chunks never move, so the usual linear-probing stopping rule remains valid. We use this search both to locate live copies and to find dead copies for reuse. We fix the buddy allocator’s choices independently of the hashes; since requests depend only on the numbers of new copies in each class, the allocated subarrays do also.
Operations.
Query (k k k ): Scan M \mathcal{M} M from c ( k ) c(k) c ( k ) to its first empty cell, collecting live records with fingerprint ϕ ( k ) \phi(k) ϕ ( k ) . For a candidate ( ϕ ( k ) , F , s ) (\phi(k),F,s) ( ϕ ( k ) , F , s ) at position p p p , scan H \mathcal{H} H from c ( k ) c(k) c ( k ) through F F F , collecting only symbols whose headers have identifier p p p and stored length s s s . If at least s s s symbols survive, decode the padded item and compare its key with k k k . Otherwise, search the backup levels of class i ( s ) i(s) i ( s ) for the copy with identifier p p p . Reject the candidate if no such copy is found or its key differs from k k k . If the backup search finds the matching item, reinflate it in H \mathcal{H} H , keeping its identifier and stored length, and update F F F in its membership record. Return the matching item with its padding removed, or Not Found if no candidate matches. In particular, a negative query whose only matching record is dead stops after the membership scan; it never decodes a dead copy of k k k .
Insert (x x x ), for an item of key k k k , length s = ∣ x ∣ s=|x| s = ∣ x ∣ , and class i = i ( s ) i=i(s) i = i ( s ) :
(1) Scan M \mathcal{M} M from c ( k ) c(k) c ( k ) to its first empty cell. For each matching fingerprint record ( ϕ ( k ) , F , s 0 ) (\phi(k),F,s_0) ( ϕ ( k ) , F , s 0 ) at position p 0 p_0 p 0 , search the backup levels of class i ( s 0 ) i(s_0) i ( s 0 ) for the copy with identifier p 0 p_0 p 0 , whether live or dead. A found copy whose key is k k k identifies the permanent record of k k k ; otherwise reject the candidate. Let p p p be the record’s position, or the first empty membership cell if k k k has no record. Set ID ( k ) = p \operatorname{ID}(k)=p ID ( k ) = p .
(2) Search the backup levels of class i i i for key k k k . Any copy found is dead, since k k k is absent.
(3) If a copy y y y is found, resurrect it: set s ′ = max { s , ∣ y ∣ } s'=\max\{s,|y|\} s ′ = max { s , ∣ y ∣ } , pad x x x to length s ′ s' s ′ , and overwrite its chunk with the padded item, marking the chunk live. The item fits in the same chunk, since s ′ < 2 i + 1 s'<2^{i+1} s ′ < 2 i + 1 and a class-i i i chunk has 2 i + 2 2^{i+2} 2 i + 2 cells. In H \mathcal{H} H , write its fresh codeword into the first 2 s ′ 2s' 2 s ′ cells available to y y y at or after c ( k ) c(k) c ( k ) , as in Section 5. Its identifier and class, and hence its priority, are unchanged. If no copy is found, set s ′ = s s'=s s ′ = s and insert a new copy into H \mathcal{H} H and the newest backup level of class i i i , using identifier p p p .
(4) Write the live record ( ϕ ( k ) , F , s ′ ) (\phi(k),F,s') ( ϕ ( k ) , F , s ′ ) at position p p p in M \mathcal{M} M , where F F F is the endpoint of the placement in step (3). If a record already existed, overwrite it in place.
For completeness, if more than one membership record matches the fingerprint, we may instead scan all chunks to identify the key and its record exactly, and then perform the requested operation. Chunk keys can be compared with k k k as they are read, keeping one partial comparison per allocated level. This uses O ( log 2 m ) O(\log^2 m) O ( log 2 m ) bookkeeping words and a constant number of full forward passes. This exceptional scan will have negligible expected cost, as in Lemma 6.1.
All scans start at c ( k ) c(k) c ( k ) and proceed forward. Returning to a membership record to update it, or to c ( k ) c(k) c ( k ) for a reinflation, requires only another forward pass over the same interval. Thus each operation makes O ( 1 ) O(1) O ( 1 ) forward passes over one contiguous physical interval starting at c ( k ) c(k) c ( k ) . Since the three tables are interleaved at constant stride, it suffices to bound the lengths scanned in the constituent tables.
Lemma C.1 (Basic Invariants). At all times:
(1) Each key has at most one copy in each size class and at most one membership record. At most one of its copies is live.
(2) Stored lengths never decrease. A copy of class i i i has stored length in [ 2 i , 2 i + 1 ) [2^i,2^{i+1}) [ 2 i , 2 i + 1 ) , and hence less than twice the length of every item inserted into that copy.
(3) The sum of the stored lengths of all copies, including dead copies, is at most the total insertion volume, and hence at most m / 64 m/64 m /64 .
(4) The copies, their stored lengths and live/dead states, and the keys with membership records are determined by the operation sequence independently of the hashes.
Proof. An insertion creates a copy only if its class contains none of that key, and creates a membership record only if the key has never had one. Existing records are always found: they never move or disappear, and lie before the first empty cell after their canonical cell. Since only absent keys are inserted, the new or resurrected copy is the key’s only live copy. A resurrection takes the maximum of the old and new lengths, both in the same size class, proving (2). Each copy’s stored length is the maximum of the lengths inserted into it, and different copies receive disjoint sets of insertions, proving (3). Finally, whether an insertion creates or resurrects a copy depends only on the key and size class of earlier insertions; deletions determine which copy becomes dead. This proves (4).
Lemma C.2 (Prefix Occupancy with Resurrections). Lemmas 5.1 and 5.2 continue to hold, with item lengths replaced by current stored lengths. After a resurrection of y y y , every primary cell naming y y y holds a symbol of its current codeword.
Proof. The priority order is fixed, cells are never cleared, and every placement writes into the first available cells from the copy’s canonical cell. Thus a cell’s owner can only increase in priority, and available sets only shrink. The inductive proof of prefix occupancy applies as before. The only additional case is a resurrection increasing a copy’s stored length from s s s to s ′ ≥ s s' \ge s s ′ ≥ s . Its surviving symbols form a prefix of at most 2 s 2s 2 s available cells, so all are among the first 2 s ′ 2s' 2 s ′ available cells overwritten by the new placement. No symbols of the previous contents remain, and the new symbols again occupy a prefix of the available cells.
Lemma C.3 (Scan Lengths in the Primary Table). Let x x x be a copy of class i i i . There is a random variable L x max L_x^{\max} L x m a x bounding the primary-table scan of every query, insertion, resurrection, or reinflation of x x x over the entire sequence, with
E [ ( L x max ) 2 ] = O ( 4 i ) and E [ L x max ] = O ( 2 i ) . \mathbb{E}\left[\left(L_x^{\max}\right)^2\right] = O\left(4^i\right) \qquad\text{and} \qquad\mathbb{E}\left[L_x^{\max}\right] = O\left(2^i\right). E [ ( L x m a x ) 2 ] = O ( 4 i ) and E [ L x m a x ] = O ( 2 i ) . Proof. Let s s s be the final stored length of x x x , and let Y Y Y contain all copies of class at most i i i at the end of the sequence, giving each copy y y y weight 2 ∣ y ∣ 2^{|y|} 2 ∣ y ∣ . Place this weight by scanning left to right, occupying a cell whenever some copy that has arrived has unplaced weight. The occupied set depends only on the total weight hashing to each cell. Define L x max L_x^{\max} L x m a x as the number of cells from c ( x ) c(x) c ( x ) through the 2 s 2s 2 s -th empty cell of this placement.
At every earlier time, every copy of higher priority than x x x belongs to Y Y Y and has at most its final weight. By prefix occupancy and the occupancy domination argument of Lemma 5.3 , its occupied cells are contained in the occupied set of Y Y Y . Thus every placement of x x x finishes by the 2 s 2s 2 s -th empty cell of that set. A query stops no later than the endpoint of x x x ’s most recent placement, so the same bound applies to queries.
Copies of one key share a hash. To recover independence, merge all copies of Y Y Y with a given key into one weighted item at that key’s canonical cell. By Lemma C.1(1), its weight is less than
∑ j = 0 i 2 ⋅ 2 j + 1 < 2 i + 3 . (7) \sum_{j=0}^{i} 2 \cdot2^{j+1} < 2^{i+3}. \tag*{(7)} j = 0 ∑ i 2 ⋅ 2 j + 1 < 2 i + 3 . (7)
Merging preserves the arrival weight at every cell, hence the occupied set. The merged items have distinct keys and independent hashes, and their weights are fixed independently of those hashes by Lemma C.1(4). Their total weight is at most m / 32 m/32 m /32 .
Now add 2 s 2s 2 s to the merged item with key k ( x ) k(x) k ( x ) . The additional words fill the first 2 s 2s 2 s previously empty cells at or after c ( x ) c(x) c ( x ) , so the resulting run containing c ( x ) c(x) c ( x ) has length at least L x max L_x^{\max} L x m a x . Every merged item still has weight O ( 2 i ) O(2^i) O ( 2 i ) , and the total weight is at most m / 32 + 2 s ≤ m / 16 m/32+2s\le m/16 m /32 + 2 s ≤ m /16 . The run-length argument of Lemma 4.2 , using blocks of O ( 2 i ) O(2^i) O ( 2 i ) cells and conditioning on c ( x ) c(x) c ( x ) , therefore gives a second moment of O ( 4 i ) O(4^i) O ( 4 i ) . It uses only the bounded item weights, independence of the other keys’ hashes, and the fact that every word of a run hashes into that run. This bounds E [ ( L x max ) 2 ] \mathbb{E}[(L_x^{\max})^2] E [( L x m a x ) 2 ] ; the expectation bound follows by Jensen’s inequality.
Lemma C.4 (Membership and Backup Scan Bounds). (1) For each key k k k , all membership scans from c ( k ) c(k) c ( k ) through the first empty cell are bounded by one random variable with O ( 1 ) O(1) O ( 1 ) expectation and second moment.
(2) The backup density bound of Lemma 6.4 continues to hold. For each key k k k and class i i i , all searches of that class and all writes of a copy of k k k in that class have scan length at most Λ k , i \Lambda_{k,i} Λ k , i from γ ( k ) \gamma(k) γ ( k ) , where
E [ Λ k , i 2 ] = O ( 2 i log 1 + δ 2 i ) . \mathbb{E}[\Lambda_{k,i}^2]=O\left(2^i\log^{1+\delta}2^i\right). E [ Λ k , i 2 ] = O ( 2 i log 1 + δ 2 i ) .
Proof. For (1), membership records form a unit-size linear-probing table with one permanent record per key ever inserted. There are at most m / 64 m/64 m /64 such keys, and their set is independent of the hashes. The final table dominates every earlier membership scan, since records are never moved or removed. Applying the run-length argument of Lemma 4.2 with unit weights gives both moment bounds, whether or not k k k has a record.
For (2), each chunk of class i i i holds a copy of stored length at least 2 i 2^i 2 i . The total stored length, including dead copies, is at most m / 64 m/64 m /64 . Thus the proof of Lemma 6.4 applies with copies in place of items. Resurrections use existing chunks and do not allocate levels.
For the scan bound, let J ℓ J_\ell J ℓ be the number of chunks from the home chunk of k k k through the first empty chunk in the final state of level ℓ \ell ℓ of class i i i . The level contains copies with distinct keys and independent home chunks. Which copies enter the level depends only on the operation sequence: allocation and insertion into a new level depend on the number of new copies, while resurrections rewrite chunks in place. The level is at most half full. Hence the unit-weight argument of Lemma 6.5 gives E [ J ℓ 2 ] = O ( 1 ) \mathbb{E}[J_\ell^2]=O(1) E [ J ℓ 2 ] = O ( 1 ) , including when k k k itself has a copy in the level.
Chunks are never freed, so J ℓ J_\ell J ℓ dominates the corresponding search length at every earlier time. A search reads at most J ℓ J_\ell J ℓ chunks of level ℓ \ell ℓ , all within distance ( J ℓ + 1 ) 2 i + 2 / D i , ℓ (J_\ell+1)2^{i+2}/D_{i,\ell} ( J ℓ + 1 ) 2 i + 2 / D i , ℓ of γ ( k ) \gamma(k) γ ( k ) . The same bound covers writing a new chunk or rewriting a resurrected one. Consequently, we may take
Λ k , i = ∑ ℓ ( J ℓ + 1 ) 2 i + 2 D i , ℓ . \Lambda_{k,i}=\sum_\ell(J_\ell+1)\frac{2^{i+2}}{D_{i,\ell}}. Λ k , i = ℓ ∑ ( J ℓ + 1 ) D i , ℓ 2 i + 2 .
where the sum is over the final levels of class i i i . Since D i , ℓ ≥ 2 ℓ d i D_{i,\ell}\ge2^\ell d_i D i , ℓ ≥ 2 ℓ d i , Minkowski’s inequality yields
E [ Λ k , i 2 ] ≤ ∑ ℓ O ( 2 i + 2 2 ℓ d i ) = O ( 2 i / d i ) = O ( 2 i log 1 + δ 2 i ) . \sqrt{\mathbb{E}[\Lambda_{k,i}^2]}\le\sum_\ell O\left(\frac{2^{i+2}}{2^\ell d_i}\right)=O(2^i/d_i)=O(2^i\log^{1+\delta}2^i). E [ Λ k , i 2 ] ≤ ℓ ∑ O ( 2 ℓ d i 2 i + 2 ) = O ( 2 i / d i ) = O ( 2 i log 1 + δ 2 i ) .
This proves the claimed bound uniformly over the operation sequence, including searches for absent copies.
Proof of Lemma 8.1 . We first bound scan costs. An item x x x held in a class-i i i copy has 2 i ≤ ∣ x ∣ 2^i \le|x| 2 i ≤ ∣ x ∣ , and that copy’s stored length is less than 2 ∣ x ∣ 2|x| 2∣ x ∣ . Thus bounds in terms of 2 i 2^i 2 i give the desired bounds in terms of the item length. Fingerprint collisions contribute only O ( 1 ) O(1) O ( 1 ) expected scan cost per operation, by the same argument as Lemma 6.1 : fingerprints are independent of the placement hash, a collision with any record has probability at most m 1 − C m^{1-C} m 1 − C , and even scanning the entire array costs only O ( m ) O(m) O ( m ) . We omit this contribution below.
Queries. A negative query scans only M \mathcal{M} M , with expected scan cost O ( 1 ) O(1) O ( 1 ) by Lemma C.4 (1). A positive query for a class-i i i copy x x x scans O ( 1 ) O(1) O ( 1 ) expected membership cells and at most L x max L_x^{\max} L x m a x primary cells, for expected scan cost O ( 2 i ) O(2^i) O ( 2 i ) . As in Lemma 6.2 , we charge any backup lookup and lazy reinflation to the insertion that first created this copy.
To justify this charge across deletions and resurrections, let K x K_x K x be the total number of lazy reinflations of x x x . A fallback occurs only when more than the current stored length, and hence at least 2 i 2^i 2 i , of its symbols have been overwritten since its most recent placement. By prefix occupancy, the next placement extends its endpoint by at least 2 i 2^i 2 i cells. Resurrections cannot move the endpoint to the left: the available set only shrinks, and the number of symbols to be placed never decreases. Every endpoint is bounded by L x max L_x^{\max} L x m a x , so
K x ≤ L x max 2 i . K_x \le\frac{L_x^{\max}}{2^i}. K x ≤ 2 i L x m a x .
After accounting for the initial membership scan, each fallback and reinflation contributes at most O ( 1 + Λ k ( x ) , i + L x max ) O(1+\Lambda_{k(x),i}+L_x^{\max}) O ( 1 + Λ k ( x ) , i + L x m a x ) additional scan cost. Updating the membership record accesses only the interval already scanned. By Cauchy–Schwarz and Lemmas C.3 and C.4 , the total expected scan cost charged to this copy is at most
O ( E [ K x ( 1 + Λ k ( x ) , i + L x max ) ] ) ≤ O ( 1 ) 2 i ( E [ L x max ] + E [ ( L x max ) 2 ] E [ Λ k ( x ) , i 2 ] + E [ ( L x max ) 2 ] ) = O ( 2 i log 1 + δ 2 i ) . \begin{aligned}
O\!\left(\mathbb{E}\left[K_x(1+\Lambda_{k(x),i}+L_x^{\max})\right]\right)
\le\frac{O(1)}{2^i}\left(\mathbb{E}[L_x^{\max}]+\sqrt{\mathbb{E}[(L_x^{\max})^2]\mathbb{E}[\Lambda_{k(x),i}^2]}+\mathbb{E}[(L_x^{\max})^2]\right) \\
&=O(2^i\log^{1+\delta}2^i).
\end{aligned} O ( E [ K x ( 1 + Λ k ( x ) , i + L x m a x ) ] ) ≤ 2 i O ( 1 ) ( E [ L x m a x ] + E [( L x m a x ) 2 ] E [ Λ k ( x ) , i 2 ] + E [( L x m a x ) 2 ] ) = O ( 2 i log 1 + δ 2 i ) .
The insertion that created the copy had length at least 2 i 2^i 2 i , so it pays this charge once, including all later resurrections. Dead copies are never reinflated. This leaves positive queries with amortized expected scan cost O ( ∣ x ∣ ) O(|x|) O ( ∣ x ∣ ) .
Insertions. Consider an insertion of length ∣ x ∣ |x| ∣ x ∣ in class i i i . Step (1) scans O ( 1 ) O(1) O ( 1 ) expected membership cells. If the key already has a record of stored length in class i 0 i_0 i 0 , verifying that record costs at most Λ k , i 0 \Lambda_{k,i_0} Λ k , i 0 in the backup table. We charge this verification to the preceding insertion of k k k , whose length was at least 2 i 0 2^{i_0} 2 i 0 . That insertion receives at most one such charge, since the next insertion overwrites the record; intervening queries change only its endpoint field. Step (2) costs at most Λ k , i \Lambda_{k,i} Λ k , i . Step (3) scans at most L y max L_y^{\max} L y m a x primary cells for the new or resurrected copy y y y , and at most Λ k , i \Lambda_{k,i} Λ k , i backup cells. Step (4) updates the record within the membership interval already scanned. These bounds and the charges from lazy reinflations give amortized expected scan cost O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) per insertion.
Deletions. Deleting an item x x x consists of a query and one additional backup search in its size class, followed by updates to the two live/dead bits. The bit updates lie in the intervals already scanned. The additional expected scan cost is O ( 2 i log 1 + δ 2 i ) O(2^i\log^{1+\delta}2^i) O ( 2 i log 1 + δ 2 i ) , so the amortized expected deletion cost is O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) O(|x|\log^{1+\delta}|x|) O ( ∣ x ∣ log 1 + δ ∣ x ∣ ) .
Finally, all accesses make O ( 1 ) O(1) O ( 1 ) forward passes over a contiguous interval starting at c ( k ) c(k) c ( k ) . Under the assumptions of Corollary 6.6 , the scan property converts these scan bounds to the corresponding 1 + O ( L / B ) 1+O(L/B) 1 + O ( L / B ) I/O bounds stated in Lemma 8.1 .
Rebuilding in Graveyard Catalan Hashing
In this section, we prove bounds on the amortized cost of rebuilds in graveyard Catalan hashing from Section 8. We do this by arguing that rebuilds can be accomplished locally and with O ( 1 ) O(1) O ( 1 ) passes over the data structure.
Why Rebuilding Is Local
A rebuild removes the flagged items, integrates the live items of C s m a l l \mathcal{C}_{\mathrm{small}} C small into H f u l l \mathcal{H}_{\mathrm{full}} H full , and empties both auxiliary structures. Rebuilding by sorting all live items in external memory would be too expensive. Instead, we will rebuild independent intervals of the table inside the cache.
Let T T T be the set of items in the old static table, including flagged items, and let Y Y Y be the live set of the insertion table. For this argument, keep both copies when a key occurs in both sets, and write U = T ⊎ Y U = T \uplus Y U = T ⊎ Y . The new live set is S ⊆ U S \subseteq U S ⊆ U , obtained by removing the flagged items. The budget and space bounds imply
Vol ( U ) ≤ ( 1 − ε / 2 ) m + ε ′ m / 64 ≤ ( 1 − ε / 4 ) m . \operatorname{Vol}(U) \le(1-\varepsilon/2)m+\varepsilon' m/64 \le(1-\varepsilon/4)m. Vol ( U ) ≤ ( 1 − ε /2 ) m + ε ′ m /64 ≤ ( 1 − ε /4 ) m .
Let P U P_U P U be any placement of U U U that scans from left to right, placing one word whenever an arrived item still has unplaced words. The occupied cells depend only on the total volume hashing to each cell, not on which pending item is chosen. Call its maximal occupied intervals the U U U -runs.
Claim D.1 (Rebuild Locality). For each U U U -run R R R , the old static items stored in R R R are exactly the items of T T T whose canonical cells lie in R R R . The new static items hashing into R R R occupy only cells of R R R , and their layout and metadata can be computed from those items alone.
Proof. Adding arrival work can only increase the occupied set. Since T , S ⊆ U T,S \subseteq U T , S ⊆ U , every run of either the old or the new static table lies within a U U U -run. An item and its canonical cell lie in the same run, proving the placement statements.
The ARCH priorities within each new run are determined by the hashes in that run. Consequently, the priorities, placement, home bits, and stop marks within R R R depend only on the items of S S S hashing into $R.
We next show that every such interval fits in the rebuild scratch region with high probability. Only a simple large-deviation bound is needed: a long run requires an interval to receive at least its own length in arrival work, even though the expected volume is smaller by a factor 1 − Θ ( ε ) 1-\Theta(\varepsilon) 1 − Θ ( ε ) .
Lemma D.2 (A Tail Bound for the Longest Run). Let Z Z Z be a fixed set of items with independent uniform hashes, maximum length at most ℓ \ell ℓ , and volume at most ( 1 − β ) m (1-\beta)m ( 1 − β ) m , where β ∈ ( 0 , 1 ) \beta\in(0,1) β ∈ ( 0 , 1 ) . Place its words whenever arrival work is pending, as above. For every integer r ≥ 1 r \ge1 r ≥ 1 ,
Pr [ some run has length at least r ] ≤ m 2 exp ( − c β 2 r / ℓ ) \Pr[\text{some run has length at least } r] \le m^2 \exp(-c\beta^2r/\ell) Pr [ some run has length at least r ] ≤ m 2 exp ( − c β 2 r / ℓ ) for an absolute constant c > 0 c>0 c > 0 .
Proof. For a fixed interval I I I , let W ( I ) W(I) W ( I ) be the total length of the items with canonical cells in I I I . It is a sum of independent variables in [ 0 , ℓ ] [0,\ell] [ 0 , ℓ ] with expectation at most ( 1 − β ) ∣ I ∣ (1-\beta)|I| ( 1 − β ) ∣ I ∣ . A Chernoff bound gives
Pr [ W ( I ) ≥ ∣ I ∣ ] ≤ exp ( − c β 2 ∣ I ∣ / ℓ ) . \Pr[W(I) \ge|I|] \le\exp(-c\beta^2|I|/\ell). Pr [ W ( I ) ≥ ∣ I ∣ ] ≤ exp ( − c β 2 ∣ I ∣/ ℓ ) .
Every word of a run belongs to an item whose canonical cell lies in that run. Thus, if I I I is a run, W ( I ) = ∣ I ∣ W(I)=|I| W ( I ) = ∣ I ∣ . There are at most m 2 m^2 m 2 intervals, so a union bound over those of length at least r r r proves the result.
Claim D.3 (All Rebuild Intervals Fit in Cache). For a sufficiently large constant c 0 c_0 c 0 , with probability at least 1 − m − 2 1-m^{-2} 1 − m − 2 , every U U U -run has length at most
M ′ = c 0 s max ε − 2 log m . M' = c_0s_{\max}\varepsilon^{-2}\log m. M ′ = c 0 s m a x ε − 2 log m . Proof. There are at most two items of U U U with a given key: one in T T T and one in Y Y Y . Merge them into a single item of length at most 2 s max 2s_{\max} 2 s m a x . This preserves the arrival work, hence the occupied cells of P U P_U P U . The merged items have distinct keys and independent hashes, and their lengths and membership are fixed by the operation sequence, independently of the hashes. Apply Lemma D.2 with ℓ = 2 s max \ell= 2s_{\max} ℓ = 2 s m a x , β = ε / 4 \beta= \varepsilon/4 β = ε /4 , and r = M ′ r = M' r = M ′ .
Only the per-item query bounds of the static table enter the operation analysis. Its expected maximum stretch is not needed.
Rebuilding with Sequential Passes
Locality reduces the rebuild to two tasks: enumerate the live insertion-table items in hash order, and identify the U U U -runs. Once these are available, each new static run can be computed in cache and written over the old table.
Lemma D.4 (Streaming Rebuild). A rebuild has expected scan cost O ( m ) O(m) O ( m ) . Under the cache-size assumption of § 9 , M ≥ C M ( B + s max ε − 2 log 3 m ) M \ge C_M(B+s_{\max}\varepsilon^{-2}\log^3 m) M ≥ C M ( B + s m a x ε − 2 log 3 m ) , it takes O ( m / B ) O(m/B) O ( m / B ) expected I/Os. With probability 1 − O ( m − 2 ) 1-O(m^{-2}) 1 − O ( m − 2 ) , it uses a constant number of sequential passes and O ( ε m ) O(\varepsilon m) O ( ε m ) words of temporary space outside the table; otherwise it uses O ( m ) O(m) O ( m ) temporary words.
Proof. We may assume δ ≤ 1 \delta\le1 δ ≤ 1 , since proving the update bound for a smaller δ \delta δ suffices. First consider a long update. Its length, or the length of its deleted key, is at least ε ′ m / 128 \varepsilon' m/128 ε ′ m /128 . Hence s max ≥ ε m / ( 128 C ) s_{\max} \ge\varepsilon m/(128C) s m a x ≥ ε m / ( 128 C ) , and for sufficiently large C M C_M C M the cache assumption puts the entire physical array in cache. We read it, apply the update, rebuild, and write it back in O ( m / B ) O(m/B) O ( m / B ) I/Os. We now consider budget-triggered rebuilds.
Pass 1: Extract the Insertions in Hash Order. Every live item of C s m a l l C_{\mathrm{small}} C small has a live chunk in B s m a l l B_{\mathrm{small}} B small (Appendix § 14 ). Scan the physical array, reading these chunks and ignoring the primary codewords. The allocator state identifies the level, chunk, and offset of each backup cell. A scan reads at most one partial chunk per allocated subarray at a time. There are O ( log m ) O(\log m) O ( log m ) levels per class, and a class-i i i chunk has 2 i + 2 2^{i+2} 2 i + 2 cells. Summing over classes, the partial chunks occupy O ( s max log m ) O(s_{\max}\log m) O ( s m a x log m ) cached words.
When a live chunk is complete, remove its padding, compute its hash, and keep its item in a buffer ordered by hash. Release an item y y y to a sequential temporary stream when the scan has advanced
λ = C 1 σ s max log 2 + δ m \lambda= C_1\sigma s_{\max}\log^{2+\delta}m λ = C 1 σ s m a x log 2 + δ m
physical cells past c ( y ) c(y) c ( y ) , for a sufficiently large constant C 1 C_1 C 1 . The placement argument of Lemmas C.4 and 6.5 bounds the distance to the end of a class-i i i chunk in level ℓ \ell ℓ by
O ( σ ( J ℓ + 1 ) s max log 1 + δ m ) , O\left(\sigma(J_\ell+1)s_{\max}\log^{1+\delta}m\right), O ( σ ( J ℓ + 1 ) s m a x log 1 + δ m ) ,
where J ℓ J_\ell J ℓ has an exponential tail: the level is a unit-size linear-probing table at load at most 1 / 2 1/2 1/2 . A union bound over the at most m m m live items shows that, with probability 1 − O ( m − 2 ) 1-O(m^{-2}) 1 − O ( m − 2 ) , every item is complete before its release point. When λ < m t o t a l \lambda< m_{\mathrm{total}} λ < m total , unwrap canonical positions into the interval [ m t o t a l − λ , 2 m t o t a l − λ ) [m_{\mathrm{total}}-\lambda,2m_{\mathrm{total}}-\lambda) [ m total − λ , 2 m total − λ ) . Use each chunk’s first complete occurrence after its represented canonical position, ignoring earlier occurrences and later duplicates. The first lap initializes the cyclic buffer; the second emits one lap of releases, processing equal-cell releases in hash order. This produces a stream of Y Y Y in cyclic hash order. When λ ≥ m t o t a l \lambda\ge m_{\mathrm{total}} λ ≥ m total , instead collect all of Y Y Y in at most two laps, sort it in cache, and write the stream directly.
On this event, buffered items have canonical cells in an interval of at most λ \lambda λ physical cells behind the scan. The set Y Y Y has independent hashes and volume O ( ε m ) O(\varepsilon m) O ( ε m ) . A Chernoff bound and a union bound over scan positions show that, with probability 1 − O ( m − 2 ) 1-O(m^{-2}) 1 − O ( m − 2 ) , the buffer uses at most
O ( ε λ + s max log m ) = O ( s max log 2 + δ m ) O(\varepsilon\lambda+s_{\max}\log m)=O(s_{\max}\log^{2+\delta}m) O ( ε λ + s m a x log m ) = O ( s m a x log 2 + δ m )
words. If λ \lambda λ exceeds the array length, the same bound follows by buffering all of Y Y Y .
Pass 2: Find a Boundary and Check Run Lengths. Start at any empty cell of the old static table and read one old run at a time. Parse each run in cache, reconstruct its items, and sort them by hash. An oversized old run triggers the fallback described below. Otherwise, merge its arrivals with the stream from pass 1 to obtain, in cyclic cell order,
a ( j ) = ∑ y ∈ U : c ( y ) = j ∣ y ∣ . a(j)=\sum_{y\in U:c(y)=j}|y|. a ( j ) = y ∈ U : c ( y ) = j ∑ ∣ y ∣.
If the starting cell is indexed 0, one lap computes a minimizer j ∗ j^* j ∗ of
F ( j ) = ∑ t = 0 j a ( t ) − ( j + 1 ) . F(j)=\sum_{t=0}^{j}a(t)-(j+1). F ( j ) = t = 0 ∑ j a ( t ) − ( j + 1 ) .
Every cyclic interval ending at j ∗ j^* j ∗ receives at most its length in work, since F ( j ∗ ) F(j^*) F ( j ∗ ) is minimal and Vol ( U ) < m \operatorname{Vol}(U)<m Vol ( U ) < m . Thus no work is pending after cell j ∗ j^* j ∗ . Continuing from there with zero pending work, the recursion
w ( j + 1 ) = max { 0 , w ( j ) + a ( j ) − 1 } w(j+1)=\max\{0,w(j)+a(j)-1\} w ( j + 1 ) = max { 0 , w ( j ) + a ( j ) − 1 }
identifies the occupied cells of P U P_U P U : cell j j j is occupied exactly when w ( j ) + a ( j ) > 0 w(j)+a(j)>0 w ( j ) + a ( j ) > 0 . In one additional lap, find an empty cell e 0 e_0 e 0 and check that every U U U -run has length at most M ′ M' M ′ . If a run crosses the starting point of this lap, combine its initial and final occupied segments when checking its length. All size checks take place before any table cells are overwritten.
Pass 3: Rebuild Each Interval in Cache. Starting just after e 0 e_0 e 0 , read the table for one lap, using the same arrival-work recursion. For each U U U -run R R R , collect the old items stored in R R R and the items of Y Y Y hashing into R R R . Their total volume is ∣ R ∣ ≤ M ′ |R|\le M' ∣ R ∣ ≤ M ′ . Discard flagged items and compute the new ARCH layout in cache: priorities in decreasing cyclic hash order, delimiters, home bits, and stop marks. By Claim D.1, this is exactly the restriction of the new static table to R R R .
Also compute the collision bits for the new static set: initialize them to zero, sort the retained items by descriptor in cache, and mark each canonical cell whose descriptors include a duplicate. All keys with a given canonical cell lie in the same U U U -run, so this detects every relevant collision. The descriptors use O ( ∣ R ∣ ) O(|R|) O ( ∣ R ∣ ) cached words and require no additional I/Os. Clear all deletion bits.
Write the new contents over the old run. The write pointer trails the read pointer, so each run is read completely before it is overwritten. Clear all cells outside the U U U -runs, both auxiliary structures, their allocator state, and the budget counters. The backup chunks have already been extracted in pass 1.
Space, Failure Events, and Cost. The cached working space is
O ( s max log 2 + δ m + M ′ ) = O ( s max ε − 2 log 3 m ) , O(s_{\max}\log^{2+\delta}m+M')=O(s_{\max}\varepsilon^{-2}\log^3m), O ( s m a x log 2 + δ m + M ′ ) = O ( s m a x ε − 2 log 3 m ) ,
and the temporary stream contains O ( Vol ( Y ) ) = O ( ε m ) O(\operatorname{Vol}(Y)) = O(\varepsilon m) O ( Vol ( Y )) = O ( ε m ) words. Each pass scans at most two laps of the array and a constant number of sequential streams, using O ( 1 ) O(1) O ( 1 ) blocks outside the scratch region. Thus the passes cost O ( m / B ) O(m/B) O ( m / B ) I/Os and scan O ( m ) O(m) O ( m ) cells.
The failure events are a late chunk, an oversized extraction buffer, or an oversized run. A chunk’s cyclic displacement from its canonical position is checked when it is read, so a late chunk is detected even if its chosen occurrence falls beyond the second lap. Their total probability is O ( m − 2 ) O(m^{-2}) O ( m − 2 ) , and they are detected in the first two passes, while the old table is intact. In that case, extract the items into external working space and build the new table directly, using O ( m ) O(m) O ( m ) temporary words and O ( m log m ) O(m \log m) O ( m log m ) RAM operations and I/Os. This fallback contributes only O ( m − 1 log m ) = O ( m / B ) O(m^{-1}\log m) = O(m/B) O ( m − 1 log m ) = O ( m / B ) expected I/Os for B ≤ m B \le m B ≤ m . For B > m B > m B > m , the whole array fits in cache under our assumptions. Charge the first access to the triggering update; once that block is cached, random rotation gives O ( m / B ) O(m/B) O ( m / B ) expected additional I/Os to load the array and rebuild it there. The same estimates give expected scan cost O ( m ) O(m) O ( m ) , including the fallback.