Using bit arrays for space-efficient data representation — sort, deduplicate, and query large datasets with bit-level operations that are orders of magnitude faster than conventional approaches.
The first time I needed to sort a billion 32-bit integers on a machine with only 2 GB of RAM, I knew the standard quicksort approach wasn't going to work. Loading a billion integers into memory would require 4 GB just for the data. That is when I discovered bitmap algorithms — techniques that use bit arrays to represent data at the bit level, achieving massive memory savings by trading some generality for space efficiency.
A bitmap (or bit array) is an array of bits where each bit represents the presence or absence of an element. If the element at position i is in the set, bit i is 1. Otherwise, it is 0. Instead of storing the actual values — which takes 32 or 64 bits per integer — you store just a single bit per possible value. For dense datasets where the range is not much larger than the number of elements, this is a transformative optimization.
For a URL deduplication system, I implemented a bitmap-based set that could track 100 million entries in under 12 MB of memory. The key was using multiple rotating bitmaps to handle the high insertion rate.
Bitmap sort was described by Jon Bentley in his classic book "Programming Pearls," and it remains one of the most elegant demonstrations of bit-level algorithmic thinking. The algorithm works in three passes:
The output is automatically sorted in ascending order, and duplicates are automatically removed. Time complexity is O(n + range), which reduces to O(n) when the range is proportional to n. Space complexity is O(range) bits.
function bitmapSort(nums, maxValue) {
// Each byte stores 8 bits — allocate (maxValue + 8) / 8 bytes
const bitmap = new Uint8Array(Math.ceil((maxValue + 1) / 8));
// Phase 1: set bits for each number
for (const x of nums) {
const byteIdx = x >> 3; // x / 8
const bitIdx = x & 0x07; // x % 8
bitmap[byteIdx] |= (1 << bitIdx); // set bit using OR
}
// Phase 2: scan bitmap and output sorted values
const result = [];
for (let i = 0; i <= maxValue; i++) {
const byteIdx = i >> 3;
const bitIdx = i & 0x07;
if (bitmap[byteIdx] & (1 << bitIdx)) { // test bit using AND
result.push(i);
}
}
return result;
}
// Example: sort [3, 7, 1, 3, 5, 9] with maxValue = 10
// Output: [1, 3, 5, 7, 9] — sorted and deduplicated
Here is a concrete breakdown that I use when deciding whether a bitmap algorithm is appropriate for a problem. For a range of 0 to 1,000,000:
The crossover point depends on the density of your data. With a 32-bit integer array (4 bytes per element), the bitmap is more memory-efficient when the number of distinct elements exceeds (range / 32). That is only about 3% density. For many real-world datasets — IP addresses, user IDs, sensor readings — the range is large but the density is low, so bitmap algorithms become even more attractive.
I once optimized a network traffic analysis pipeline that was tracking which source IPs had been seen in each 5-minute window. With the bitmap approach, processing 1 million unique IPs across the 32-bit IPv4 range took 512 MB of bitmap memory — which was acceptable for our server. The previous implementation used a hash set and consumed over 4 GB.
The real power of bitmap algorithms becomes apparent when you need to perform set operations on large datasets. Because a bitmap is just an array of bits, set operations reduce to fast bitwise word-level operations.
// Set operations on bitmaps using 32-bit word-level operations
// Union: A ∪ B
function bitmapUnion(a, b, wordCount) {
const result = new Uint32Array(wordCount);
for (let i = 0; i < wordCount; i++) {
result[i] = a[i] | b[i]; // bitwise OR
}
return result;
}
// Intersection: A ∩ B
function bitmapIntersection(a, b, wordCount) {
const result = new Uint32Array(wordCount);
for (let i = 0; i < wordCount; i++) {
result[i] = a[i] & b[i]; // bitwise AND
}
return result;
}
// Difference: A \ B
function bitmapDifference(a, b, wordCount) {
const result = new Uint32Array(wordCount);
for (let i = 0; i < wordCount; i++) {
result[i] = a[i] & ~b[i]; // bitwise AND NOT
}
return result;
}
On modern CPUs, these bitwise operations process 32 or 64 bits per instruction cycle. For a 1-million-bit bitmap, the union, intersection, or difference completes in under a microsecond. This is the foundation for high-performance data processing in databases, search engines, and analytics platforms.
Plain bitmaps have a problem: if your dataset is sparse — only 1% of bits are set — you waste 99% of memory on zeros. Two optimized bitmap formats solve this. EWAH (Enhanced Word-Aligned Hybrid) compresses runs of consecutive zero words using a single marker word. Roaring Bitmaps, developed at the University of Lyon and now used by Apache Druid, Spark SQL, and Elasticsearch, take a different approach: they partition the bitmap into 16-bit chunks and use either an array or a bitmap per chunk depending on density. Roaring is faster than EWAH for most real-world workloads and is my default choice when I need bitmap operations in production.
Roaring Bitmaps from the CRoaring library can compute the intersection of two 1-million-element sets in under 50 microseconds — roughly 100x faster than a hash set-based intersection. The compression also means they use 2-5x less memory than plain bitmaps for typical sparse datasets. Try bitwise AND on our bitwise calculator to see how fast the operation is at the word level.
PostgreSQL uses BRIN (Block Range INdex) indexes that store a bitmap summary of each block range. When a query filters on the indexed column, PostgreSQL checks the bitmap before scanning the block — skipping entire ranges that cannot contain matching rows. This is identical in spirit to the bitmap sort algorithm.
InfluxDB uses bitmaps for its tag index system. Each tag value (like "host=server01") has an associated bitmap where bit i is 1 if that tag value appears in the i-th series. Queries like "find all series where host=server01 AND region=us-east" reduce to a bitwise AND between two bitmaps — a single CPU instruction per 32 or 64 series.
Ad platforms represent user segments as bitmaps. Segment A (users interested in sports) and Segment B (users aged 18-35) each have a bitmap over the user ID space. To find the intersection for a targeting campaign, the platform performs a bitwise AND. The result is a bitmap of user IDs who match all criteria, computed in microseconds.
// Audience targeting with bitwise operations
const sportsLovers = loadBitmap("segment_sports"); // 1M users
const age18to35 = loadBitmap("segment_age_18_35"); // 800K users
const premiumUsers = loadBitmap("segment_premium"); // 200K users
// Target: sports lovers aged 18-35 who are also premium users
const target = bitmapIntersection(
bitmapIntersection(sportsLovers, age18to35),
premiumUsers
);
// Result: ~50K matching user IDs in under 100 microseconds
The core operations behind bitmap algorithms — AND for intersection, OR for union, NOT for complement — are all available in our interactive calculator. See them work on individual integers and imagine them scaling to millions of bits.
A bitmap algorithm uses a bit array (bitmap) where each bit represents the presence or absence of an element. Instead of storing the elements themselves, you mark their positions in the bitmap. This enables space-efficient sorting, deduplication, and membership queries. For sorting a set of unique integers in a known range, bitmap sort runs in O(n) time using O(range/8) memory.
Bitmap sort works by scanning the input once: for each integer x, set bit x in the bitmap. Then iterate through the bitmap from low to high: for each set bit at position i, output i. The output is automatically sorted and deduplicated. This is O(n) time and O(range) bits, making it extremely efficient for dense integer ranges.
A bitmap representing 0 to 1,000,000 requires only 125 KB (1,000,001 bits / 8). An array of 32-bit integers holding 1 million numbers requires 4 MB. If you are storing only 100,000 elements across a 1 million range, the bitmap takes 125 KB while the array takes 400 KB — the bitmap still wins. The advantage grows with the range size.
A standard bitmap uses one bit per value, so it inherently deduplicates — setting a bit that is already set has no effect. For counting duplicates, you need a multi-bit bitmap variant. A 2-bit counter per position supports up to 3 occurrences per value. An 8-bit counter per position supports up to 255. The trade-off is memory: 2-bit counters use 2x the space, 8-bit counters use 8x.
Bitmap algorithms are used in database indexing (PostgreSQL's BRIN indexes track min/max block ranges), time-series databases (InfluxDB uses bitmaps for tag indexes), network monitoring (tracking IP addresses in traffic logs), operating systems (page frame allocation in memory management), and ad targeting platforms (audience overlap analysis via bitwise AND on user segment bitmaps).