Skip Lists are almost *always* presented as randomized data structures in the literature, sometimes accompanied by vague mentions of their deterministic counterparts[3]. A quick survey of *production* implementations tells a different story about the real world use of skip lists: randomization isn’t required for good performance, and may in fact be a hinderance.

Munro and Sedgewick showed that skip lists can be implemented deterministically by altering the definition of a skip list to include the following two properties:

- Two nodes are considered “linked” if there exists
*at least*one direct link from one node to the other. - The “gap size” between two nodes linked at height h is equal to the number of nodes at height h-1 between them.

These definitions ensure the property that a constant number of forward links are traversed at each level before dropping to the next – bounding the worst case search operation to O(log N).

A skip list that conforms to the above definition, who’s gap sizes are of either 1, 2 or 3 happen to be an isometric mapping of 2-3-4 tree’s, the very same tree’s Red/Black trees are modeled after. It also turns out that this class of skip list happens to be *incredibly* easy to implement.

The “traditional” Skip List had a node consisting of an array of forward pointers, with each level represented explicitly in the same way as they are commonly depicted.

struct SkipNode {

int info;

SkipNode* forward[MAX_LEVELS];

};

Deterministic Skip Lists(DSL) on the other hand use just two pointers: down and right, doing away with the arrays, but not the towers they represent. The “towers” are not made of individual nodes explicitly linked together.

struct SkipNode {

int info;

SkipNode *down, *right;

};

That node looks an awful lot like a binary tree node, doesn’t it? It turns out that searching in a DSL is much more closely related to searching in a binary search tree than searching in randomized skip list. In fact, the algorithm to perform a search is *exactly* the same as for binary search trees, which is phenomenal when one considers that we are now performing binary search on a linked list!

K find(K key) {

bottom->element = key;

link h = header;

while (key != h->element)

h = (key < h->element) ? h->down:h->right;

return (h == bottom) ? std::numeric_limits<K>::max():h->element;

}

And with good reason: we have done the same with a skip list as when you convert m-ary trees to binary trees by using the left child right sibling representation:

Now that I’ve whet your appetite a bit, lets get down to the nitty gritty and put some code together.

DSLs are built in a top down fashion, making ample use of sentinel nodes. Sentinel nodes are used the same way they are for top down red black trees in order to allow dereferencing pointers without having to worry about null pointers. This is because some operations on DSLs requiring dereferencing pointer *chains* and if explicit null checks were required the code would quickly become messy.

Before we can add elements to a skip list, we first have to initialize the sentinel nodes. The sentinel nodes are named header, tail, and bottom, and from there names I’m sure you can deduce what each of their responsibilities are. All three nodes are given then sentinel value of “infinity”.

const int INFINITY = std::numeric_limits<K>::max();

class SkipList {

private:

using node = SkipNode;

using link = node*;

link header, tail, bottom;

public:

SkipList();

int size() const { return count; }

bool empty() const { return count == 0; }

void insert(int key);

void find(int key);

void show();

};

SkipList::SkipList() {

header = new node(INFINITY);

tail = new node(INFINITY);

bottom = new node(INFINITY);

header->down = bottom;

header->right = tail;

tail->right = tail;

tail->down = tail;

bottom->right = bottom;

bottom->down = bottom;

count = 0;

}

The result of this initialization leaves us with an empty Skip List that looks like this:

It may seem strange to set three sentinel nodes to all have the same value. At first it seems all we’ve done is create a cycle of nodes with no way out, But as you will see momentarily, it is an ingenious way of dictating branch results during insertion.

The code for insertion really is delightfully simple. Inserting a new value into the skip list requires three steps. Two of these steps happen at each “level” of the skip list, and the third taking place as the final step of each insertion.

Insertion begins by pointing an iterator to the header node and setting the key of the “bottom” sentinel node to the value we are inserting. this ensures our insertion doesn’t devolve into an endless loop, as well as placing the key in preparation for insertion. Insertion proceeds from the header, top down.

void insert(K key) {

bottom->key = key;

link curr = header;

while (curr != bottom) {

curr = traverseRight(curr, key);

curr = promoteOrSink(curr, key);

}

growIfNeeded();

}

Upon entering a level of the skip list, the first thing we do is to traverse the current level as far right as we can in sorted order.

link traverseRight(link curr, K key) {

link x = curr;

while (x->key < key)

x = x->right;

return x;

}

Once we have traversed laterally as far as possible, we must decide if we should simply drop to the next level, or if we need to promote the height of the current node upwards in order to preserve the gap height invariant. The decision of whether to promote the current node is a simple one. We need to ensure that when we add the new value on level h that it doesn’t create a “gap” any larger than 3 nodes wide for the nodes on level h + 1.

link promoteOrSink(link curr, K key) {

if (curr->down->right->right->key < curr->key) {

curr = insertAndPromote(curr);

} else {

curr = curr->down;

}

return curr;

}

We compare the key of the current node against the key of the node located one level down and two places over, as this is the position where an insertion would violate the specified invariant. If the current key is less than the node being compared against we simply drop down to the next level in the skip list and go back to the first step.

If however, this happens to be the position where we need to an insert a node, then we must do a little rearranging to ensure our gap size doesn’t exceed 3. You can think of this as the skip-list analog of performing a rotation in a red/black tree. This is accomplished by pre-emptively splitting any gap of size 3 into two gaps of size one.

link insertAndPromote(link curr) {

curr->right = new node(curr->key, curr->right, curr->down->right->right);

curr->key = curr->down->right->key;

}

Once we have reached the bottom level and installed our new value in the base list, the only thing left to do is go back and check that none of the promotions reached the level of the header node, and if it did we must promote the header node one level higher.

void growIfNeeded() {

if (header->right != tail) {

header = new node(numeric_limits<K>::max(), tail, header);

}

}

To relate back to red/black tree’s one more time, this is analogous to “color flips” pushing a red node up and out of the root during rebalancing which in its self is analogous to a B Tree splitting at the root.

When you pause to think that we just implemented a data structure with identical functionality and comparable performance to red/black tree’s in about 1/3rd the code, deterministic skip lists become even more impressive.

But it’s not *just* red/black tree’s or even specifically 2-3-4 trees that these trees share an isomorphism to: it’s the entire family of B-Trees! And this revelation has opened the door to some truly amazing discoveries in the world of data structures.

Anyway, that’s what I’ve got for today. Until next time, Happy Hacking.

[1] Pugh, William, “Skip Lists: A Probabilistic Alternative to Balanced Trees”, 1990 https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf

[2] Munro, Popadakis, and Sedgewick, “Deterministic Skip Lists”

Proceedings of the ACM-SIAM Third Annual Symposium on Discrete Algorithms, January 1992. https://www.ic.unicamp.br/~celio/peer2peer/skip-net-graph/deterministic-skip-lists-munro.pdf

[3] Sedgewick, R. “Algorithms in C++” (3rd. ed) Prentice Hall 1999

]]>The 1978 paper “A Dichromatic Framework for Balanced Trees” by Guibas and Sedgewick[1] in which the authors introduced red/black trees to the world, they discuss algorithms for bottom up 2-3 tree’s, bottom up 2-3-4 tree’s, and bottom up AVL tree’s using the Red/Black framework as the mechanism for deciding when to rotate. In addition, the authors presented – and were primarily concerned with – *top down* algorithms for 2-3-4 red/black trees, as these were considered to posses more desirable properties.

Nearing the end of the paper the authors very briefly mention a variant of Red/Black tree’s which they call “Top Down Single Rotation Trees” and go on to claim that this variant requires 60% less code than what is needed for “traditional” AVL and 2-3 trees while offering comparable performance.

It was the authors desire to implement balanced red/black tree’s using only single rotations which led to this discovery. In order to eliminate double rotations, they allow two red nodes to occur in a row in the tree – *so long as they slant in the same direction. *It is only revealed that these “Top Down single rotation” trees are in fact 2-3-4-5 trees in the caption under a figure for “Program 7″[1].

As far as I know, this is the only discussion of Red/Black 2-3-4-5 trees *anywhere* in published literature, and Sedgewick was nice enough to include an algorithm for top-down insertion – albeit in ALGOL (68?). Being the nice guy that I am, I’ve taken the liberty of implementing the insertion algorithm in C++ to share with all of you.

With the exception of the node structure, these trees share no other code with the bottom up variants I’ve been discussing lately, with even the rotations being handled differently.

template <class K, class V>

struct rbnode {

K key;

V value;

bool color;

int N;

rbnode* left;

rbnode* right;

rbnode(K key_, V value_) {

key = key_; value = value_;

color = true;

left = nullptr; right = nullptr;

N = 0;

}

rbnode() {

color = true;

}

};

It is customary for iterative implementations of Red/Black Trees to use sentinel nodes as both a header and as sentinel nil nodes in the place of null pointers. While this would be more of an annoyance in a recursive implementation, for a Top/Down Iterative approach it makes our job much easier by allowing us to safely dereference certain links without having to perform additional checks, simplifying the code greatly. In addition to sentinel nodes, I’ve also made use of named accessor methods to make the code a little nicer to read and somewhat more in-line with the original ALGOL.

template <class K, class V>

class TopDown2345 {

private:

using node = rbnode<K,V>;

using link = node*;

link head, z;

link curr, parent, grand;

int count;

node* left(node* t) {

return t->left;

}

node* right(node* t) {

return t->right;

}

bool color(node* t) {

return t->color;

}

K key(node* t) {

return t->key;

}

bool isnil(node* x) {

return x == z;

}

node* rotate(K key, node* y);

void balance(K key);

public:

TopDown2345() {

z = new node(numeric_limits<K>::max(), numeric_limits<V>::max(), black, nullptr, nullptr);

z->left = z; z->right = z;

head = new node(numeric_limits<K>::min(), numeric_limits<V>::min(), black, z, z);

count = 0;

}

void insert(K key, V value);

};

If you’ve never used sentinel nodes in a binary search tree before, the reason we initialize head to the lowest possible value is because head is not the root node. Head *points* to the root node through its right link. Thus when we compare any value to head’s key, it will always result in a right branch. Z’s left and right pointers point back to it’s self, while head’s point to z. This allows us to de reference links like head->right->left->right->right without worrying about a possible null pointer.

During insertion we have to track some of our ancestors on our search path as they may be needed for recoloring/balancing. In 2-3-4 tree’s we needed to track the current node, its parent, grand-parent, and great-grand parent. Since 2-3-4-5 tree’s don’t perform double rotations, we no don’t need to track the current nodes great-grandparent anymore.

