Skip to main content

Treap

Introduction​

A Treap is a fascinating hybrid data structure that combines the properties of two fundamental data structures: a binary search tree (BST) and a heap. The name "Treap" itself is a blend of "tree" and "heap," reflecting its dual nature.

In a Treap, each node contains:

  • A key value that follows the binary search tree property
  • A priority value that follows the heap property (typically a max-heap)

What makes Treaps special is that they use randomization to achieve balance, making them simpler to implement than self-balancing trees like AVL or Red-Black trees, while still providing good expected performance.

Why Learn Treaps?​

  • Balanced Performance: Treaps provide O(log n) expected time for operations without complex balancing rules
  • Simple Implementation: Much easier to code than other balanced BSTs
  • Versatility: Useful for implementing ordered sets, maps, and priority queues
  • Randomized Algorithm: Demonstrates the power of randomization in algorithm design

Treap Structure and Properties​

A Treap node contains:

  1. A key (following BST property)
  2. A priority (typically a random value following max-heap property)
  3. Left and right child pointers

The Treap maintains two properties:

  • BST Property: For any node, all keys in its left subtree are smaller, and all keys in its right subtree are larger
  • Heap Property: The priority of any node is greater than or equal to the priorities of its children

Here's a visual representation of a Treap:

Implementing a Treap​

Let's implement a basic Treap in C++:

cpp
#include <iostream>
#include <cstdlib> // For rand()

class Treap {
private:
struct Node {
int key;
int priority;
Node* left;
Node* right;

Node(int k) : key(k), priority(rand()), left(nullptr), right(nullptr) {}
};

Node* root;

// Helper functions for rotations
Node* rotateRight(Node* y) {
Node* x = y->left;
y->left = x->right;
x->right = y;
return x;
}

Node* rotateLeft(Node* x) {
Node* y = x->right;
x->right = y->left;
y->left = x;
return y;
}

// Recursive insert function
Node* insert(Node* root, int key) {
if (!root) {
return new Node(key);
}

if (key < root->key) {
root->left = insert(root->left, key);

// Fix heap property if violated
if (root->left->priority > root->priority) {
root = rotateRight(root);
}
} else if (key > root->key) {
root->right = insert(root->right, key);

// Fix heap property if violated
if (root->right->priority > root->priority) {
root = rotateLeft(root);
}
}
// If key already exists, do nothing

return root;
}

// Recursive delete function
Node* remove(Node* root, int key) {
if (!root) {
return nullptr;
}

if (key < root->key) {
root->left = remove(root->left, key);
} else if (key > root->key) {
root->right = remove(root->right, key);
} else {
// Node found, delete it

// Case 1: Leaf node
if (!root->left && !root->right) {
delete root;
return nullptr;
}
// Case 2: One child is NULL
else if (!root->left) {
Node* temp = root->right;
delete root;
return temp;
} else if (!root->right) {
Node* temp = root->left;
delete root;
return temp;
}
// Case 3: Two children
else {
// Decide which way to rotate based on priorities
if (root->left->priority > root->right->priority) {
root = rotateRight(root);
root->right = remove(root->right, key);
} else {
root = rotateLeft(root);
root->left = remove(root->left, key);
}
}
}
return root;
}

void inorder(Node* root) {
if (root) {
inorder(root->left);
std::cout << "Key: " << root->key << ", Priority: " << root->priority << std::endl;
inorder(root->right);
}
}

void cleanup(Node* root) {
if (root) {
cleanup(root->left);
cleanup(root->right);
delete root;
}
}

public:
Treap() : root(nullptr) {
// Seed random number generator
srand(time(nullptr));
}

~Treap() {
cleanup(root);
}

void insert(int key) {
root = insert(root, key);
}

void remove(int key) {
root = remove(root, key);
}

void display() {
inorder(root);
}
};

Operations in a Treap​

Let's explore the key operations in a Treap:

Insertion​

