A bitwise trie is a special form of trie where each node with its child-branches represents a bit sequence of one or more bits of a key. A bitwise trie with bitmap uses a bitmap to denote valid child branches.

Tries and bitwise tries

A trie is a type of search tree where – unlike for example a B-tree – keys are not stored in the nodes but in the path to leaves. The key is distributed across the tree structure. In a "classic" trie, each node with its child-branches represents one symbol of the alphabet of one position (character) of a key.

In bitwise tries, keys are treated as bit-sequence of some binary representation and each node with its child-branches represents the value of a sub-sequence of this bit-sequence to form a binary tree (the sub-sequence contains only one bit) or n-ary tree (the sub-sequence contains multiple bits).

To give an example that explains the difference between "classic" tries and bitwise tries: For numbers as keys, the alphabet for a trie could consist of the symbols '0' .. '9' to represent digits of a number in the decimal system and the nodes would have up to 10 possible children.

There are multiple straight forward approaches to implement such a trie as physical data structure. To state two:

A node can be represented having an array of child pointers for each symbol of the alphabet – an array of 10 pointers per node in the decimal number example. This gives a lookup time with being the length of the key. But this isn't space efficient as each node preserves space for all possible child symbols even if there's no key that realizes that path. A node contains a binary tree of (symbol, child pointer) tuples, ordered by symbol. This has a better space efficiency but the lookup time now is . An ideal trie has an access time that is independent of the amount of keys stored.

These approaches get worse for larger alphabets, if, for example, the key is a string of Unicode characters. Treating the key as bit-sequence allows to have a fixed cardinality per node.

Bitwise trie with bitmap

Bagwell presented a time and space efficient solution for tries named Array Mapped Tree (AMT). The Hash array mapped trie (HAMT) is based on AMT. The compact trie node representation uses a bitmap to mark every valid branch – a bitwise trie with bitmap. The AMT uses eight 32-bit bitmaps per node to represent a 256-ary trie that is able to represent an 8 bit sequence per node. With 64-Bit-CPUs (64-bit computing) a variation is to have a 64-ary trie with only one 64-bit bitmap per node that is able to represent a 6 bit sequence.

To determine the index of the child pointer of a node for such a given 6-bit value, the amount of preceding child pointers has to be calculated. It turns out that this can be implemented quite efficiently.

Node traversal

long bitMap = mem[nodeIdx]; long bitPos = 1L << value; // 6-bit-value if ((bitMap & bitPos) == 0) return false; // not found long childNodeIdx = mem[nodeIdx + 1 + Long.bitCount(bitMap & (bitPos - 1))];

The offset to find the index based on the current node index is the amount of least significant bits set in the bitmap before the target position plus one for the bitmap. The amount of least significant bits set can be calculated efficiently with constant time complexity using simple bit operations and a CTPOP (count population) operation that determines the number of set bits, which is available as Long.bitCount() in Java. CTPOP itself can be implemented quite efficiently using a "bit-hack"

and many modern CPUs even provide CTPOP as a dedicated instruction treated by compilers as intrinsic function.