void insert(K k, V value) {

curr = head;

parent = curr;

grand = parent;

while (!isnil(curr)) {

grand = parent; parent = curr;

curr = (k < key(curr)) ? left(curr):right(curr);

if ((color(curr->left) && color(curr->right))

|| (color(curr) && color(curr->left))

|| (color(curr) && color(curr->right))) {

balance(k);

}

}

curr = new node(k, value, black, z, z);

if (k < key(parent)) parent->left = curr;

else parent->right = curr;

balance(k);

head->right->color = black;

count++;

}

If during insertion we encounter a node that has two red children or a red node with a red child, we call our balance routine which handles the re-coloring and rotations. Otherwise, insertion proceeds as normal for Iterative insertion, inserting the new node as a leaf. While 2-3 and 2-3-4 tree’s have newly inserted nodes colored *red*, 2-3-4-5 nodes are colored *black* upon insertion. Once installed at the leaf, we call the balance routine one final time before coloring the root node black and exiting.

It’s interesting that newly inserted nodes should be colored black, though inconsequential as upon calling balance after their insertion, we immediately change it’s color to red.

void balance(K v) {

if (color(curr) == black) {

curr->color = red;

curr->left->color = black;

curr->right->color = black;

} else {

curr->color = black;

parent->color = red;

}

if (color(curr) == black ||

(color(parent) == red && (v < key(grand) != v < key(parent)))) {

parent = rotate(v, grand);

}

head->right->color = black;

}

If on the other hand balance has been called during the search, and the current node is red, it will color it black and its parent red, setting it up for the next case, where if the current node is black, or we are the *bottom* node of two red’s which are slanting in the wrong direction, we perform a single rotation of the current nodes parent. The rotation method is interesting in that it “re-discovers” which direction it should rotate. This is the same rotation procedure in the earlier editions of Sedgewicks “Algorithms in C++”[2].

node* rotate(K v, node* y) {

node* gchild, *child = (v < key(y)) ? left(y):right(y);

if (v < key(child)) {

gchild = child->left; child->left = gchild->right; gchild->right = child;

} else {

gchild = child->right; child->right = gchild->left; gchild->left = child;

}

if (v < key(y)) y->left = gchild; else y->right = gchild;

return gchild;

}

We can of course validate the resulting tree’s in the usual way by performing the following checks:

- Is the tree a valid Binary Search Tree?
- Does the tree have the same number of black nodes along every path from root to leaf? (Height Balanced)
- Are red nodes arranged according to the rules laid out in the paper?

The first two tests are exactly the same as for any other red/black tree. It is the newly allowed orientations of the red nodes that we must confirm is correct.

bool isBST(node* x) {

if (x == z)

return true;

if (x->left != z && x->key < x->left->key)

return false;

if (x->right != z && x->right->key < x->key)

return false;

return isBST(x->left) && isBST(x->right);

}

bool isBal(node* x, int bb) {

if (x == z)

return bb == 0;

if (!isRed(x))

bb--;

return isBal(x->left, bb) && isBal(x->right, bb);

}

bool blackBalance(node* root) {

int bb = 0;

node* x = root;

while (x != z) {

if (!isRed(x)) bb++;

x = x->left;

}

return isBal(root, bb);

}

Determining the red balance is also straight forward. If two red nodes occur in a row, they must “lean” the same direction. Other than that, we must ensure that no *more* than two red nodes line up.

bool isRed(node* x) { return (x == z) ? false:(x->color == true); }

bool is2345(node* x) {

if (x == z)

return true;

if ((isRed(x->left) && isRed(x->left->right)) ||

(isRed(x->right) && isRed(x->right->left))) {

return false;

}

if (isRed(x->left) && isRed(x->left->left) &&

(isRed(x->left->left->left) || isRed(x->left->left->right))) {

return false;

}

if (isRed(x->right) && isRed(x->right->right) &&

(isRed(x->right->right->right) || isRed(x->right->right->left))) {

return false;

}

return is2345(x->left) && is2345(x->right);

}

It’s a shame these tree’s have been practically forgotten to history as they are both a fun and interesting way of taking a different look at the red/black framework while showing the surprising degree of flexibility the framework posses. Being height balanced, they offer the same O(log N) worst case performance as the other balanced red black tree’s.

But here’s the rub: those pesky constants are it again. It’s known that for AVL tree’s the hidden constant is 1.44, and 2-3-4 Red Black Tree’s are O(2LogN). This is where the adage that AVL tree’s are better when search is the dominant use, and Red/Black tree’s are better for insertion heavy tasks comes from. Sedgewick speculates that for “Top Down single rotation tree’s” that hidden constant is 3. The practical real world implications of this are unknown, as for as far as I can tell, these trees never left the halls of academia.

Anyway, that’s what I’ve got for today. Until next time, Happy Hacking!

[1] Guibas, L., Sedgewick, R. “A di-chromatic framework for balanced trees”, 1978, https://sedgewick.io/wp-content/themes/sedgewick/papers/1978Dichromatic.pdf

[2] Sedgewick, R. “Algorithms” 2nd, ed. (1992) Ch. 15 “Balanced Trees”

]]>In the beginning there was 2-3 trees. They were serendipitously discovered by Hopcroft in 1970[5] – around the same time as the B Tree family of search trees with which they share many properties. 2-3 Tree’s have many desirable properties that make them great data structures for in-memory searching. Unfortunately they are also rather difficult to implement directly in most modern programming languages. This is where Red/Black Trees entered the equation. Red/Black Tree’s take the concept of representing m-ary tree’s as binary trees, and extend it to search trees, making it possible to implement *balanced* m-ary search tree’s as binary *search* trees. Guibas and Sedgewick mention 2-3 trees in the 1978 paper originally introducing Red/Black trees but found them (at the time) to be inferior to 2-3-4 trees.

And there they stayed until Arne Anderson published his paper on AA trees, in which he detailed a set of algorithms which implemented the Red/Black rules with one addition: Red nodes can only be right children(sound familiar?). Anderson also removed the idea of “color” from the nodes, using instead a nodes “level” in the tree. While inspired by them. it can be argued that AA tree’s aren’t true Red/Black Trees.

In 2008 Sedgewick introduced Left Leaning Red/Black tree’s, which took Arne Andersons Idea of enforcing the direction of red children (conveniently choosing the opposite of Anderson’s), and re-implementing it directly in the red black framework. This made for a much easier implementation than the original Red/Black algorithms thanks to eliminating symmetric balancing cases, reducing the number of conditions requiring rotations in half.

While the algorithms were simple to explain, It wasn’t long before people started noticing that something wasn’t *quite* right with their performance, particularly with respect to deletion.[3] As it turns out, the very thing that made for such a simple insertion algorithm was the self-same property that was strangling the performance of Left Leaning Red/Black trees: asymmetry. This shouldn’t have really come as much of a surprise, as AA tree’s had long been known to perform much worse than Red/Black trees. And Left Leaning Red/Black trees have similar performance to AA trees.

Sedgewick was eager to point out that Left Leaning Red/Black trees have a one-to-one correspondence with 2-3 trees. What he failed to mention in his 2008 papers is that you can do the same trick with right leaning Red/Black trees, and that in fact red nodes can lean in either direction in the same tree: the only condition separating 2-3 Red/Black tree’s from 2-3-4 Red/Black Trees is that a red node can have a red sibling in 2-3-4 trees, In 2-3 trees you cannot. It is this last point that bring us to the point of todays post: 2-3 Red/Black trees that may have red child nodes as both right and left within the same tree.

I found out about this class of Red/Black tree very recently, I can’t remember what it was I was searching for when I came across the article[4], but the author termed them “Parity Seeking 2-3 Red/Black Trees”. While the algorithms for maintaining balancing in them *do* have to handle more cases than is addressed by Left Leaning 2-3 trees, the logic behind the rotations is very simple, *and* don’t suffer the same performance hits that LLRB and AA trees have – performing on par with 2-3-4 red/black trees. I’ve blabbed on a bit longer than I meant to, so let’s stop talking about them and start implementing them!

Insertion in these trees takes place bottom up – we perform a normal BST insertion, inserting a new node as a leaf which we color red. Once the that is done, we then proceed back up the path towards the root, fixing any violations of the Red/Black rules.

node* insert23(node* x, K key, V value) {

if (x == nullptr)

return new node(key, value);

if (key < x->key) x->left = insert23(x->left, key, value);

else x->right = insert23(x->right, key, value);

return fixInsert23(x);

}

node* insert(K key, V value) {

root = insert23(root, key, value);

root->color = black;

count++;

}

The papers authors use the iterative Red/Black Tree implementation from CLRS which balances purely bottom up as the foundation from which they created their 2-3 balancing rules. I have instead chose to implement the algorithms they described using a recursive approach without parent pointers, using my bottom up 2-3-4 Red/Black Tree implementation covered in a previous post as a starting point.

The bottom up balancing belies a beautiful simplicity in it’s logic of when to rotate. The algorithm for insertion follows a rather intuitive set of rules laid out by the authors of the original paper[4].

node* fixUp(node* x) {

x = makeRedChildrenLean(x);

x = makeRedParentSibling(x);

x = pushRedUp(x);

return x;

}

Regardless of which style you prefer – iterative or recursive – we have three conditions that will require rotations or recoloring. Case A and Case B have Right and Left mirrored cases while Case C is the same. The conditions described in the original paper are as follows:

Case A) If two nodes in a row are red, but are “bent”, we rotate the child node to make the two red nodes be “straight”.

node* makeRedChildrenLean(node* x) {

if (isRed(x->left) && isRed(x->left->right))

x->left = rotL(x->left);

if (isRed(x->right) && isRed(x->right->left))

x->right = rotR(x->right);

return x;

}

Case B) If two nodes in a row are red and leaning in the same direction, we rotate the parent node, turning the parent-child pair into siblings.

node* makeRedParentSibling(node* x) {

if (this->isRed(x->left) && this->isRed(x->left->left))

x = this->rotR(x);

if (this->isRed(x->right) && this->isRed(x->right->right))

x = this->rotL(x);

return x;

}

C) If a node has a red sibling, we do a color flip on the node, its parent, and its sibling, effectively pushing the red node “up” towards the root.

node* pushRedUp(node* x) {

if (this->isRed(x->left) && this->isRed(x->right))

x = this->colorFlip(x);

return x;

}

“But Wait!” I hear you saying, “Doesn’t having the root node be red violate the rules for red black trees?” And that is absolutely correct. It is for this reason that we end every insertion by coloring the root node black. This seemingly random final step is a safe guard against the event that during the rebalancing the root has become red – as in the example above. We can safely change the root’s color because it will increase the black length of *all* paths in the tree equally. This is the equivalent of when a B-Tree splits at the root and grows upwards.

