Implementation of Min Heap data structure not working properly

Hello!

I tried to implement a min heap to make my pathfinding system work much faster, however it seems to often break when deleting the first (smallest) element.

The main functions for the min heap are the following: (Note that each element in the min-heap/priority-queue is a table that consists of a value, which is the first one, and an index, which is the second one, but the second should be irrelevant for this case)

@native 
local function priorityQueue_Heapify(pq: {{number}}, i: number)
	if i < 0 then
		return
	end
	
	local size: number = #pq
	
	-- Find the smallest among root, left child, and right child
	local smallest: number = i
	local l: number = 2 * i + 1 -- l and r are from 0 to n-1
	local r: number = 2 * i + 2
	
	if l < size and pq[i+1][1] > pq[l+1][1] then
		smallest = l
	end

	if r < size and pq[smallest+1][1] > pq[r+1][1] then
		smallest = r
	end
			
	-- Swap and continue heapifying if root is not the smallest
	if smallest ~= i then
		pq[i+1], pq[smallest+1] = pq[smallest+1], pq[i+1]
		priorityQueue_Heapify(pq, math.floor((i+1) / 2) - 1)
	end
end

@native 
local function priorityQueue_Insert(pq: {{number}}, data: {number})
	if #pq == 0 then
		table.insert(pq, data)
	else
		table.insert(pq, data)
		priorityQueue_Heapify(pq, math.floor(#pq / 2) - 1)
	end
end

@native 
local function priorityQueue_Delete(pq: {{number}}, i: number)
	local size: number = #pq
		
	pq[i+1], pq[size] = pq[size], pq[i+1]

	table.remove(pq, size)
	priorityQueue_Heapify(pq, math.floor((size-1) / 2) - 1)
end

And the simplest case where I was able to reproduce the issue is the following:

local pq: {{number}} = {}
	for i: number = 1, 5 do
		priorityQueue_Insert(pq, {i, i})
	end

	priorityQueue_Delete(pq, 0)
	
	for i: number = 2, #pq do
		if pq[i][1] < pq[1][1] then
			print("bruh")
		end
	end
	
	for _, a in pq do
		print(a[1])
	end

	--[[
	OUTPUT:
	
	bruh (x3)
	
	5
	2
	3
	4

	]]

Now, I have been doing experimentation here & there and found one thing that significantly increased the success rate of correctly forming the min-heap, but that was still broken. The reason I didn’t include that in the min heap code above is because every where I checked for pre-made implementations didn’t do that, so I figured it probably isn’t the right thing either. Anyways, the alternative that I found is to take the following line out of the if statement in priorityQueue_Heapify

priorityQueue_Heapify(pq, math.floor((i+1) / 2) - 1)

But as I just mentioned, that approach isn’t perfect either and doing something like this can get it to fail very easily:

local seed: number = 2
math.randomseed(seed)
	
local pq: {{number}} = {}
for i: number = 1, 20 do
	priorityQueue_Insert(pq, {math.random(1, 10), i})
end

for i: number = 1, 10 do
	priorityQueue_Delete(pq, 0)
end
	
for i: number = 2, #pq do
	if pq[i][1] < pq[1][1] then
		print("bruh")
	end
end
	
for _, a in pq do
	print(a[1])
end

As much as I can tell about the original code, it seems that the first element never gets reached after it is switched with the last one in the priorityQueue_Delete function, so it wont be appropriately switched around.

Also, I should mention that the priorityQueue_Insert function seems to work perfectly. This doesn’t neccessarely mean that the problem is in the priorityQueue_Insert function.

All the code for this is essentially translated into luau after googling an implementation and trying to understand how the min-heap works.

Any attempt to help make the min-heap is greatly appreciated!

What do you want your Delete to be doing? Delete a specific index or delete a specific value? Right now it’s kinda doing neither which is playing to your issue.
Edit: Also I have a feeling it comes down to things getting lost in the translation of Lua’s indexes starting at 1 vs almost every other language starting at 0. But I’ll try to confirm later.

	local smallest: number = i
	local l: number = 2 * i + 1 -- l and r are from 0 to n-1
	local r: number = 2 * i + 2

Alright, some slight tweaks to what you had and it seems to be working.
The main changes were making sure all of the index parameters were assuming 0 based indexing, then adding 1 when interacting with pq. Also making sure to kick off the heapify at the subtree of the parent of an inserted element, or the element that was deleted.

@native 
local function priorityQueue_Heapify(pq: {{number}}, i: number)
	if i < 0 then
		return
	end
	
	local size: number = #pq

	-- Find the smallest among root, left child, and right child
	local smallest: number = i
	local l: number = 2 * i + 1 -- l and r are from 0 to n-1
	local r: number = 2 * i + 2

	if l < size and pq[smallest+1][1] > pq[l+1][1] then
		smallest = l
	end

	if r < size and pq[smallest+1][1] > pq[r+1][1] then
		smallest = r
	end

	-- Swap and continue heapifying if root is not the smallest
	if smallest ~= i then
		pq[i+1], pq[smallest+1] = pq[smallest+1], pq[i+1]
		priorityQueue_Heapify(pq, smallest)
	end
end

@native 
local function priorityQueue_Insert(pq: {{number}}, data: {number})
	if #pq == 0 then
		table.insert(pq, data)
	else
		table.insert(pq, data)
		priorityQueue_Heapify(pq, math.floor((#pq - 2) / 2)) -- -2 here to translate to 0 based index for the parent
	end
end

@native 
local function priorityQueue_Delete(pq: {{number}}, i: number)
	local size: number = #pq

	pq[i+1], pq[size] = pq[size], pq[i+1]

	table.remove(pq, size)
	priorityQueue_Heapify(pq, i)
end

The delete functions is supposed to remove the i’th element from the heap, though I only use it to remove the first one.

About the indices, I attempted to use indicies from 0 to n-1 instead of 1 to n so that the indices would work with the same logic as most other languages do. And every time the heap was directly accessed, the index would have 1 added to it to turn it back to a lua index.

Oh, you got it working? I’ll check it out first thing once I get home!
Thought I had done the indices correctly, but apparently not. Tysm!

Ah, unfortunately this doesn’t seem to fix the issue.
Perhaps it got slightly better, but still clearly doesn’t work like it’s supposed to. Just off by trying large tests, something is very clearly not working properly.

local pq: {{number}} = {}
for i: number = 1, 20000 do
	priorityQueue_Insert(pq, {math.random(1, 1000), i})
end

for i: number = 1, 1000 do
	priorityQueue_Delete(pq, 0)
end
	
for i: number = 2, #pq do
	if pq[i][1] < pq[1][1] then
		print("bruh")
	end
end

The output for the above is often at least a thousand mis-placed elements.

I’ll try to investigate further too.

Alright, switching things up for the insert to trickle the new node up to the right spot in the graph and the heapify after the delete to trickle down then it looks like it’s all working now.

@native 
local function priorityQueue_Heapify(pq: {{number}}, i: number)
	if i < 0 then
		return
	end
	
	local size: number = #pq

	-- Find the smallest among root, left child, and right child
	local smallest: number = i
	local l: number = 2 * i + 1 -- l and r are from 0 to n-1
	local r: number = 2 * i + 2

	if l < size and pq[smallest+1][1] > pq[l+1][1] then
		smallest = l
	end

	if r < size and pq[smallest+1][1] > pq[r+1][1] then
		smallest = r
	end

	-- Swap and continue heapifying if root is not the smallest
	if smallest ~= i then
		pq[i+1], pq[smallest+1] = pq[smallest+1], pq[i+1]
		priorityQueue_Heapify(pq, smallest)
	end

end

@native 
local function priorityQueue_Insert(pq: {{number}}, data: {number})
	if #pq == 0 then
		table.insert(pq, data)
	else
		table.insert(pq, data)
		local curIndex = #pq - 1
		while(curIndex  > 0 and pq[curIndex+1][1] < pq[math.floor((curIndex-1) / 2) + 1][1]) do
			pq[curIndex +1], pq[math.floor((curIndex-1) / 2) + 1] = pq[math.floor((curIndex-1) / 2) + 1], pq[curIndex +1]
			--print("Swapped: ", pq[curIndex +1][1], pq[math.floor((curIndex-1) / 2) + 1][1])
			curIndex = math.floor((curIndex-1) / 2)
		end
	end
end

@native 
local function priorityQueue_Delete(pq: {{number}}, i: number)
	local size: number = #pq

	pq[i+1], pq[size] = pq[size], pq[i+1]

	table.remove(pq, size)
	priorityQueue_Heapify(pq, i)
end

and to do multiple iterations of your larger test:

print("Starting Test Iterations")
for testIteration = 1, 50, 1 do
	local pq: {{number}} = {}
	
	for i: number = 1, 1000 do
		priorityQueue_Insert(pq, {math.random(1, 1000), i})
	end
	
	for i: number = 1, 500 do
		priorityQueue_Delete(pq, 0)
	end

	bruhCount = 0
	for i: number = 2, #pq do
		if pq[i][1] < pq[1][1] then
			print("found ", i)
			bruhCount += 1
		end
	end
	print("bruh's found: ", bruhCount)
end
1 Like

Oml dude. TYSM!
I’ve been stuck on this for so long. You’re awesome bro!
Works perfectly now!