int CTPOP(long x) { // A "bit-hack" implementation of the population count function. x -= ((x >>> 1) & 0x5555555555555555L); x = (x & 0x3333333333333333L) + ((x >>> 2) & 0x3333333333333333L); x = (x + (x >>> 4)) & 0x0f0f0f0f0f0f0f0fL; x += (x >>> 8); x += (x >>> 16); x += (x >>> 32); return x & 0x7f; }

Set implementation example

Physical data structure

In this example implementation for a bitwise trie with bitmap, nodes are placed in an array of long (64-bit) integers. A node is identified by the position (index) in that array. The index of the root node marks the root of the trie.

Nodes are allocated from unused space in that array, extending the array if necessary. In addition, nodes, that are replaced, are collected in free lists and their space is recycled. Without this recycling, the data structure can be used to implement a persistent data structure by just keeping the previous root index and never overriding existing nodes but always creating a copy of a changed node.

Leaf nodes are inlined: Instead of having a child-pointer to a leaf node, the bitmap of the leaf node itself is stored.

public class BBTrieSet {

long[] mem; long[] freeLists; long freeIdx;

long root; long count;

// maximum node size is 1 (bitMap) + 64 (child pointers or leaf values) + 1 as arrays are zero based final static int FREE_LIST_SIZE = 1+64+1;

final static int KNOWN_EMPTY_NODE = 0; final static int KNOWN_DELETED_NODE = 1; final static int HEADER_SIZE = 2; // KNOWN_EMPTY_NODE, KNOWN_DELETED_NODE

public BBTrieSet(int size) { mem = new long[size]; freeLists = new long[FREE_LIST_SIZE]; freeIdx = HEADER_SIZE; root = KNOWN_EMPTY_NODE; count = 0;

private long allocate(int size) { long free = freeLists[size]; if (free != 0) { // requested size available in free list, re-link and return head freeLists[size] = mem[(int) free]; return free;

else { // expansion required? if (freeIdx + size > mem.length) { // increase by 25% and assure this is enough int currSize = mem.length; int newSize = currSize + Math.max(currSize / 4, size); mem = Arrays.copyOf(mem, newSize);

long idx = freeIdx; freeIdx += size; return idx;

private long allocateInsert(long nodeIdx, int size, int childIdx) { long newNodeRef = allocate(size + 1);

int a = (int) newNodeRef; int b = (int) nodeIdx;

// copy with gap for child for (int j = 0; j < childIdx; j++) mem[a++] = mem[b++]; a++; // inserted for (int j = childIdx; j < size; j++) mem[a++] = mem[b++];

deallocate(nodeIdx, size);

return newNodeRef;

private long allocateDelete(long nodeIdx, int size, int childIdx) { long newNodeRef = allocate(size - 1);

// copy with child removed int a = (int) newNodeRef; int b = (int) nodeIdx; for (int j = 0; j < childIdx; j++) mem[a++] = mem[b++]; b++; // removed for (int j = childIdx + 1; j < size; j++) mem[a++] = mem[b++];

deallocate(nodeIdx, size);

return newNodeRef;

private void deallocate(long idx, int size) { if (idx == KNOWN_EMPTY_NODE) return; // keep our known empty node

// add to head of free-list mem[(int) idx] = freeLists[size]; freeLists[size] = idx;

private long createLeaf(byte[] key, int off, int len) { long newNodeRef = allocate(2); int a = (int) newNodeRef; mem[a++] = 1L << key[len - 2]; mem[a] = 1L << key[len - 1]; // value len -= 3; while (len >= off) { long newParentNodeRef = allocate(2); a = (int) newParentNodeRef; mem[a++] = 1L << key[len--]; mem[a] = newNodeRef; newNodeRef = newParentNodeRef;

return newNodeRef;

private long insertChild(long nodeRef, long bitMap, long bitPos, int idx, long value) { int size = Long.bitCount(bitMap); long newNodeRef = allocateInsert(nodeRef, size + 1, idx + 1); mem[(int) newNodeRef] = bitMap | bitPos; mem[(int) newNodeRef+ 1 + idx] = value; return newNodeRef;

private long removeChild(long nodeRef, long bitMap, long bitPos, int idx) { int size = Long.bitCount(bitMap); if (size > 1) { // node still has other children / leaves long newNodeRef = allocateDelete(nodeRef, size + 1, idx + 1); mem[(int) newNodeRef] = bitMap & ~bitPos; return newNodeRef;

else { // node is now empty, remove it deallocate(nodeRef, size + 1); return KNOWN_DELETED_NODE;

public long size() { return count;

Set operations

Contains key

The get method tests, if a key is part of the set. The key is delivered as byte[] where each byte represents one 6-bit bit sequence of the key – so only 6 of the 8 bits per byte are used.

public boolean get(byte[] key, int len) { if (root == KNOWN_EMPTY_NODE) return false;

long nodeRef = root; int off = 0;

for (;;) { long bitMap = mem[(int) nodeRef]; long bitPos = 1L << key[off++]; // mind the ++ if ((bitMap & bitPos) == 0) return false; // not found

long value = mem[(int) nodeRef + 1 + Long.bitCount(bitMap & (bitPos - 1))];

if (off == len - 1) { // at leaf long bitPosLeaf = 1L << key[off]; return ((value & bitPosLeaf) != 0);

else { // child pointer nodeRef = value;

Set (add) key

public boolean set(byte[] key, int len) { long nodeRef = set(root, key, 0, len); if (nodeRef != KNOWN_EMPTY_NODE) { // denotes change count++; root = nodeRef; return true;

else return false;

private long set(long nodeRef, byte[] key, int off, int len) { long bitMap = mem[(int) nodeRef]; long bitPos = 1L << key[off++]; // mind the ++ int idx = Long.bitCount(bitMap & (bitPos - 1));

if ((bitMap & bitPos) == 0) { // child not present yet long value; if (off == len - 1) value = 1L << key[off]; else value = createLeaf(key, off, len); return insertChild(nodeRef, bitMap, bitPos, idx, value);

else { // child present long value = mem[(int) nodeRef + 1 + idx]; if (off == len - 1) { // at leaf long bitPosLeaf = 1L << key[off]; if ((value & bitPosLeaf) == 0) { // update leaf bitMap mem[(int) nodeRef + 1 + idx] = value | bitPosLeaf; return nodeRef;

else // key already present return KNOWN_EMPTY_NODE;

else { // not at leaf, recursion long childNodeRef = value; long newChildNodeRef = set(childNodeRef, key, off, len); if (newChildNodeRef == KNOWN_EMPTY_NODE) return KNOWN_EMPTY_NODE; if (newChildNodeRef != childNodeRef) mem[(int) nodeRef + 1 + idx] = newChildNodeRef; return nodeRef;

Clear (remove) key

public boolean clear(byte[] key, int len) { long nodeRef = clear(root, key, 0, len); if (nodeRef != KNOWN_EMPTY_NODE) { count--; if (nodeRef == KNOWN_DELETED_NODE) root = KNOWN_EMPTY_NODE; else root = nodeRef; return true;

else return false;

public long clear(long nodeRef, byte[] key, int off, int len) { if (root == KNOWN_EMPTY_NODE) return KNOWN_EMPTY_NODE;

long bitMap = mem[(int) nodeRef]; long bitPos = 1L << key[off++]; // mind the ++ if ((bitMap & bitPos) == 0) { // child not present, key not found return KNOWN_EMPTY_NODE;

else { // child present int idx = Long.bitCount(bitMap & (bitPos - 1)); long value = mem[(int) nodeRef + 1 + idx]; if (off == len - 1) { // at leaf long bitPosLeaf = 1L << key[off]; if ((value & bitPosLeaf) == 0) // key not present return KNOWN_EMPTY_NODE; else { // clear bit in leaf value = value & ~bitPosLeaf; if (value != 0) { // leaf still has some bits set, keep leaf but update mem[(int) nodeRef + 1 + idx] = value; return nodeRef;

else return removeChild(nodeRef, bitMap, bitPosLeaf, idx);

else { // not at leaf long childNodeRef = value; long newChildNodeRef = clear(childNodeRef, key, off, len); if (newChildNodeRef == KNOWN_EMPTY_NODE) return KNOWN_EMPTY_NODE; if (newChildNodeRef == KNOWN_DELETED_NODE) return removeChild(nodeRef, bitMap, bitPos, idx); if (newChildNodeRef != childNodeRef) mem[(int) nodeRef + 1 + idx] = newChildNodeRef; return nodeRef;

Set operators

Set operators for intersection (and), union (or) and difference (minus) are feasible using a flyweight pattern as shown below.

An interface represents physical nodes and "virtual" result nodes of an operator. Instances of this interface are created on demand during a trie traversal. Compound expressions, involving more than one operator, can be expressed directly by combining these operators as an operator can be used as argument (input) for another operator.

Flyweight interface

public interface BBTrieNode { public long getBitMap(); public long getBitMapLeaf(long bitPos); public BBTrieNode getChildNode(long bitPos);