As you can see, once done we are left with a valid red black Tree. The helper methods used to perform rotations and color changes are exactly the same ones used for 2-3-4 Red/Black Trees and Left Leaning Red/Black Tree’s:

bool isRed(node* x) {

return (x == nullptr) ? false:(x->color == red);

}

node* colorFlip(node* x) {

x->color = !x->color;

x->left->color = !x->left->color;

x->right->color = !x->right->color;

return x;

}

node* rotR(node* x) {

node* y = x->left;

x->left = y->right;

y->right = x;

y->color = x->color;

x->color = red;

return y;

}

node* rotL(node* x) {

node* y = x->right;

x->right = y->left;

y->left = x;

y->color = x->color;

x->color = red;

return y;

}

If you’re still curious of what exactly each rotation accomplishes I’ve made this handy graphic to help explain how the rotations and color changes effect the structure of the tree, showing the tree before and after the changes are applied and the relevant line of code that precipitated it:

Just as Sedgewick noted that by changing the placement of the color flip changed his Left Leaning Red Black Tree from a 2-3-4 to a 2-3 tree, you can convert parity seeking 2-3 tree’s into 2-3-4 trees by checking and handling case C *before* cases A and B:

node* fixInsert23(node* x) {

if (isRed(x->left) && isRed(x->right)) //doing case c first yields a 2-3-4 tree!

x = colorFlip(x);

if (isRed(x->left) && isRed(x->left->right)) //case a

x->left = rotL(x->left);

if (isRed(x->right) && isRed(x->right->left))

x->right = rotR(x->right);

if (isRed(x->left) && isRed(x->left->left)) //case b

x = rotR(x);

if (isRed(x->right) && isRed(x->right->right))

x = rotL(x);

return x;

}

I’m unsure if the original authors missed this point, or if they did, they didn’t feel it worth mentioning in a predominantly 2-3 tree focused article. Oddly enough, the code snippet they included in the original article is actually the 2-3-4 variant, despite being labeled as the 2-3 fix up routine! It is because they also discussed 2-3-4 tree’s in their paper I believe it to be a labeling error on their part, one which I only discovered after doing side by side analysis of my own implementations of parity seeking 2-3 Red/Black, bottom up 2-3-4 Red/Black, and bottom up 2-3 LLRB trees.

In any case, that’s what I got for today. The article I found this variant in is linked below as number 4, and contains a full treatment on deletion as well and is definitely worth the read.

If you’re interested in my implementations of bottom up red/black tree’s they are available on my github.

Until next time, Happy Hacking!

[1] Guibas, L., Sedgewick, R. “A di-chromatic framework for balanced trees”, 1978, https://sedgewick.io/wp-content/themes/sedgewick/papers/1978Dichromatic.pdf

[2] Wayne, K., Sedgewick, R. “Algorithms” 4th, ed. 2008. Ch 3 “Searching”

[3] Kohler, Eddie. “Left Leaning Red-Black Trees Considered Harmful” – https://read.seas.harvard.edu/~kohler/notes/llrb.html

[4]Gandhi, et. Al “Revisiting 2-3 Red-Black Trees with a Pedagogically Sound yet Efficient Deletion Algorithm: The Parity-Seeking Delete Algorithm”, 2022, https://arxiv.org/pdf/2004.04344.pdf

[5] Aho, Hopcroft, Ullman “Data Structures And Algorithms”, 1982, Addison-Wesley, Ch. 5, section 4, pp. 169 – 180

]]>In previous posts I have discussed various tree visualization algorithms, culminating in my TreeDraw project, which I use to generate the various binary search tree graphics in my posts. In this post I want to discuss the algorithms used to ensure a given tree satisfies all of the properties of a Red/Black or Left Leaning Red/Black tree, as well as gather performance statistics of various Trees. When combined with the TreeDraw algorithm, I am able to instantly gather information like the following when comparing two trees:

Red/Black Tree:

Tree Size: 63

Left rotations: 16

Right rotations: 19

Color Flips: 35

Insertions requiring rotations(%): 30.1587

Average Rotations / Insertion: 1.42105 (Right: 0.789474 / Left: 0.631579)

Max Single Insertion rotations: 2

+ Tree is black balanced.

+ Tree is valid 2-3-4 tree

+ Tree is a valid BST

-------------------------

Left Leaning Red/Black Tree:

Tree Size: 63

Left rotations: 38

Right rotations: 28

Color Flips: 42

Insertions requiring rotations(%): 46.0317

Average Rotations / Insertion: 1.68966 (Right: 0.724138 / Left: 0.965517)

Max Single Insertion rotations: 5

+ Tree is black balanced.

+ Tree is valid 2-3 tree.

+ Tree is a valid BST

This is very useful information, especially when fine-tuning an algorithm, or comparing differing implementations. The statistics regarding the number of rotations and color flips are very straightforward, of real interest is the final three tests performed on both trees. It is these three tests which determine if the provided tree is a *valid* Red/Black Tree (or Left Leaning Red/Black). But what is a valid Red/Black Tree?

On page 574 of “Algorithms in C++”(3rd. ed) Sedgewick provides the definition of a A Red/Black tree as: “a binary search tree in which each node is marked to be either red or black, with the additional restriction that no two red nodes appear consecutively. A balanced red-black BST is a red-black BST in which all paths from root to leaf have the same number of black nodes.” In the 4th addition, the Left Leaning invariant is introduced with the additional rule that no right child may be a red node(All red nodes “lean left”).

Ok there’s a bit to unpack there, lets start with the simplest: Red/Black Tree’s are binary search tree’s. At the very least, a tree must be a valid binary search tree, or we don’t even to need to bother with any more complicated tests.

All binary search trees must posses the “binary search tree property”, that is, a binary tree where the value of a given nodes key is greater then the key of its left child, and less then the key of its right child. We can check the validity of a binary search tree by performing this check on each node visited during a pre-order traversal, exiting if we encounter a node that doesn’t satisfy the BST property.

bool isBST(node* x) {

if (x == nullptr)

return true;

if (x->left && x->key < x->left->key)

return false;

if (x->right && x->right->key < x->key)

return false;

return isBST(x->left) && isBST(x->right);

}

This algorithm neatly exemplifies the paradigm we will be using for the other validation algorithms as well, mainly, if a provided node is null, we return true: an empty tree is *always* a valid tree. We then perform the test on the invariant we are validating, and then recur on the left and right branches of the current node.

Now that we’ve determined if the tree is a valid binary search tree, lets validate that it conforms to the red/black property laid out above.

We have *two* color invariants that we need to validate. First we must validate that the tree maps to a 2-3-4 tree by checking that know two red nodes occur consecutively. We already know how to do this, as we simply need to perform the same checks used to determine when to color flip and/or rotate!

bool is234(node* x) {

if (x == nullptr)

return true;

if (isRed(x->left) && isRed(x->left->left))

return false;

if (isRed(x->left) && isRed(x->left->right))

return false;

if (isRed(x->right) && isRed(x->right->right))

return false;

if (isRed(x->right) && isRed(x->right->left))

return false;

return is234(x->left) && is234(x->right);

}

The code for is23() which is used for validating Left Leaning Red/Black trees is exactly the same, with the addition of one more test to ensure that all red nodes lean left. The full code is available on my github and linked at the bottom.

This is actually the trickiest of the three cases to validate. We want to know if every path in the tree has the same number of black nodes. At first glance this is actually much easier than it appears, but it requires a few insights to arrive at the proper solution. Like all things, it helps to take a step back, and try to simplify things. Before we can tell if all paths have the same number of black nodes, we need to know how many black nodes *one* path has. We know we the path to the min node in a binary search tree is obtained by following the left pointer from the root to a null node, so counting the black nodes we encounter while traversing to the left most node gets us our baseline.

bool blackBalanced() {

int numBlack = 0;

node* x = root;

while (x->left) {

if (!isRed(x)) numBlack += 1;

x = x->left;

}

return blackBal(root, numBlack);

}

Now we can take this baseline and perform.. you guessed it, a traversal of the tree! As we traverse *down* a path, we subtract one from our black count when we encounter a black node. Any time we arrive at a leaf, numBlack should equal zerom, otherwise the two paths have differing numbers of black nodes and are thus unbalanced. As the stack unwinds and we backtrack up the path the black count is restored before continuing back down another path in the tree.

bool blackBal(node* x, int numblack) {

if (x == nullptr)

return numblack == 0;

if (!isRed(x)) numblack--;

return blackBal(x->left, numblack) && blackBal(x->right, numblack);

}

In order for a tree to be considered a valid red black tree it needs for all three of the above methods to return as true, this is as simple as combining the three tests into one boolean expression:

bool isRedBlackTree() {

return isBST(root) && is234(root) && isBalanced();

}

Using these methods you can know play around with different balancing strategies, fine tune your own red/black algorithms, or just explore the effects of applying diferent rotations, and validate if the result is a red/black tree. Until next time, happy hacking!

]]>In response to this perceived complexity, both AA trees and Left-Leaning Red/Black trees were introduced, which simplified their implementations by enforcing asymmetry in their balancing. What do I mean by “enforcing asymmetry”? In a “Traditional” red black tree, a red node can appear as either a left or right child of a black node – or even as both if they are leaf nodes. In a “Left Leaning Red/Black Tree” however, a red node can *only* occur as the left child of a node. AA tree’s, an earlier adaptation of Red/Black trees perform this same trick, except slanting to the right. This reduces the number of cases we need to handle during restructuring essentially in half. But does it *really*?

Unfortunately this perceived simplicity is at the cost of performance. Despite having fewer cases to deal with when it comes to restructuring, they have to perform those few cases more often, as their are fewer valid asymmetric red/black tree’s than there are symmetric. Take as an example the two trees pictured above. Both tree’s were built from the same input, but the left leaning variety required 17 right rotations to the red/black tree’s 11 right rotations. They don’t fare any better for left rotations either, requiring 29 left rotations compared to the red/black tree’s 10 left rotations. That’s three times the work for a tree that ends up less balanced.

One often overlooked property of bottom up insertion allows us to simplify our conceptual view of Red/Black trees. If you’re one of those people who just couldn’t wrap their head around the 2-3-4 tree abstraction from which they derive, you’ll be happy to know we that can ignore it entirely utilizing *only* the red/black framework. Some people find this easier, though I still recommend trying to grok the relationship to 2-3-4 trees. Regardless of which way were looking at it, a valid Red/Black tree must maintain the following properties:

- The root node must be black
- No red node can be the child of another red node
- All paths from the root to the node has the same number of black nodes.

