Finding the K-th Set Bit With Select Queries
The select query is a fundamental operation in succinct
data structures that returns the exact index of the \(k\)-th occurrence of a set bit
(1) within a bitvector. While a straightforward linear scan
requires linear time, advanced implementations achieve constant time
\(O(1)\) by utilizing auxiliary index
structures and word-level bit manipulation algorithms. This article
details the mechanics of the select operation, comparing
basic search strategies, two-level succinct indexing frameworks, and
hardware-accelerated word-level techniques.
The Problem Definition
Given a bitvector \(B\) of length \(n\) containing binary values indexed from \(0\) to \(n-1\), the query \(\text{select}_1(B, k)\) finds the index \(i\) such that \(B[i] = 1\) and the total number of ones in the prefix \(B[0 \dots i]\) is exactly \(k\).
For example, in the bitvector 01001101, the set bits
appear at indices 1, 4, 5, and 7. Executing \(\text{select}_1(B, 3)\) returns index 5,
corresponding to the third set bit.
Binary Search over Rank Structures
The most direct way to locate the \(k\)-th bit without a linear scan is to leverage the dual operation: \(\text{rank}_1(B, i)\), which counts the number of ones up to index \(i\).
Because the prefix sum of set bits is monotonically non-decreasing, standard binary search can locate the index \(i\) where \(\text{rank}_1(B, i) = k\) and \(B[i] = 1\). By precomputing prefix sums for blocks of the bitvector, a binary search yields the position in \(O(\log n)\) time. However, achieving optimal query throughput requires constant \(O(1)\) time.
Succinct Two-Level Directory Scheme
To achieve \(O(1)\) time complexity while adding minimal memory overhead—specifically \(o(n)\) sub-linear auxiliary bits—practical implementations use hierarchical directories (often referred to as Clark’s select structure).
Superblocks (Primary Directory): The set bits are partitioned into groups of a fixed size \(S = \lfloor (\log n)^2 \rfloor\). An array stores the absolute index of every \(S\)-th set bit. This divides the bitvector into variable-length spans containing exactly \(S\) ones.
Handling Sparse Spans (Long Spans): If the bit distance between the \(j\)-th and \((j+1)\)-th sample is large (specifically \(\ge (\log n)^4\)), storing all absolute positions of the ones within that span explicitly in an array takes negligible space relative to the span’s large length. In these sparse regions, \(\text{select}\) resolves in \(O(1)\) time via a direct lookup.
Handling Dense Spans (Short Spans): If the bit distance is small (\(< (\log n)^4\)), the ones are packed tightly. A second-level directory is created by sampling every \(s' = \lfloor \log n \rfloor\)-th set bit within the span. Because the offset values are bounded by the span size, they can be stored using smaller bit-width integers, keeping auxiliary space bounded within \(o(n)\).
Leaf Blocks: Between second-level samples, the search space narrows to a single machine word (e.g., 64 bits), where word-level primitives compute the final exact index.
Word-Level Select Operations
When the search narrows down to a 64-bit machine integer, modern architectures and algorithmic techniques resolve the internal bit position in constant instructions:
- Hardware Instructions: Modern x86 processors
support the
PDEP(Parallel Bit Deposit) instruction alongsideTZCNT(Trailing Zero Count). Placing the value1ULL << (k - 1)as the source and the target 64-bit word as the mask isolates the \(k\)-th bit. Counting the trailing zeros of the result immediately yields the local bit position. - Broadword (SWAR) Algorithms: On processors lacking
dedicated bit-manipulation instructions, SIMD-within-a-register (SWAR)
arithmetic uses bitwise parallel masking and population count
(
POPCNT) steps to locate the target bit in \(O(1)\) clock cycles. - Lookup Tables: Small segments (e.g., 8 to 16 bits) can be evaluated instantly via precomputed tables indexed by the segment value and \(k\).
By combining multi-level directory sampling with hardware-level word
scans, the select query translates the theoretical binary
search into an instantaneous \(O(1)\)
memory lookup and bit-twiddling operation.