To insert a key into a Treap:

  1. Insert the key as in a normal BST
  2. Assign a random priority to the new node
  3. Perform rotations as needed to maintain the heap property

Here's how insertion works step by step:

cpp
// Usage example
int main() {
Treap t;

// Insert elements
t.insert(5);
t.insert(3);
t.insert(8);
t.insert(1);
t.insert(7);

std::cout << "Treap contents:" << std::endl;
t.display();

return 0;
}

Possible output (priorities will vary due to randomization):

Treap contents:
Key: 1, Priority: 234
Key: 3, Priority: 874
Key: 5, Priority: 989
Key: 7, Priority: 345
Key: 8, Priority: 567

Searching in a Treap is identical to searching in a binary search tree:

cpp
bool search(Node* root, int key) {
if (!root) {
return false;
}

if (key == root->key) {
return true;
} else if (key < root->key) {
return search(root->left, key);
} else {
return search(root->right, key);
}
}

Deletion​

Deletion is more complex and can be done in two ways:

  1. BST with Rotations: First find the node, then use rotations to move it down to a leaf position, then remove it
  2. BST Delete with Heap Property Fix: Delete as in a BST, then fix heap property

The implementation we saw above uses the first approach.

Time and Space Complexity​

OperationAverage CaseWorst Case
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
SpaceO(n)O(n)

While the worst case is O(n), it's extremely unlikely due to randomization. The expected height of a random Treap is O(log n), making operations efficient in practice.

Applications of Treaps​

Treaps are useful in various scenarios:

1. Implementing Ordered Sets and Maps​

Treaps can efficiently maintain a set of elements in sorted order while allowing fast insertions and deletions.

2. Priority Queue with Ordering​

When you need both priorities and ordering of elements.

3. Fast Set Operations​

Treaps can be used to implement Union, Intersection, and Difference operations between sets efficiently.

4. Persistent Data Structures​

Treaps can be adapted to create persistent versions (where previous versions are preserved after modifications).

Practical Example: Text Editor Buffer​

Consider implementing a text editor buffer that supports efficient insertion and deletion at any position:

cpp
class TextEditorBuffer {
private:
struct Node {
char data;
int priority;
int subtreeSize; // Size of the subtree rooted at this node
Node* left;
Node* right;

Node(char c) : data(c), priority(rand()), subtreeSize(1),
left(nullptr), right(nullptr) {}
};

Node* root;

// Update subtree size
void updateSize(Node* node) {
if (node) {
node->subtreeSize = 1;
if (node->left) node->subtreeSize += node->left->subtreeSize;
if (node->right) node->subtreeSize += node->right->subtreeSize;
}
}

// Split treap into two treaps at position
std::pair<Node*, Node*> split(Node* root, int pos) {
if (!root) {
return {nullptr, nullptr};
}

int leftSize = root->left ? root->left->subtreeSize : 0;

if (pos <= leftSize) {
auto [l, r] = split(root->left, pos);
root->left = r;
updateSize(root);
return {l, root};
} else {
auto [l, r] = split(root->right, pos - leftSize - 1);
root->right = l;
updateSize(root);
return {root, r};
}
}

// Merge two treaps
Node* merge(Node* left, Node* right) {
if (!left) return right;
if (!right) return left;

if (left->priority > right->priority) {
left->right = merge(left->right, right);
updateSize(left);
return left;
} else {
right->left = merge(left, right->left);
updateSize(right);
return right;
}
}

public:
TextEditorBuffer() : root(nullptr) {
srand(time(nullptr));
}

// Insert character at position
void insert(int pos, char c) {
Node* newNode = new Node(c);
auto [left, right] = split(root, pos);
root = merge(merge(left, newNode), right);
}

// Delete character at position
void remove(int pos) {
auto [left, rightWithPos] = split(root, pos);
auto [nodeToRemove, right] = split(rightWithPos, 1);
delete nodeToRemove; // Free memory
root = merge(left, right);
}

// Get character at position
char charAt(int pos) {
Node* current = root;
while (current) {
int leftSize = current->left ? current->left->subtreeSize : 0;
if (pos == leftSize) {
return current->data;
} else if (pos < leftSize) {
current = current->left;
} else {
pos -= leftSize + 1;
current = current->right;
}
}
return '\0'; // Position out of bounds
}

// Get the entire text as a string
std::string getText() {
std::string result;
inorderTraversal(root, result);
return result;
}

private:
void inorderTraversal(Node* node, std::string& result) {
if (node) {
inorderTraversal(node->left, result);
result += node->data;
inorderTraversal(node->right, result);
}
}
};

