Implementing a Priority Queue Using Leftist Trees with Elixir: A Powerful Data Structure

PandaMan
4 min readJul 17, 2023

--

Introduction:

In computer science, efficient data structures play a vital role in optimizing algorithms and solving complex problems. One such data structure is the Priority Queue, which stores elements based on their priority. In this article, we’ll explore the concept of a Priority Queue and dive into the fascinating world of Leftist Trees. We will learn how to implement a Priority Queue using Leftist Trees with Elixir, a functional programming language known for its concurrency and fault-tolerant capabilities.

Understanding Priority Queues:

A Priority Queue is a data structure that allows efficient insertion and deletion of elements based on their priority. Unlike a regular queue, where details are processed in the order they are added, a priority queue retrieves elements based on their priority level. Elements with higher priority are given precedence and are retrieved before lower-priority details.

Introducing Leftist Trees:

Leftist Trees are a specialized type of binary tree that can be used to implement Priority Queues efficiently. They are particularly suitable for operations such as merging and deletion, which are crucial for maintaining the order of elements in a Priority Queue.

In a Leftist Tree, each node contains an element and a rank value that represents the node’s priority. The rank value is determined by the length of the right spine, which is the longest path from the root to a leaf node that contains only the right children. The rank property ensures that the Leftist Tree is always balanced and optimized for efficient operations.

Implementing a Priority Queue with Leftist Trees in Elixir:

To implement a Priority Queue using Leftist Trees in Elixir, we first need to define the structure of a Leftist Tree node. Each node will contain the following information:

defmodule LeftistTree do
defstruct [:element, :rank, :left, :right]
end

Next, let's create the functions necessary for constructing and manipulating Leftist Trees:

defmodule PriorityQueue do
# Function to merge two Leftist Trees
def merge(tree1, nil), do: tree1
def merge(nil, tree2), do: tree2
def merge(tree1 = %LeftistTree{rank: rank1, right: right1} = t1, tree2 = %LeftistTree{rank: rank2, right: right2} = t2) when rank1 > rank2 do
%LeftistTree{element: element, rank: rank2, left: left, right: merged} = merge(t1, right2)
%LeftistTree{element: element, rank: rank2, left: merged, right: left}
end
def merge(tree1, tree2) do
merge(tree2, tree1)
end

# Function to insert an element into the Priority Queue
def insert(tree, element, rank) do
merge(tree, %LeftistTree{element: element, rank: rank, left: nil, right: nil})
end

# Function to find and delete the element with the highest priority
def delete_max(%LeftistTree{element: element, left: left, right: right}), do: {element, merge(left, right)}
end

Time Complexities:

  • merge O(logN)
  • insert O(logN)
  • delete_max(logN)

Let’s dive deeper into the merge function, which is a crucial operation in implementing a Priority Queue using Leftist Trees in Elixir.

The merge function is responsible for merging two Leftist Trees while maintaining the priority order based on the rank of each node. It takes two Leftist Trees as input and returns a new Leftist Tree that represents the merged result.

Here’s a breakdown of how the merge function works:

  1. The base cases:
  • If either of the trees is nil, the non-nil tree is returned as the result. This ensures that the merging operation can be performed even when one of the trees is empty.

2. When both trees are non-empty:

  • The function compares the ranks of the root nodes of the two trees.
  • If the rank of the first tree’s root node is greater than the rank of the second tree’s root node, the function recursively calls merge on the first tree's right child and the second tree. This allows the higher-ranked tree to be merged with the right child of the lower-ranked tree.
  • The resulting merged tree is assigned to the variable merged.
  • The left child of the second tree is set as the left child of the merged tree, and the merged tree becomes the new left child of the root node of the second tree. This step ensures that the Leftist Tree property is maintained, as the left child always has a higher rank than the right child.

3. Handling the second case when the rank of the first tree’s root node is not greater than the rank of the second tree’s root node:

  • In this case, the function swaps the first and second trees, ensuring that the higher-ranked tree is always merged with the right child of the lower-ranked tree. This step guarantees that the resulting tree remains a valid Leftist Tree.

4. Finally, the merged tree is returned as the result.

The merge function allows for efficient merging of Leftist Trees by leveraging the rank property. By recursively merging the trees while considering their ranks, it ensures that the resulting tree maintains the balanced structure and optimizes the Priority Queue operations.

Using this merge function, you can effectively merge two Leftist Trees and create a robust Priority Queue implementation in Elixir that efficiently handles priority-based elements.

Conclusion:

We explored the concept of Priority Queues and how Leftist Trees can be used to implement them efficiently. We discovered that Leftist Trees provide an optimized way of managing a Priority Queue by maintaining a balanced structure. We also implemented a Priority Queue using Leftist Trees in Elixir, leveraging the language’s functional programming features. By using this implementation, you can create a powerful Priority Queue data structure that efficiently handles elements based on their priority levels. Leftist Trees and Elixir together offer a winning combination for building robust and performant applications.

References:

Heap — Leftist Tree

https://github.com/stackcats/collections/blob/main/lib/heap/heap.ex

--

--