Heap
A generic Heap implementation for both min and max heaps. It is designed for allowing the association of a cost to a given value so that users are not restricted to just numbers as values.
What is a Heap?
Heaps are an implementation of Priority Queues that excel at tracking the smallest or largest element in an array. They are commonly used in algorithms like Dijkstra's shortest path algorithm and Huffman coding.
Examples:
local minHeap = Heap.min()
minHeap:Push("A", 5) -- A is the value, and 5 is the cost associated with it
minHeap:Push("B", 2)
minHeap:Push("C", 8)
minHeap:Push("D", 4)
-- look at the current lowest value
local minValue, minCost = minHeap:Peek()
print(minValue, minCost) -- B 2
local maxHeap = Heap.max()
minHeap:Push("A", 5)
minHeap:Push("B", 2)
minHeap:Push("C", 8)
minHeap:Push("D", 4)
local poppedValue, costOfPoppedValue = maxHeap:Pop() -- pops off the top value
print(poppedValue, costOfPoppedValue) -- C 8
-- look at the new highest value
print(maxHeap:Peek()) -- A 5
MetaMethods
Supports the following metamethods:
Exported Types
This file exports a Heap type in the form of Heap<T>
where T
is the type of value stored in the heap.
Usually Luau can infer the type of T
from your usage, but it may be useful to manually provide the type if
you are using more complex datatypes.
local Heap = require(ReplicatedStorage.Heap)
type Heap<T> = Heap.Heap<T>
local myHeap: Heap<string> = Heap.min()
myHeap:Push("A", 5)
Info for Nerds
Read this only if you want to know more about the internals of this heap implmenentation.
In order to support the association of a cost to a value, the heap is implemented as three separate arrays.
One to store the values, one to store the costs, and one to store the index pointing to the first two.
I used this structure in order to optimize for cache hits on table lookups since using an object based
approach Ex: {Value: T, Cost: number}
could cause the CPU to potentially miss the Cost
data since it may
not be stored continguously in memory.
When a value is popped from the heap, its reference index is stored in a stack of open indices for reuse. This is done to hopefully reduce fragmentation of the heap and improve the likelyhood of cache hits.
I originally opted to use a dynamic cost system such that you would provide a comparator function to the constructor that would be used to determine the cost of each value. However, this introduced a number of potential issues such as if the comparator function was not deterministic, causing the order to desync. There could very likely be cases where the cost could change without the heap's knowledge of it. So instead I opted to require the cost to be provided at the time of insertion. Although technically the type definition specifies for only number costs, you could in theory use any type luau considers comparable with relational operators.
Functions
min
staticCreates a min-heap where the smallest element is always on top.
max
staticCreates a max-heap where the largest element is always on top.
Peek
Heap:
Peek
(
) →
(
T?
,
--
The top value from the heap.
number?
--
The cost of the top value.
)
Returns the top value from the heap without removing it. The top value is the value with the lowest cost in a min-heap and the value with the highest cost in a max-heap. If the heap is empty, nil is returned for both values.
Time Complexity: Runs in O(1)
time.
Push
Heap:
Push
(
value:
T
,
cost:
number?
) →
(
)
Inserts a value into the heap.
Time Complexity: Runs in worst case O(log n)
time.
local minHeap = Heap.min()
minHeap:Push("A", 5)
minHeap:Push("B", 2)
minHeap:Push("C", 8)
local minValue, minCost = minHeap:Peek()
print(minValue, minCost) -- B 2
Cost
If no cost is given, the value itself is used as the cost. Ensure that the given value is comparable with relational operators.
local minHeap = Heap.min()
minHeap:Push(2) -- uses 2 for both value and cost
print(minHeap:Peek()) -- 2 2
Pop
Heap:
Pop
(
) →
(
T?
,
number?
)
Removes and returns the top value from the heap.
Time Complexity: Runs in worst case O(log n)
time.
Size
Heap:
Size
(
) →
number
Returns the number of elements in the heap.
Equivalent to using the #
operator on the heap.
Time Complexity: Runs in O(1)
time.
Has
Heap:
Has
(
valueToCheckFor:
T
,
cost:
number?
) →
boolean
Takes a value or function and checks if the heap contains it. If a cost is provided then it will also ensure the cost matches. Returns true if the heap contains a specified value.
Time Complexity: Runs in worst case O(n)
time.
UpdateCost
Heap:
UpdateCost
(
valueToUpdate:
T
|
(
value:
T
)
→
(
boolean
)
,
--
The value to update the cost of.
newCost:
number
|
(
value:
T
,
oldCost:
number
)
→
number
,
--
The new cost to assign to the value. Can also be a function that takes the old cost and returns a new cost.
updateAll:
boolean?
--
If true, all occurrences of the value will be updated. Defaults to false.
) →
boolean
--
True if the cost was updated, false otherwise.
Updates the cost of a value in the heap. If no value is found, false is returned.
Repeated Values
If you have multiple instances of the same value, this method will only update the cost of the first valid instance found,
unless the third parameter updateAll
is set to true
. There is no guarantee of which instance will be found first.
Using updateAll
can be expensive as it may need to perform a large resorting of the heap to ensure proper ordering.
local minHeap = Heap.min()
minHeap:Push("A", 5)
minHeap:Push("B", 2)
minHeap:Push("C", 8)
minHeap:Push("D", 10)
print(minHeap:Peek()) -- B 2
minHeap:UpdateCost("A", 1)
print(minHeap:Peek()) -- A 1
-- update the cost of the first value that matches either "A" or "B" to 15
minHeap:UpdateCost(function(value)
return value == "A" or value == "B"
end, 15, false)
-- update the cost of all values that match "A" or "B" to 30
minHeap:UpdateCost(function(value)
return value == "A" or value == "B"
end, 30, true)
RemoveFirstOccurrence
Heap:
RemoveFirstOccurrence
(
valueToRemove:
T
) →
boolean
Removes the first occurrence of a given value from the heap. Heaps are not optimized for search removals, so this method should be used sparingly.
RemoveAllOccurrences
Heap:
RemoveAllOccurrences
(
valueToRemove:
T
) →
number
Removes all occurrences of a value from the heap and returns the number of occurrences removed. Heaps are not optimized for search removals, so this method should be used sparingly.
ToTree
Heap:
ToTree
(
) →
Branch
<
T
>
?
--
A tree representation of the heap.
A utility method that converts the heap into a tree structure. This is useful for debugging and visualizing the heap.
type Branch<T> = {Value: T, Left: Branch<T>?, Right: Branch<T>?}
iterating over Heap
metamethodfor
value:
T
,
cost:
number
in
Heap
do
MetaMethod for iterating over the heap.
In a for loop, the first variable is the Value
and the second variable is the Cost
.
local minHeap = Heap.min()
minHeap:Push("A", 5)
minHeap:Push("B", 2)
minHeap:Push("C", 8)
minHeap:Push("A", 4)
minHeap:Push("D", 10)
for value: string, cost: number in minHeap do
print(value, cost)
end
-- Output: the order may vary
B 2
A 4
C 8
A 5
D 10
Manipulating the Heap during iteration
It is not recommended to modify the Heap during iteration as it may cause undefined behavior. Internal orders may change and the iterator may not be able to find the next value.
Iteration Order
There is no guaranteed order of iteration. You should assume you will receive the values in a random order.