To add a node bottom up in a Red/Black tree, you begin by performing a regular BST insertion at a leaf. All newly inserted nodes in a Red/Black tree are colored red. As usual, null links are considered black. If the newly inserted node also happens to be the root node, we simply color it black, and we’re done. In fact, we end *every* insertion by setting the root’s color to black.

node* putRB(node* x, K key, V value) {

if (x == nullptr) {

count++;

return new node(key, value);

}

if (key < x->key) x->left = putRB(x->left, key, value);

else x->right = putRB(x->right, key, value);

return fixInsert(x);

}

void insert(K key, V value) {

root = putRB(root, key, value);

root->color = black;

}

Inside fix insert we’re going to use the same isRed() and colorFlip() methods as for left leaning red black trees. For rotateLeft() and rotateRight() I’ve used the same methods as in my other various implementations of red black tree’s and AVL trees. This separates the logic of node coloring from rotations. No mixing of concerns!

bool isRed(node* x) {

return (x == nullptr) ? false:(x->color == red);

}

node* colorFlip(node* x) {

x->color = !x->color;

x->left->color = !x->left->color;

x->right->color = !x->right->color;

return x;

}

void swapColors(node* x, node* y) {

bool tmp = x->color;

x->color = y->color;

y->color = tmp;

}

node* rotateRight(node* x) {

node* y = x->left;

x->left = y->right;

y->right = x;

return y;

}

node* rotLeft(node* x) {

node* y = x->right;

x->right = y->left;

y->left = x;

return y;

}

If one so desired, they could replace all calls to swapColors() by adding the following to both rotateLeft() and rotateRight(). The direct comparison with Left Leaning Red/Black trees below used this method.

y->color = x->color;

x->color = red;

All of the tri-node restructuring takes place in the fixInsert() method. The decision of when to rotate is determined by the color of the newly inserted nodes uncle, in combination with the orientation of the node with respect to it’s parent and grandparent. If a newly inserted nodes parent and it’s sibling are both red, we can perform a colorFlip() on the nodes grandparent, balancing the tree without requiring any rotations. Otherwise we have two symmetrical cases to handle for when the nodes uncle is red. We have to address these cases in the event the the current nodes left child is red and right child is black, as well as for the inverse case when the nodes right child is red and left child is black:

node* fixInsert(node* x) {

if (isRed(x->right) && isRed(x->left))

x = colorFlip(x);

if (isRed(x->left)) {

x = handleLeftIsRed(x);

}

if (isRed(x->right)) {

x = handleRightIsRed(x);

}

return x;

}

The code for handleLeftIsRed() and handleRightIsRed() are mirror images of each other. They each address two conditions. The first condition is that both the node and its parent are aligned. This can be balanced by swapping the parent’s color with its grandparent, and doing a single rotation on the grandparent. The other case is when the node and its parent are not aligned and requires a double rotation to bring the tree back in to balance.

node* handleLeftIsRed(node* x) {

if (isRed(x->left->left)) {

swapColors(x, x->left);

x = rotateRight(x);

}

else if (isRed(x->left->right)) {

x->left = rotateLeft(x->left);

swapColors(x, x->left);

x = rotateRight(x);

}

return x;

}

node* handleRightIsRed(node* x) {

if (isRed(x->right->right)) {

swapColors(x, x->right);

x = rotateLeft(x);

} else if (isRed(x->right->left)) {

x->right = rotateRight(x->right);

swapColors(x, x->right);

x = rotateLeft(x);

}

return x;

}

And that’s all there is to it. I had formatted the code in a way to make it as understandable as possible, but the balancing code can be written inline with the insert operation as is done with Left Leaning Red Black Tree’s in “Algorithms”(4th ed).

When you comparing the two algorithms side by side, the idea that the left leaning variety is any magnitude simpler evaporates. Don’t get me wrong: I have nothing but the utmost respect for Dr. Sedgewick, and one can argue that being a professor of computer science, his primary concern is the presentation of material in a way that is agreeable to his students. However, I believe this can be done *without* compromising the integrity of the algorithms being presented. Further, I believe it is actually to the detriment of future computer science students to advocate for the learning of conceptually simpler but less efficient algorithms, __ especially__ when “conceptually simpler” equates to the removal of two If statements in

Until next time, happy hacking!

]]>The academic literature is fairly divided when it comes to the use of parent pointers in search tree structures. Some resources, such as CLRS[1] include them implicitly in all of their examples. Others, such as Sedgewick[2] disavow their use completely.

There are pro’s and con’s to both sides of the argument. The big one for the anti-parent pointer side of the argument is that they waste space by needing an extra pointer in every node. However, including a parent pointer in your node structures means that iterators for that tree can implemented in O(1) space and O(1) complexity. To implement iterators *at all* – let alone *efficiently* in a tree without parent pointers requires the use of so called “Fat Iterators” and are the subject of this post.

Iterators work by breaking up the act of traversing a container into it’s individual steps, so that the traversal can then be performed step-wise. Tree based structures are by their very nature non-linear. This means that in order for an iterator to know where to go next, for a tree it also needs to know where it came from. In a tree with parent pointers, we can simply follow the link back to the nodes parent.

In a tree with out parent pointers, we need to cache the path that was followed to get to the current node. So-called “Fat” iterators work by saving this path in a stack. If the current node is a leaf node, we pop the first node off the stack and check if it has an unexplored child, if it does, we move down that branch, saving the new path on to the stack. In this way, the iterator can explore the tree in the same step-wise manner as a tree containing a parent pointer.

Search Tree’s maintain a total ordering which can be realized by performing an in-order traversal of a BST. The “natural” form of stack based traversal for binary trees performs a pre-order traversal, but we want to retrieve the items in sorted order. In-order traversal works by first visiting the left child, then process the current node, and finally visiting the right child. When written recursively, it is a very simple algorithm.

void inorderTraversal(bstnode* node) {

if (node != nullptr) {

inorderTraversal(node->left);

processNode(node);

inorderTraversal(node->right);

}

}

To do the same iteratively, we need to use an explicit stack in place of the call stack utilized by the recursive version. We begin by initializing the stack with the path from the root to the left most node.

void inorderIter(bstnode* node) {

stack<bstnode*> sf;

bstnode* curr = node;

while (curr) {

sf.push(curr);

curr = curr->left;

}

With our stack initialized, we loop until the stack is empty. During each iteration of the loop the top node is removed from the stack and processed. Once processed, we move to the current nodes *right* child, and once again perform a traversal to the left most child of the subtree rooted at current node, saving the path to the stack. Once this loop is complete, all nodes will have been processed in an in-order ordering. (Say that ten times fast…)

while (!sf.empty()) {

curr = sf.top();

sf.pop();

processNode(curr);

curr = curr->right;

while (curr) {

sf.push(curr);

curr = curr->left;

}

}

}

int main() {

string sed = "abstiteratorexample";

bstnode* root = nullptr;

for (char c : sed) {

root = put(root, c);

}

inorderRec(root);

inorderIter(root);

}

max@MaxGorenLaptop:~/cppEx$ ./a.out

a a a b e e e i l m o p r r s t t t x

a a a b e e e i l m o p r r s t t t x

max@MaxGorenLaptop:~/cppEx$

Now that we see how we can traverse the tree without recursion, it’s time to turn this method of traversal into an iterator object. An iterator needs to support at the very least, the following operations:

- Initialization – the iterator should be directly ready for use upon being instantiated.
- Data access – we need to be able to retrieve the data held at the position in the tree represented by the iterator.
- Traversal – the iterator must have a way to move to the next position in the container.
- Existence – A method for determining if the iteration has completed or should continue.

I’ve covered enough articles on iterators, that if you want to implement an STL compatible operator, you can easily adapt the following interface. To facilitate the required operations, our Iterator will utilize the following interface:

template <class K>

class Iterator {

private:

stack<bstnode<K>*> sf;

public:

Iterator(bstnode<K>* root);

bool hasNext();

void next();

K get() const;

};

Looking at the stack based traversal code we just covered, its not hard to see which pieces of code will go where in order to implement the required methods. To initialize our iterator, we pass the root of the tree to be traversed to the constructor, which initializes our stack with a path to the left more child.

Iterator<K>::Iterator(bstnode<K>* root) {

bstnode<K>* t = root;

while (t) {

sf.push(t);

t = t->left;

}

}

Many of the options now translate directly to stack operations. To check if we have complete iteration of the tree, we check if there are any nodes left on the stack to explore:

bool Iterator<K>::hasNext() {

return !sf.empty();

}

Similarly, to retrieve the value of the current iterator, we simply retrieve the element from the node held on the top of the stack. In an ordered container, you want __immutable__ access to the key-value, as changing it could invalidate the search property. If you’re tree hold’s Key-Value pairs, having the value be mutable is fine however.

template <class K>

K Iterator<K>::get() const {

return sf.top()->key;

}

To move to the next position in the tree, we want the in-order successor of the current node, which we get by retrieving the *left*-most child of the current-nodes *right* child. Being that the current node is the top most item currently on the stack, we can write this method as follows:

template <class K>

void Iterator<K>::next() {

bstnode<K>* curr = sf.top()->right;

sf.pop();

while (curr) {

sf.push(curr);

curr = curr->left;

}

}

For reasons I will never understand, the STL stack’s pop() operation is returns void, or else we could just write curr = sf.pop()->right instead of the ugly top()/pop() idiom you get with the STL.

Anyway, now that our Iterator is complete, if you wanted to make it STL compatible, you would just need to implement the appropriate operators. At the least, you’d need operator*, operator++, operator++(int), operator==, and operator!=. I will leave those as “an exercise to the reader”.

With our iterator complete we can test it using our previous example from above:

int main() {

string sed = "abstiteratorexample";

bstnode<char>* root = nullptr;

for (char c : sed) {

root = put(root, c);

}

Iterator<char> it = Iterator<char>(root);

while (it.hasNext()) {

cout<<it.get()<<" ";

it.next();

}

cout<<endl;

}

max@MaxGorenLaptop:~/$ ./a.out

a a a b e e e i l m o p r r s t t t x

max@MaxGorenLaptop:~/$

That’s all i’ve got for today. Until next time, Happy Hacking!

]]>So what’s can be done? The answer – it turns out – is iterators! An iterator is an object that abstracts the concept of a “position” in a collection, and as it turns out, they offer a clean way of signifying that an item is *not* present in the collection: Return an iterator to a position “one past the last item in the collection.” In the C++ standard library, this is denoted by the special iterator returned by the class method end().

#include <iostream>

#include <map>

#include <vector>

using namespace std;