Example usage:

cpp
int main() {
TextEditorBuffer buffer;

// Insert some text
buffer.insert(0, 'H'); // H
buffer.insert(1, 'e'); // He
buffer.insert(2, 'l'); // Hel
buffer.insert(3, 'l'); // Hell
buffer.insert(4, 'o'); // Hello

std::cout << "Text: " << buffer.getText() << std::endl;

// Insert in the middle
buffer.insert(2, 'X'); // HeXllo
std::cout << "After insertion: " << buffer.getText() << std::endl;

// Delete a character
buffer.remove(2); // Hello
std::cout << "After deletion: " << buffer.getText() << std::endl;

return 0;
}

Output:

Text: Hello
After insertion: HeXllo
After deletion: Hello

Split and Merge Operations​

Two powerful operations in Treaps are split and merge, which are useful for range operations:

Split​

Splits a Treap into two Treaps at a given key:

cpp
// Split treap into < key and ≥ key
std::pair<Node*, Node*> split(Node* root, int key) {
if (!root) {
return {nullptr, nullptr};
}

if (key < root->key) {
auto [left, right] = split(root->left, key);
root->left = right;
return {left, root};
} else {
auto [left, right] = split(root->right, key);
root->right = left;
return {root, right};
}
}

Merge​

Merges two Treaps where all keys in the first Treap are less than all keys in the second:

cpp
Node* merge(Node* left, Node* right) {
if (!left) return right;
if (!right) return left;

if (left->priority > right->priority) {
left->right = merge(left->right, right);
return left;
} else {
right->left = merge(left, right->left);
return right;
}
}

Implicit Treap (Array Treap)​

An implicit Treap uses positions as keys, allowing array-like operations with logarithmic performance. This is the basis of our text editor example.

Comparison with Other Balanced Trees​

FeatureTreapAVL TreeRed-Black Tree
BalanceProbabilisticStrictly balancedApproximately balanced
ImplementationSimpleComplexVery complex
Rotations per operationO(1) expectedO(log n) worst caseO(1) worst case
Space overheadLowerMediumMedium
Performance in practiceGoodVery goodGood

Summary​

Treaps combine the simplicity of implementation with good expected performance:

  • They merge binary search tree and heap properties
  • Randomization provides good expected balance
  • Operations have O(log n) expected time complexity
  • Treaps are simple to implement compared to other balanced BSTs
  • They support powerful operations like split and merge
  • Useful for ordered sets, maps, and more complex structures

Treaps demonstrate how randomization can simplify algorithm design while still achieving good performance characteristics, making them an important tool in a programmer's arsenal.

Exercises​

  1. Implement a function to find the k-th smallest element in a Treap
  2. Modify the Treap implementation to support duplicates
  3. Implement range queries to count elements between two keys
  4. Create a Treap-based implementation of an ordered multiset
  5. Implement split and merge operations and use them to implement set operations (union, intersection)

Additional Resources​

  • "Introduction to Algorithms" by Cormen, Leiserson, Rivest, and Stein
  • "Randomized Algorithms" by Motwani and Raghavan
  • "Treaps: The Magical Algorithm" on Competitive Programming websites
  • "Advanced Data Structures" by Peter Brass

Mastering Treaps will not only enhance your understanding of randomized data structures but also provide you with a powerful tool for solving complex algorithmic problems efficiently.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)