Cartesian trees, also known as Treap (tree + heap) are a type of binary search tree that offers a combination of the performance of a self-balancing tree with the efficiency of a heap. Cartesian trees are used to store and organize data in a way that allows for quick access, search, insertion, and deletion. This makes them one of the most efficient data structures for dealing with large datasets. In this article, we will explore the time and space complexities associated with access, search, insertion, and deletion in Cartesian trees. We will also provide coding examples to illustrate how these operations are performed in Python.
What is a Cartesian Tree?
A Cartesian tree is a binary tree that consists of a root node and two subtrees, each of which is also a Cartesian tree. Each node in the tree stores a value, and the elements in a Cartesian tree are arranged according to a specific ordering rule. The Cartesian tree is typically constructed using a heap-ordered tree structure, with the root node having the highest priority. Each node in the tree has a priority which is used to determine the ordering of the elements.
How does Cartesian Trees Work?
Cartesian Trees are a data structure used to store binary search trees (BSTs) in an array. A Cartesian Tree consists of a root node and a number of other nodes representing the elements that make up the BST. Each node has three fields:
- The value of the node
- The index of its left child
- The index of its right child
The root node is always located at index 0. The left and right child nodes of each node can be determined by knowing the index of the node and its left and right child values.
Cartesian Trees are used to speed up certain operations on BSTs. For example, searching for a particular element can be done in O(log n) time instead of O(n) time with a regular BST. This is because the nodes of the Cartesian Tree are stored in an array, which allows for quicker access than a regular BST.
To build a Cartesian Tree, one must first build a BST using the standard insertion and deletion algorithms. Once the BST is built, the root node is placed at index 0 and the remaining nodes are placed in the array in an in-order fashion. For example, if the BST contains the elements 5, 3, 8, 7, 6, 4, then the array would be [5, 3, 4, 8, 6, 7]. Then, the left and right child indices for each node can be determined. For example, the left child of the root node (5) is 3 and the right child is 8.
Once the tree is built, the same operations that can be performed on a regular BST (such as searching, insertion, and deletion) can be performed on the Cartesian Tree.
Time Complexity for Access, Search, Insertion, and Deletion
Access
Accessing an element in a Cartesian tree has an average time complexity of O(log n). This means that the time required to access an element increases logarithmically with the size of the tree.
Search
Searching for an element in a Cartesian tree has an average time complexity of O(log n). This means that the time required to search for an element in the tree increases logarithmically with the size of the tree.
Insertion
Inserting an element into a Cartesian tree has an average time complexity of O(log n). This means that the time required to insert an element into the tree increases logarithmically with the size of the tree.
Deletion
Deleting an element from a Cartesian tree has an average time complexity of O(log n). This means that the time required to delete an element from the tree increases logarithmically with the size of the tree.
Worst Case Time Complexity for Access, Search, Insertion, and Deletion
Access
The worst case time complexity for accessing an element in a Cartesian tree is O(n). This means that the time required to access an element may increase linearly with the size of the tree.
Search
The worst case time complexity for searching for an element in a Cartesian tree is O(n). This means that the time required to search for an element in the tree may increase linearly with the size of the tree.
Insertion
The worst case time complexity for inserting an element into a Cartesian tree is O(n). This means that the time required to insert an element into the tree may increase linearly with the size of the tree.
Deletion
The worst case time complexity for deleting an element from a Cartesian tree is O(n). This means that the time required to delete an element from the tree may increase linearly with the size of the tree.
Space Complexity
The space complexity for a Cartesian tree is O(n). This means that the amount of space required for the tree is proportional to the size of the tree.
Python Implementation
In this section, we will provide coding examples in Python that demonstrate how to create and manipulate Cartesian trees.
Creating a Cartesian Tree
We will start by creating a simple Cartesian tree in Python.
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
class CartesianTree:
def __init__(self):
self.root = None
def insert(self, val):
# Create a new node
newNode = Node(val)
# Base Case
if self.root is None:
self.root = newNode
return
# Traverse and find the correct position
temp = self.root
while temp is not None:
if newNode.val < temp.val:
if temp.left is None:
temp.left = newNode
return
else:
temp = temp.left
else:
if temp.right is None:
temp.right = newNode
return
else:
temp = temp.right
In the above code, we create a Node class that contains the data associated with each node in the tree. We then create a CartesianTree class, which contains the methods for manipulating the tree. The insert() method is used to add a node to the tree. It first checks to see if the tree is empty, and if so it creates a new node and assigns it to the root. Otherwise, it traverses the tree to find the correct position for the new node.
Searching in a Cartesian Tree
Next, we will demonstrate how to search for an element in a Cartesian tree.
def search(self, val):
# Base Case
if self.root is None:
return False
# Traverse and search for the element
temp = self.root
while temp is not None:
if temp.val == val:
return True
elif val < temp.val:
temp = temp.left
else:
temp = temp.right
return False
The search() method is used to search for an element in the tree. It first checks to see if the tree is empty, and if so it returns False. Otherwise, it traverses the tree to search for the element. If the element is found, it returns True; otherwise, it returns False.
Deleting from a Cartesian Tree
Finally, we will demonstrate how to delete an element from a Cartesian tree.
def delete(self, val):
# Base Case
if self.root is None:
return
# Find the node to be deleted
temp = self.root
parent = None
while temp is not None and temp.val != val:
parent = temp
if val < temp.val:
temp = temp.left
else:
temp = temp.right
# If node to be deleted is not found
if temp is None:
return
# If node to be deleted has one child
if temp.left is None:
if parent is None:
self.root = temp.right
elif temp is parent.left:
parent.left = temp.right
else:
parent.right = temp.right
# If node to be deleted has two children
elif temp.right is None:
if parent is None:
self.root = temp.left
elif temp is parent.left:
parent.left = temp.left
else:
parent.right = temp.left
# If node to be deleted has two children
else:
successor = temp.right
while successor.left is not None:
successor = successor.left
temp.val = successor.val
if successor is temp.right:
temp.right = successor.right
else:
successorParent.left = successor.right
The delete() method is used to delete an element from the tree. It first finds the node to be deleted and its parent. If the node to be deleted has one child, it deletes the node and assigns its child to the parent. If the node to be deleted has two children, it finds the in-order successor of the node and deletes it.
Conclusion
In this article, we have explored the time and space complexities associated with access, search, insertion, and deletion in Cartesian trees. We have also provided coding examples in Python to illustrate how these operations are performed. Cartesian trees are a powerful data structure that are used to store and organize data in a way that allows for quick access, search, insertion, and deletion. Coding
Exercises
Write a function to traverse a Cartesian tree in pre-order.
def preOrder(root):
if root is not None:
print root.val
preOrder(root.left)
preOrder(root.right)
Write a function to traverse a Cartesian tree in post-order.
def postOrder(root):
if root is not None:
postOrder(root.left)
postOrder(root.right)
print root.val
Write a function to traverse a Cartesian tree in-order.
def inOrder(root):
if root is not None:
inOrder(root.left)
print root.val
inOrder(root.right)
Write a function to search for an element in a Cartesian tree.
def search(root, val):
if root is None:
return False
if root.val == val:
return True
elif val < root.val:
return search(root.left, val)
else:
return search(root.right, val)
Write a function to delete an element from a Cartesian tree.
def delete(root, val):
# Base Case
if root is None:
return
# Find the node to be deleted
temp = root
parent = None
while temp is not None and temp.val != val:
parent = temp
if val < temp.val:
temp = temp.left
else:
temp = temp.right
# If node to be deleted is not found
if temp is None:
return
# If node to be deleted has one child
if temp.left is None:
if parent is None:
root = temp.right
elif temp is parent.left:
parent.left = temp.right
else:
parent.right = temp.right
# If node to be deleted has two children
elif temp.right is None:
if parent is None:
root = temp.left
elif temp is parent.left:
parent.left = temp.left
else:
parent.right = temp.left
# If node to be deleted has two children
else:
successor = temp.right
while successor.left is not None:
successor = successor.left
temp.val = successor.val
if successor is temp.right:
temp.right = successor.right
else:
successorParent.left = successor.right