int main() {

map<string, int> nameAndYear = { {"C++", 1983}, {"Java",1996}, {"Smalltalk", 1980},{"python", 1993}, {"javascript", 1996} };

vector<string> names = {"C++", "Smalltalk", "perl", "Java", "ruby" };

for (string lang : names)

if (nameAndYear.find(lang) != nameAndYear.end())

cout<<lang<<" was released in "<<nameAndYear.at(lang)<<endl;

else cout<<"Sorry, no year known for the release of "<<lang<<endl;

return 0;

}

Aside from providing us with a clean way to handle search misses, proper implementation of iterators also allows us to use our collection with C++’s foreach loop, as shown above looping over the names vector. With these reasons in mind, lets implement an iterator for the B+ Tree discussed in my previous post!

If we want to be able to use our iterators with C++’s foreach loop, then our iterators must conform to the interface that C++ expects them to provide. Unlike languages such as Java, iterators are manipulated almost entirely through operator overloading. Because B+ Tree’s are ordered collections, we want our Iterator to perform an in-order traveral of the elements. In a B+ Tree’s, this means traversing the bottom level of the tree, which is conveniently arranged as a double linked list, allowing us to iterate both forwards and backwards. To take full advantage of this, our Iterator should implement the following interface:

template <class T, class T2>

class BPIterator {

private:

BPNode<T,T2>* node;

int idx;

public:

BPIterator(BPNode<T,T2> ptr, int pos);

std::pair<T,T2> operator*();

BPIterator operator++();

BPIterator operator++(int);

BPIterator operator--();

BPIterator operator--(int);

bool operator==(const BPIterator& it) const;

bool operator!=(const BPIterator& it) const;

};

In past articles I’ve covered implementing iterators for both linked and array based data structures. When I covered implementing iterators for hash tables with separate chaining, I detailed how to manage switching from an array based iterator to a linked iterator and back in order to traverse the hash table and its buckets. Similarly to that scenario, an Iterator to a B+ Tree will requires traversing a linked structure, and an array based structure as it process the leaf level of the B+ tree. While it may initially sound complicated, B+ Tree’s are actually the easiest tree to write an iterator for, looked at from a certain perspective, it is the same process as writing an iterator for an *unrolled* linked list.

The constructor takes a pointer to the node iteration should start on, as well as an integer for the index of the position in the nodes array to start at. These are used to initialize the private members of the iterator class.

class BPIterator {

private:

BPNode<T,T2>* node;

int idx;

public:

BPIterator(BPNode<T,T2> ptr, int pos) {

` node = ptr;

idx = pos;

}

We implement operator*() to return an std::pair of the key/value pair for the current position, Iterators are not kind and will not check if a pointer is null before dereferencing:

std::pair<T,T2> operator*() {

return std::make_pair(node->page[aptr].key, node->page[aptr].value);

}

Ok, so iterators aren’t *total* assholes, they’ll check for null pointers when they __have__ to, such as when we implement pre- increment and decrement:

BPIterator operator++() {

if (node != nullptr) {

if (aptr < node->size()-1)

aptr++;

else {

node = node->next;

aptr = 0;

}

}

return *this;

}

BPIterator operator--() {

if (node != nullptr) {

if (aptr > 0)

aptr--;

else {

node = node->prev;

if (node)

aptr = node->size()-1;

}

}

return *this;

}

Not muich that really bears discussion. Take note of how we check if the *next* position is valid before take any action, as it may require either changing of a node in addition to a resetting of the array index. Post- increment and decrement operators, as well as tests for equality are all implemented in the normal way:

BPIterator operator++(int) {

BPIterator it = *this;

++*this;

return it;

}

BPIterator operator--(int) {

BPIterator it = *this;

--*this;

return it;

}

bool operator==(const BPIterator& it) const {

return node == it.node && aptr == it.aptr;

}

bool operator!=(const BPIterator& it) const {

return !(*this==it);

}

And that rounds out our iterator class, nothing too fancy, certainly simpler than for a binary search tree! Now lets get it working in our B+ tree.

It’s not just our iterators that have to conform to C++’s expectations, our Collection must posses the appropriate interface as well. In reality, all we need to do is provide an iterator to the first element in the collection, name, imaginatively, begin(), and the equally inspiringly named end(), which points to an *imaginary* element one past the last. We should also provide const varieties of both.

template <class T, class T2>

class BPTree {

private:

/* other tree related code */

using iterator = BPIterator<T,T2>;

using bpnode = BPNode<T,T2>;

bpnode* min(bpnode* node, int ht);

bpnode* min(bpnode* node, int ht) const;

public:

iterator begin();

iterator end();

iterator begin() const;/ /one day ill figure out

iterator end() const; //what this const correct thing is all about.

/* other tree related code */

};

In order to provide in iterator to the first element in the collection, we need to a way to *get* to the first element in the collection. B+ Tree’s being ordered search tree’s, means we simply follow the left most branch until we are at the leaf, and is what the min() method is for:

bpnode* min(bpnode* node, int ht) {

if (ht == 0)

return node;

return min(node->at(0).child, ht-1);

}

With that helper in place, we implement begin() to simply return our iterator object, initialized to the left most leaf node, array position zero.

Similarly, we initialize end() with a reference to nullptr, and index -1:

iterator begin() {

return iterator(min(root, height), 0);

}

iterator end() {

return iterator(nullptr, -1);

}

We can re turn an iterator to any position in the tree, by simply instantiating an iterator object with a pointer to the node containing the element we want, as well as the index of the array position, This allows to cleanly implement search() as follows:

iterator find(K key) {

return search(root, key, height);

}

iterator search(bpnode* node, K key, int ht) {

if (ht == 0) {

int j = node->search(key);

if (j > -1) return iterator(node, j);

else return end();

}

return search(node->at(node->floor(key)).child, key, ht-1);

}

Now we can use the same familiar idiom of comparing the result of find against end() that I went over in the beginning of the post, while using our B+ tree in place of std::map. Another bonus to using iterators is the we also user opertor++ and operator– to quickly retrieve the elements near in order to the element we originally queried.

That’s All I’ve got for today, Until next time, Happy Hacking!

The two resources primarily referenced in the making of this post were:

[1] Gamma, Helm, Johnson, Vlissides, “Design Patterns: Elements of Reusable Object Oriented Software” – The famous “Gang Of Four” Book. Just read it.

[2] Plauger, Stepanov, “The C++ Standard Template Library” – This book is a (*very*) detailed look at how the standard template library was implemented, albeit early on.

[3] And for a full implementation of B+ Tree using iterators, check my github at: https://github.com/maxgoren/BPlusTree/

]]>Since Bayer & McCreight introduced the family of balanced search tree’s collectively known as “B Trees” in their 1972 paper[1], they have traditionally been used as a data structure for external storage devices, which is why they are very often used in implementing on-disk filesystems. Recent examples of filesystems using some form of B Tree are ReiserFS, btrfs, bcachefs, and even Microsoft’s NTFS, just to name a few. Another domain where B Tree’s are pervasive is in both relational *and* non relational databases. MySQL and PostgreSQL both provide B+ tree based indexing, for example.

The structure of the nodes make them particularly well suited for these kinds of tasks. Nodes in B Tree’s very have a very large fan out, leading to the trees themselves having a shallow depth. They are commonly described as “Bushy”, meaning values can be found in very few “probes”.

With the cost of memory experiencing a cost/performance curve reminiscent of what CPU’s have traditionally experienced, the non-optimal space usage of B Tree’s has become less of a limiting factor to their usefulness as in memory data structures. And people have been paying attention.

In 2013 Google released their opensource B Tree Map for C++ as a drop in replacement for the Red/Black Tree traditionally found in STL map implementations, as well as in the standard libraries of many popular programming languages. Bucking the trend alongside google, is the Rust programming language, which also makes use of a B Tree for the standard library Map and Set types.

According to CLRS[3], a B Tree is an ordered search tree who’s nodes contain between M/2 and M-1 keys, and M/2 and M children, and which has all of its leaf nodes at the same level which is the height of the tree. B Tree’s Are perfectly balanced and so perform all operations in O(log N).

The paper which originally presented B Tree’s listed several variations as possible optimizations. Two of those variants which have gained popularity are the B+ and B* trees. B+ Trees have become so popular that generally speaking, when people say “B Tree’s” they are usually referring to some form of B+ Tree.

So what exactly *is* a B+ tree, anyway? Well, it’s actually made up of (at least) two separate data structures. The *index set* is made up of a B Tree, and it is used to index the *entry set*. The entry set is a linked list of all the entry’s that the tree is used to efficiently navigate through. So in a quite literal sense, a B+ Tree is exactly that: a B Tree, Plus a linked list.

For the remainder of this post, when I say B Tree, I’m talking about B+ Tree’s except when specifically mentioned. B+ tree’s have several properties which offer advantages with regards to space usage and iteration compared to “traditional” B Tree’s, while simultaneously simplifying their implementation. B+ Tree’s uses it’s internal nodes as an index, to guide the search to the appropriate leaf node which stores a pointer to the actual data. Additionally, B+ Tree’s also link sibling nodes together in a linked list, which allows for fast in order iteration.

I have read many, *many* books on data structures and algorithms, and while B Tree’s are mentioned in practically single one of them, I have come across exactly *one* which includes a working example of B Tree insertion. In addition, each book describes them slightly differently. The most rigid definition of what constitutes a B Tree is thanks to Knuth(who else?). On the whole however, B Tree’s don’t have as nearly a rigid definition as AVL tree’s or Red/Black Tree’s, despite being more rigidly balanced. This leaves quite a bit of wiggle room and explains why everybody implements them a little differently.

In computer science, a “tree” is a recursively defined object. Take for example a binary tree: A binary tree is a tree, whos individual nodes are them selves binary trees. Obviously such a definition holds for B Tree’s as well, and we *could* implement B Tree’s similar to how we would a binary search tree, with nodes laid out as follows:

const int M = 4;

template <class T, class T2>

struct BPNnode {

T keys[M-1];

T2 vals[M-1];

int n;

bool is_leaf;

BPNode* children[M];

BPNode* next;

BPNode* prev;

};

This is very similar to the description provided in CLRS for B-Trees, and it *will* work, but using such a node layout creates much more work for ourselves. In the above example, we have three separate arrays of two different sizes, which makes inserting and removing keys/values/children from the tree much more complicated than it need be.

If we accept that one position in the key and value array may be “wasted” we can greatly simplify things by just make all three arrays the same size. If in addition we impose the restriction on our tree’s that M must be an even number of at least 4 we simultaneously simplify the logic for splitting node when an insertion causes an overflow. That lead’s us towards the following layout:

const int M = 4;

template <class T, class T2>

struct BPNode {

T keys[M];

T2 vals[M];

BPNode *children[M];

BPNode *next, *prev;

};

We can still do better. Since all three arrays are now the same size, and since the values in those arrays are related, we can create an object to represent the tuple<Key, Value, Pointer>, and our node well contain a single array of that object, as shown below:

const int M = 4;

template <class T, class T2>

struct BPNode; //needs forward declaration for use in BPEntry

//Our node well hold one array of size M of this object

//Instead of three arrays as in the example above

template <class T, class T2>

struct BPEntry {

T key;

T2 value;

BPNode<T,T2>* child;

};

//Our greatly simplified layout.

template <class T, class T2>

struct BPNode {

BPEntry<T,T2> page[M]; //key, value, child

int n; //number of entries in this page

BPNode *prev, *next; //linked list pointers to siblings

};

In a B+ Tree the internal nodes don’t contain any data pointers, so our internal nodes will use the BPEntry’s “child” pointer to point down the tree towards the proper leaf, leaving the value field empty. Conversely, as leaf node’s in a B+ tree don’t have child pointers, they __will__ use the value field, leaving the child pointer empty. In this way we handle the two “different” node types used for internal and leaf nodes.

By arranging our nodes in such a layout we have also serendipitously transformed each node of our B+ Tree to simultaneously be an ordered array based symbol table! This observation presents us with a decision: do we leave our nodes as *Plain Old Data* structures, or should we add a layer of abstraction, by implementing our B+ Tree nodes as “Pages”.

By viewing each node as a page, it gives us greater flexibility with how we structure the nodes. So long as a data structure provides the methods listed below for BPNode, we can use any data structure we want. An ordered array is the standard. My first implementation was a “leaky” abstraction of pages, by using the C++ friend keyword. I’ve since cleaned it up by adding appropriate accessors to the BPNode API.

const int M = 4;

template <class T, class T2>

class BPNode;

template <class T, class T2>

struct BPEntry {

T key;

T2 value;

BPNode<T,T2>* child;

BPEntry(T k, T2 v, BPNode<T,T2>* c) : key(k), value(v), child(c) { }

};

template <class T, class T2>

class BPNode {

private:

BPEntry<T,T2> page[M];

int n; // number of entries in page

BPNode* next; // for efficient level order

BPNode* prev; // traversal using O(1) extra space

BPNode* split();

public:

BPNode(int k = M);

int size();

bool isFull();

int floor(T key);

int search(T key);

BPNode* insertAt(BPEntry<T,T2> entry, int position);

BPNode* insert(BPEntry<T,T2> entry);

BPNode* removeAt(int position);

BPEntry<T,T2>& at(int position);

BPNode* leftSibling();

BPNode* rightSibling();

~BPNode() {

}

};

The interface for the node class is actually pretty straight forward. I’ll go through them all, giving extra attention where I feel it’s needed. The private members are our array of BPEntry structures, and the variable n, which is used to hold the number of entries currently in the node. The next/prev pointers are for pointing to the nodes siblings. These are used for efficient level order traversal, particularly of the leaf level, as this represents an in order traversal of the data stored in the tree. They are accessed through the leftSibling() and rightSibling() methods. The split() method is used during the insertion process, and I will explain more below.

The constructor is used for setting the initial entry count. The method size() returns the number of entries in the node, and isFull() is a quick test if the node has room for insertion or not.

template <class T, class T2>

int BPNode<T,T2>::size() const {

return n;

}

template <class T, class T2>

bool BPNode<T,T2>::isFull() {

return !(size() < M);

}

The next two methods are used for finding indexes in the page array. The method search(T key) searches for an exact match of the key, and if present returns its index in page[], upon failing it returns -1. The method floor(T key) is used for determining the position in page[] where an entry should be inserted.

template <class T, class T2>

int BPNode<T,T2>::search(T key) {

for (int j = 0; j < n; j++)

if (key == page[j].key)

return j;

return -1;

}

template <class T, class T2>

int floor(K key) {

int j;

for (j = 0; j < n; j++)

if (j+1 == n || key < page[j+1].key)

break;

return j;

}

Despite maintaining an ordered array, AND despite that fact that B+ Trees themselves generalize a binary search of an ordered collection, it is actually more efficient to use a simple linear search as shown above, for all but the largest sizes of M when using B+ Tree’s as an in-memory data structure. If you want to use binary search instead, it will obviously work just fine, but do you really need to use binary search for 8 or even 16 elements?

Whichever search method you choose, now that we can obtain the index of entries, we can implement the logic to insert and remove values from those given positions, which is exactly what insertAt() and removeAt() do. This is one area where bundling everything into page elements really leads to a concise implementation. On side node, if you wish to implement top down tree’s for thread safety reasons, you’ll want to move the split-on-overflow portion of these methods *out* of these methods.

template <class T, class T2>

BPNode<T,T2>* BPNode<T,T2>::insertAt(BPEntry<T,T2> entry, int j) {

for (int i = n; i > j; i--)

page[i] = page[i-1];

page[j] = entry;

n++;

if (isFull()) return split();

else return nullptr;

}

template <class T, class T2>

BPNode<T,T2>* BPNode<T,T2>::removeAt(int j) {

int i;

for (i = j; i < n; i++)

page[i] = page[i+1];

n--;

if (n == 0)

return next;

return this;

}

The removeAt(int) method is simple array deletion, that shuffles all of the elements to the right of the element being deleted over by one. If this causes the node to be come empty, the node replaces its self with its neighbors. I’ll go in to more detail on this when I cover deleting items from the B+ Tree.

Insert at places an entry at the provided position, moving all items over one space to make room for it. If an insertion does not cause a node to become full, we just return the node. If it does become full, we call the nodes split() method.

template <class T, class T2>

BPNode<T,T2>* BPNode<T,T2>::split() {

BPNode* new_node = new BPNode<T,T2>(M/2);

for (int i = 0; i < M/2; i++) {

new_node->page[i] = page[M/2+i];

}

n = M/2;

new_node->next = next;

if (next != nullptr)

next->prev = new_node;

next = new_node;

new_node->prev = this;

return new_node;

}

Split() allocates a new node, copies the rear half of the current nodes page[] array into the new node, and sets both nodes sizes to M/2. After that, the newly created node is hooked into it’s siblings as a linked list, and returned for inclusion in it’s parent.

BPNode* insert(bpentry& entry) {

int j = n;

while (j > 0 && page[j-1].key > entry.key) {

page[j] = page[j - 1];

j--;

}

page[j] = entry;

n++;

if (isFull()) return split();

else return nullptr;

}

The insert() method above does __not__ take an index argument, and instead places the item in its correct position with insertion sort. It might seem redundant to have two ways of inserting elements into the leaf, and indeed we could get away with one, but you’ll see why this is preferred when we cover insertion into the tree. Speaking of which, with these method’s in place we have what we need to begin implementing our B+ Tree, so let’s get to it.

So far we’ve only talked about the individual nodes of the tree. Now it’s time to take a step back and look at the bigger picture. The most common use case for in-memory B+ Tree’s is for use as a dictionary, and such our B+ Tree should implement the basic Dictionary ADT API, mainly insert, get, search, and remove. I’ve also decided to add STL compatible Iterators so we can use C++’s foreach loops on our B+ Tree.

template <class T, class T2>

class BPTree {

public:

using iterator = BPIterator<T,T2>;

private:

using bpnode = BPNode<T,T2>;

using nodeptr = bpnode*;

nodeptr root;

int height;

int count;

void printLevel(bpnode* node);

void printNode(bpnode* node);

bpnode* insert(bpnode* node, T k, T2 v, int ht);

bpnode* erase(bpnode* node, T key, int ht);

bpnode* splitRoot(bpnode* node, bpnode* new_node);

bpnode* mergeNodes(bpnode* a, bpnode* b);

bpnode* min(bpnode* node, int ht);

bpnode* min(bpnode* node, int ht) const;

iterator search(bpnode* node, T key, int ht);

void cleanup(bpnode* node);

T nullKey; //for use in blank entries.

T2 nullItem; // ^ ^

public:

BPTree();

BPTree(const BPTree& o);

~BPTree();

void insert(T key, T2 value);

void erase(T key);

iterator find(T key);

T min();

void sorted();

void levelorder();

iterator begin();

iterator end();

iterator begin() const;

iterator end() const;

BPTree& operator=(const BPTree& o);

};

I’ve introduced the full class definition, but this post will be primarily concerned with insertion and search, this post is already getting pretty long and deletion would make double it’s length. Remember when I said implementing those in-node methods was going to make implementing the tree it’s self much simpler? I wasn’t kidding. Let’s Take a look at implementing insertion.

Insertion begins by checking the level of the node we are currently processing. If we’re not at the bottom level, we find the correct child pointer for inserting our value into. Once we have reached the leaf level, we insert our new value into the leaf. If insertion causes an overflow the node calls it’s internal split method, returning the newly created node, who’s key is passed up to be inserted in the parent of the node it was split from.

template <class T, class T2>

void BPTree<T,T2>::insert(T key, T2 value) {

nodeptr new_node = insert(root, key, value, height);

if (new_node != nullptr) {

root = splitRoot(root, new_node);

}

}

template <class T, class T2>

typename BPTree<T,T2>::bpnode* BPTree<T,T2>::insert(bpnode* node, T key, T2 bpnode* insert(bpnode* node, K key, V value, int ht) {

int i, j;

bpentry nent = bpentry(key, value, nullptr);

if (ht == 0) {

return node->insert(nent);

} else {

j = node->floor(key);

bpnode* result = insert(node->at(j++).child, key, value, ht-1);

if (result == nullptr) return nullptr;

nent = bpentry(result->at(0).key, nullItem, result);

}

return node->insertAt(nent, j);

}

If at the root, the root is split, we call the splitRoot method, which is even simpler than the BPNode::split() method. It stores the current root in a temporary variable, ‘oldroot’, allocates a *new* root node of size 2, and points to the node returned by insertion, and the new root.

template <class T, class T2>

typename BPTree<T,T2>::bpnode* BPTree<T,T2>::splitRoot(bpnode* head, bpnode* new_node) {

nodeptr oldroot = head;

head = new bpnode(2);

head->at(0) = BPEntry<T,T2>(oldroot->at(0).key, nullItem, oldroot);

head->at(1) = BPEntry<T,T2>(new_node->at(0).key, nullItem, new_node);

oldroot->next = new_node;

HT++;

return head;

}

Searching is even easier. Again, we track our position in the tree by using the height of the tree. While it may seem hackish compared to say, an isLead flag as seen in many implementations, it is also one less variable for each node to both store, __and__ monitor. For return values, I prefer to use an iterator, as I’m a firm believer that the iterator pattern produces cleaner code. If you prefer, you could return a BPEntry object, an std::pair<T,T2> of key and value, or whatever else you feel like returning.

template <class T, class T2>

typename BPTree<T,T2>::iterator BPTree<T,T2>::find(T key) {

return search(root, key, HT);

}

template <class T, class T2>

typename BPTree<T,T2>::iterator BPTree<T,T2>::search(bpnode* node, T key, int ht) {

int j = node->floor(key);

if (ht == 0) {

j = node->search(key);

if (j > -1) return iterator(node, j);

else return end();

}

return search(node->at(j).child, key, ht-1);

}

By keeping our nodes in a linked list with their siblings, a simple traversal of the leaf level will yield our keys in sorted order. We’ll come back to this property in a moment, as it makes implementing an iterator for our tree’s about as easy it gets.

template <class T, class T2>

void BPTree<T,T2>::sorted() {

bpnode* x = min(root, HT);

while (x != nullptr) {

for (int j = 0; j < x->size(); j++)

cout<<x->at(j).key<<" ";

x = x->leftSibling();

}

cout<<endl;

}

template <class T, class T2>

typename BPTree<T,T2>::bpnode* BPTree<T,T2>::min(bpnode* node, int ht) {

nodeptr x = root;

while (ht-- > 0) {

x = x->at(0).child;

}

return x;

}

Before I cover iterators, I want to show how we can *also* use the sibling lists to efficiently implement level order traversal of the tree *without needing a queue*.

template <class T, class T2>

void BPTree<T,T2>::printNode(BPNode<T,T2>* node) {

if (node != nullptr) {

cout<<"(<";

for (int j = 0; j < node->size()-1; j++)

cout<<node->at(j).key<<", ";

cout<<node->at(node->size()-1).key<<">) ";

}

}

template <class T, class T2>

void BPTree<T,T2>::printLevel(BPNode<T,T2>* node) {

if (node == nullptr) return;

BPNode<T,T2>* x = node;

cout<<"[ ";

while (x != nullptr) {

printNode(x);

x = x->rightSibling();

}

cout<<"]"<<endl;

}

template <class T, class T2>

void BPTree<T,T2>::levelorder() {

nodeptr x = root;

int ht = HT;

while (ht >= 0) {

printLevel(x);

x = x->at(0).child;

ht--;

}

}

Using the above code for level both level order traversal and inorder traversal, we can check that our B+ Tree is working. We’ll Insert the characters “A B T R E E E X A M P L E W I T H A L O T O F K E Y S” as the key, and their position in the above list of characters as the associated value. Once the tree is built, we’ll call sorted() and levelorder().

A A A B E E E E E F H I K L L M O O P R S T T T W X Y

[ (A | M) ]

[ (A | E | I) (M | R) ]

[ (A | A | E) (E | F) (I | L) (M | O) (R | T | W) ]

[ (A | A) (A | B) (E | E) (E | E | E) (F | H) (I | K) (L | L) (M | O) (O | P) (R | S | T) (T | T) (W | X | Y) ]

That’s the tree built with M = 4, let’s see the effect of M = 8:

A A A B E E E E E F H I K L L M O O P R S T T T W X Y

[ (A | E | L | P | T) ]

[ (A | A | A | B | E | E) (E | E | E | F | H | I | K) (L | L | M | O | O) (P | R | S | T | T) (T | W | X | Y) ]

As you can see, if with a fan out of 8, the tree has only 2 levels. Searching in B+ Tree’s is obviously fast. In addition to being examples of balanced ordered search tree’s, and there nodes examples of ordered array key value maps, the leaf level of of B+ Tree’s are an example of an ordered, unrolled linked list! Talk about a hodge podge of data structures.

I think this is probably a good place to stop for today, In my next post ill be covering deletion, and implementing iterators for B+ Trees. Until next time, Happy Hacking!

You might look at the date of these resources with incredulity, I implore you to still read at least the first two papers thoroughly, as you will be hard pressed to find a better treatment of the topic.

[1] Bayer, R. and McCreight, E. “Organization and Maintenance of Large Ordered Indexes”, Boeing Co. 1972 (or 1970, depending on source) – This is the paper that introduced the B Tree family of tree’s that Bayer And McCreight developed while they were working at Boeing. Despite it’s age it is definitely worth a read. The algorithm flow charts are a nice touch you don’t see much anymore.

[2] Comer, Douglas “The Ubiqitous B Tree”, 1979, ACM Library. – Another of what’s considered “Seminal” works on the topic of B and B+ trees. A very thorough treatise on the topic, as well as the state of the art in 1979.

[3] Cormen, Et al. “Introduction to Algorithms”, ch. 18 – This focuses on traditional B Tree’s, as suited to external storage mediums.

[4] Sedgewick, R , “Algorithms, 4th ed”. ch. 6 – this gives a high level description implementing of B Tree’s using the “Page” data structure abstraction, and partially inspired the code above.

[6] My full implementation, including iterators can be found on my github: https://github.com/maxgoren/BPlusTree

]]>Unfortunately, the same properties that allows efficient lookups, is also incredibly *inefficient* when it comes to memory usage: this is one bulky data structure. The reason for this is that despite us using the trie to store strings as keys, the internal representation of those strings in a trie has a node to represent each individual character in the string, as well as a collection of pointers to its children – one for every possible character of our input alphabet!

#define ALPHASIZE 256 //there's 256 representable characters in ASCII

struct TrieNode {

char letter;

bool endOfString;

TrieNode* next[ALPHASIZE];

};

That’s not to say this a trait shared by *all* tries. As far as forest’s go, the trie family is diverse. On the complete opposite end of the spectrum from the prefix trie, is the family of “Digital Search Tries”, which includes amongst others, ternary search tree’s, and PATRICIA tries. What they gain in space efficiency, they unfortunately lose in difficulty of implementation(disregarding TST’s).

struct PatNode {

unsigned int b; //Patricia Trie's store single bit's as keys.

PatNode* left; //in addition to being binary trees, which

PatNode* right; //makes them very space efficient tries

};

Thankfully, their usefulness was recognized and finding efficient ways to implement tries has been an area of active research in computer science for quite some time. In this post I will be covering my own implementation of a Compressed Trie. The data structure I describe results in trees with the same split-key compressed paths as the PATRICIA algorithm produces, while loosening the restriction on being a binary tree, and a different way of deciding on making those splits. In recognition of PATRICIA tries inspiration, I have decided to call them SARLA Tries – for “Split And Re-Label” – the two fundamental operations performed.

Looking at images of tries you may have noticed that a large majority of the nodes have a branching factor of one despite this low number of child nodes, man trie implementations allocate an array large enough to contain pointers for the entire input alphabet. The Trie pictured below has 26 child pointers, one for each uppercase letter, despite none of the node having more than two children! This is quite obviously very wasteful – especially considering each letter get its own node.

The reason for the one character per node implementation is that in a trie like the one pictured above, it’s not actually the node that is labeled. Tries have labeled *edges* and it is the concatenation of those edges by following them that we use to test for existence of a key in the trie. While this makes their implementation simple, it makes their memory usage extraordinary. Wouldn’t it be great if we could make the tree pictured above look something more like this? That’s exactly what SARLA Tries do.

By converting edge labels to node labels, and combining characters that only have one child to form new substring keys we can drastically reduce the memory footprint of the trie while still maintaining efficient lookups.

The first thing we need to do is change the Node representation for our Trie. Previously, we were concerned with only one letter at a time, but now we will be doing substring comparisons instead of character comparisons. To accommodate this change, the nodes will have strings for keys instead of chars.

struct CTNode {

string key;

map<char, CTNode*> next;

CTNode(string k = "") {

key = k;

}

};

As far as managing the child pointers go – i’m leaving that implementation detail up to you, though a few good choices are self balancing BST’s, hash tables, or any other efficient way of mapping a character key to a pointer you devise. For simplicities sake in this post, i’ll be using std::map. I believe that an LCRS rep binary tree would be a fantastic candidate for this particular trie, though i have only implemented insert and search for that particular implementation so can’t speak on its performance.

class CompressedTrie {

private:

using link = CTNode*;

link head;

int wordCount;

link insert(link node, string key);

bool contains(link node, string key);

public:

CompressedTrie();

void insert(string word);

bool contains(string word);

void levelorder();

};

There is a header node that’s only purpose is to route the algorithm to the correct subtree based on the first character of the key. Once on the proper subtree, path compression takes place top-down during the insertion phase. If we arrive at a null link we create a new node that will contain the remainder of the key that has not yet been consumed. At each node we compare our key to the current nodes key, unlike in uncompressed tries we have to check the keys for *partial* matches.

CTNode* insert(CTNode* root, string word) {

if (root == nullptr) {

return new CTNode(word);

}

int i = 0, j = 0;

link x = root;

while (i < word.length() && j < x->key.length())

if (word[i] == x->key[j]) {

i++; j++;

} else break;

string newWord = word.substr(i);

if (j == x->key.size()) {

x->next[newWord[0]] = insert(x->next[newWord[0]], newWord);

} else {

x = split(x, word, newWord, i, j);

}

return x;

}

There’s two special cases for insertion we need to address. First, if while comparing the keys we consume the entire __nodes__ key, but __not__ the word being inserted we continue by:

1) Extract the substring comprising the non-consumed letters of the input word.

2) insert the extracted subtring in the current node by calling insert recursively on the current node, with the sub string as the key.

For example, if we were inserting the word “newspaper” into a trie and the ‘n’ node is already occupied by the word “news”, we would continue by then inserting the substring “paper” into ‘p’ link of the current node(‘news’). Likewise, for the word ‘newsday’, we would continue by inserting ‘day’ into the ‘d’ position of the ‘news’ node.

The second special case is If the keys diverge. This case is handled by the split method, which performs the the extracting of substrings and re-distribution of child nodes. If when comparing our insertion key to the current node, the keys diverge:

1) we create a new node using the characters that did match as it’s key, and insert it in the current nodes __parent__, (Split)

2) the substring created from the leftover characters in the current node’s key will become the current nodes new key (Re-Label).

This preserves any splits which occurred *after* the new key being inserted diverged. In this case, the tree actually grows *upwards* similar to a B-tree, instead of downwards when adding non-splitting keys.

CTNode* split(CTNode* x, string word, string newWord, int i, int j) {

string newkey = x->key.substr(0, j);

string rest = x->key.substr(i);

CTNode* t = new CTNode(newkey);

x->key = rest;

t->next[rest[0]] = x;

if (newWord.size() > 0)

t->next[newWord[0]] = insert(t->next[newWord[0]], newWord);

return t;

}

Keeping with our example from before, if we now insert the word “never” into our collection of words, the split() method gets called, resulting in the following trie:

An additional bonus of using the split/relabeling algorithm is that it doesn’t overwrite keys that are prefixes of longer keys that get inserted. Instead, they are preserved by splitting the key being inserted to create a node from the words non-matching suffix.

Now that we can insert strings into our trie, we must also be able to retrieve them. This process is *very* similar to how it is performed in in a normal trie, We perform char-by-char comparisons of the keys, the difference being we have to chase far, far, fewer pointers in the process which makes this much faster than one your typical one-char-per-node implementations.

bool contains(string str) {

CTNode* x = head->next[str[0]];

int j = 0;

while (x != nullptr) {

int i = 0;

while (i < x->key.size() && j < str.size()) {

if (str[j] != x->key[i])

return false;

i++; j++;

}

if (j == str.size())

return true;

if (x->next.find(str[j]) == x->next.end())

return false;

else {

x = x->next[str[j]];

}

}

}

To traverse the tree, you build the strings incrementally, outputting the full string upon reaching a leaf node. An in-order traversal will yield the keys in lexicographically sorted order:

void traverse() {

for (auto p : head->next) {

string str = "";

preorder(p.second, str);

}

}

void traverse(nodeptr h, string& sofar) {

if (h == nullptr) return;

string curr = sofar + h->key;

if (h->next.empty()) {

cout<<curr<<endl;

} else {

for (auto child : h->next) {

traverse(child.second, curr);

}

}

}

If at first glance you thought that was a preorder traversal I wont fault you for it, but take my word for it, it’s an in-order traversal.

Counter to just about every other tree based data structure, erasing a key is fairly straight forward. The only nodes we ever have to remove from the tree is leaf nodes, as every node on the path *above* the leaf being removed is shared by a another entry.

The one tricky detail about removing keys from a trie – and this is true for any indexing data structure – erasing keys *will* leave artifacts behind: keys higher on the path whos descendants have been removed and who’s keys no longer match any previously inserted records. This is a side-effect of path compression being a one way algorithm.

nodeptr erase(nodeptr node, string key, string subkey) {

if (node == nullptr)

return node;

dbg.onEnter("Enter node " + node->key + " with key + " + key);

if (node->key == key && node->next.empty()) {

return nullptr;

}

int i = node->key.length();

string remainder = key.substr(i);

node->next[key[i]] = erase(node->next[key[i]], remainder, node->key.substr(0, i));

dbg.onExit("Exiting: " + node->key);

return node;

}

So there you have it: Compressed Tries without the fuss. That’s all I’ve got for today, Until next time happy Hacking!.

The wikipedia site on Radix Tree’s is where I got the list of ‘r’ words used in this post. It also has a good description of compressed tries, all be it in their binary form. https://en.wikipedia.org/wiki/Radix_tree

“Algorithms in C++” (3rd. ed) by Robert Sedgewick has a very thorough chapter on Radix searching that outlines various Trie based structures.

A full implementation is available on my github

]]>When it comes to non-manual memory management as you find in languages such as Java, Perl, and Lisp, it generally falls into one of two broad categories: Garbage Collection and Reference Counting. Some people consider Reference Counting its self to be a form of garbage collection and while its mostly a matter of semantics, they work in fundamentally different ways. Ultimately they are both forms of automatic dynamic memory management with different positives and negatives.

Anyone who has done any non trivial programming with C has encountered malloc(), the C language equivalent to C++’s operator new. It is used to allocate objects on the heap. These objects are different than objects which are allocated on the stack, in that when these object go out of scope their *memory* is not automatically returned to the system. Returning the memory used by an object that is allocated on the heap __must__ be done explicitly either through the use of free() in C, or operator delete in C++.

Failure to free an objects memory when it goes out of scope is the cause of memory leaks – one of the major arguments for using garbage collected programming languages since this step is handled for you. If you have experience using both C++ and Java then I’m sure you noticed that creating an object is done the same way in both languages: through the use of operator new. When you are done with an object in C++ however you need to do the extra step of deleting that object, where in Java you don’t have to do anything. This is because Java uses a Garbage Collector.

“But computers are stupid!” you might be thinking “how would *they* know when an object isn’t being used anymore?”. There are many different types of garbage collectors, each with their own algorithms for reclaiming unused memory. The algorithm I will be discussing falls into the category of “tracing garbage collectors”.

The purpose of a garbage collector is to offload the work of freeing unused memory back to the system. With that being said, “Garbage Collector” is somewhat of a misnomer. Object’s don’t magically appear, and neither do they magically become “garbage”. Object’s have a life time, from object creation, to while it is in use, and when it is no longer needed. Garbage collectors are involved through the entire life cycle – not just when the objects become garbage.

Appropriately named or otherwise, there is another matter of more importance: automatic memory management is not free, there is a cost to be paid in both time and space complexities. Each object managed by a tracing garbage collector needs some kind of “in use” flag. Additionally, the GC manages a list of pointers to each object created.

Aside from this additional memory usage, tracing garbage collectors periodically block normal program execution to perform their various GC tasks, which can impact overall program performance.

To determine when objects become garbage, the collector will periodically inspect all objects that have been created and compare them to all objects that are in use. The set difference of the two groups are the objects that can be freed. This is possible because because of the list of created objects the allocator/garbage collector maintains. How the “objects in use set” is determined is application specific. Let’s create a basic garbage collected Binary Search Tree to make these ideas a bit more concrete.

To implement our GC’d BST we first need to implement our Garbage Collected Allocator, and the BST Node type who’s lifetime the allocator/garbage collector will be managing for us. As you can see, we’ve added the _gc_mark field to the node, this will be used for flagging garbage to be freeed.

template <class T>

struct GC_BTNode {

//Normal Binary Tree Node members

T info;

GC_BTNode* left;

GC_BTNode* right;

//for GC use

bool _gc_mark; //Is this object in use?

};

The next data structure is going to be for a “Roots” list. Since our GC’d objects are BST nodes, we can determine which of them are still in use by traversing any BST’s created starting from their root which we ill conveniently keep handy for just such purposes!

Now we can focus on the allocator. The allocator will also perform the garbage collection, but we have to create objects before we can worry about deleting them.

template <class T>

class GC_Allocator {

private:

vector<GC_BTNode<T>*> rootList; //list of roots of tree's in use

vector<GC_BTNode<T>*> nodeList; //objects created list

public:

GC_Allocator() {

}

GC_BTNode<T>* allocNode(T info, GC_BTNode<T>* left, GC_BTNode<T>* right) {

GC_BTNode<T>* tmpNode = new GC_BTNode<T>;

tmpNode->info = info;

tmpNode->left = left;

tmpNode->right = right;

//all nodes start with mark set false.

//and are immediately added to nodesList

tmpNode->_gc_mark = false;

nodeList.push_back(tmpNode);

return tmpNode;

}

void addRoot(GC_BTNode<T>* root) {

rootList.push_back(root);

}

};

As you can see we have methods for allocating nodes, and adding trees to the in use list. allocNode() will be replacing the use of operator new in our BST class.

template <class T>

class BST {

private:

GC_Allocator<T>* allocator;

using nodeptr = GC_BTNode<T>*;

nodeptr root;

int count;

nodeptr put(nodeptr h, T info) {

if (h == nullptr) {

count++;

return allocator->allocNode(info, nullptr, nullptr);

}

if (info < h->info) h->left = put(h->left, info);

else h->right = put(h->right, info);

return h;

}

/* additional code for

BST traversal, and for erasing elements

goes here */

public:

BST(GC_Allocator<T>* alloc) {

allocator = alloc;

root = nullptr;

}

void insert(T info) {

if (root == nullptr) {

root = allocator->allocNode(info, nullptr, nullptr);

allocator->addRoot(root);

count++;

} else {

root = put(root, info);

}

}

void traverse();

void erase(T info);

};

I’ve only shown the code for class definition so you can see that the allocator is passed as a pointer to the BST class, and that BST specific code is no different from normal except that we are using our custom allocator to create nodes, as well as adding the root of the tree to the root list when it is established.

Speaking of our custom allocator, we’re now ready to add the garbage collection code. Garbage Collection will take place in two phases: the mark phase, which finds all reachable nodes. And the sweep phase, which free’s the memory used by any nodes found to no longer be in use.

template <class T>

class GC_Allocator {

private:

vector<GC_BTNode<T>*> rootList;

vector<GC_BTNode<T>*> nodeList;

void mark_node(GC_BTNode<T>* node);

void mark();

void sweep();

public:

/*

Allocator specific code shown previously

*/

void gc() {

cout<<"[GC START]"<<endl;

mark();

sweep();

cout<<"[GC END]"<<endl;

}

};

The mark phases begins with a call to the aptly named mark() method, which traverses the rootList of created binary search tree’s. It then calls mark_node() on each of those BST’s which does a preorder traversal, of the tree to set each nodes _gc_mark flag.

void mark_node(GC_BTNode<T>* node) {

if (node != nullptr) {

node->_gc_mark = true;

mark_node(node->left);

mark_node(node->right);

}

}

void mark() {

for (GC_BTNode<T>* x : rootList) {

mark_node(x);

}

}

Pretty straight forward, right? Implementing sweep is just as simple. Sweep is a simple traversal of the nodeList, removing any node who wasn’t found to be reachable in the previous phase. The rest of the nodes get to live on in the next generation.

void sweep() {

int freedCnt = 0;

vector<GC_BTNode<T>*> nextGen;

for (auto& node : nodeList) {

if (node->_gc_mark)

nextGen.push_back(std::move(node));

else delete node;

}

freedCnt = nodeList.size() - nextGen.size();

nodeList = nextGen;

std::for_each(nodeList.begin(), nodeList.end(), [&](GC_BTNode<T>* h) { h->_gc_mark = false; });

cout<<freedCnt<<" items freed."<<endl;

}

And that’s it! All thats left to do is create a BST, delete some nodes without manually managing their memory, and watch the GC manage it for us!

int main() {

GC_Allocator<char> alloc;

BST<char> bst(&alloc);

string str = "GarbageCollectedBST";

for (char c : str)

bst.insert(c);

bst.print();

bst.erase('c');

bst.erase('S');

bst.print();

alloc.gc();

bst.print();

return 0;

}

max@MaxGorenLaptop:~$ g++ MarkSweep.cpp -o ms

max@MaxGorenLaptop:~$ ./ms

G C B a S T r b a g e c d e e o l l t

Delete 'c'

Delete 'S'

G C B a T r b a g e d e e o l l t

[GC START]

S is unreachable, freeing memory.

c is unreachable, freeing memory.

2 items freed.

[GC END]

G C B a T r b a g e d e e o l l t

max@MaxGorenLaptop:~$

Pretty Nifty, eh? Until next time, Happy Hacking!

]]>