├── .travis.yml ├── LICENSE ├── README.md ├── lua ├── binary_heap.lua └── skew_heap.lua ├── rockspecs ├── heaps-1-1.rockspec └── heaps-scm-1.rockspec └── test └── heap-test.lua /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | - LUA='lua=5.1' 6 | - LUA='lua=5.2' 7 | - LUA='lua=5.3' 8 | - LUA='luajit=2.0' 9 | - LUA='luajit=2.1' 10 | 11 | before_install: 12 | - pip install hererocks 13 | - hererocks lua_install -r^ --$LUA 14 | - export PATH=$PATH:$PWD/lua_install/bin 15 | 16 | install: 17 | - luarocks install luacheck 18 | 19 | script: 20 | - luacheck lua 21 | - luacheck test 22 | - lua test/heap-test.lua 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2011 Incremental IP Limited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-heaps 2 | 3 | [![Build Status](https://travis-ci.org/geoffleyland/lua-heaps.svg?branch=master)](https://travis-ci.org/geoffleyland/lua-heaps) 4 | 5 | This started as a benchmark for binary and skew heaps in Lua. It compares them 6 | to sorting a table, which doesn't fare well. 7 | 8 | Now it's a reasonably good implementation of both kinds of heaps, 9 | probably a little better for binary heaps than skew heaps because I use binary heaps. 10 | 11 | The binary heap implementation tries to keep garbage creation to a minimum, 12 | which seems to help speed things up. 13 | 14 | Usage is: 15 | 16 | local heap = require"binary_heap" -- or "skew_heap" 17 | local H = heap:new() 18 | 19 | H:insert(2, "world") 20 | H:insert(1, "hello") 21 | 22 | local k1, w1 = H:pop() 23 | local k2, w2 = H:pop() 24 | 25 | print(k1, k2) -- prints "hello world" 26 | 27 | Keys and values are kept separate because it can, in some cases (like my common one) 28 | reduce garbage creation. 29 | 30 | You can provide a comparison function to `heap:new`, which will be passed the keys 31 | (but not values) of the two items to compare. 32 | 33 | 34 | The heaps tested against Lua 5.1-5.3 and LuaJIT. 35 | -------------------------------------------------------------------------------- /lua/binary_heap.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2007-2011 Incremental IP Limited. 2 | 3 | --[[ 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | --]] 22 | 23 | 24 | -- heap construction --------------------------------------------------------- 25 | 26 | local heap = {} 27 | heap.__index = heap 28 | 29 | 30 | local function default_comparison(k1, k2) 31 | return k1 < k2 32 | end 33 | 34 | 35 | function heap:new(comparison) 36 | return setmetatable( 37 | { length = 0, comparison = comparison or default_comparison }, self) 38 | end 39 | 40 | 41 | -- info ---------------------------------------------------------------------- 42 | 43 | function heap:next_key() 44 | assert(self.length > 0, "The heap is empty") 45 | return self[1].key 46 | end 47 | 48 | 49 | function heap:empty() 50 | return self.length == 0 51 | end 52 | 53 | 54 | -- insertion and popping ----------------------------------------------------- 55 | 56 | function heap:insert(k, v) 57 | assert(k, "You can't insert nil into a heap") 58 | 59 | local cmp = self.comparison 60 | 61 | -- float the new key up from the bottom of the heap 62 | self.length = self.length + 1 63 | local new_record = self[self.length] -- keep the old table to save garbage 64 | local child_index = self.length 65 | while child_index > 1 do 66 | local parent_index = math.floor(child_index / 2) 67 | local parent_rec = self[parent_index] 68 | if cmp(k, parent_rec.key) then 69 | self[child_index] = parent_rec 70 | else 71 | break 72 | end 73 | child_index = parent_index 74 | end 75 | if new_record then 76 | new_record.key = k 77 | new_record.value = v 78 | else 79 | new_record = {key = k, value = v} 80 | end 81 | self[child_index] = new_record 82 | end 83 | 84 | 85 | function heap:pop() 86 | assert(self.length > 0, "The heap is empty") 87 | 88 | local cmp = self.comparison 89 | 90 | -- pop the top of the heap 91 | local result = self[1] 92 | 93 | -- push the last element in the heap down from the top 94 | local last = self[self.length] 95 | local last_key = (last and last.key) or nil 96 | -- keep the old record around to save on garbage 97 | self[self.length] = self[1] 98 | self.length = self.length - 1 99 | 100 | local parent_index = 1 101 | while parent_index * 2 <= self.length do 102 | local child_index = parent_index * 2 103 | if child_index+1 <= self.length and 104 | cmp(self[child_index+1].key, self[child_index].key) then 105 | child_index = child_index + 1 106 | end 107 | local child_rec = self[child_index] 108 | local child_key = child_rec.key 109 | if cmp(last_key, child_key) then 110 | break 111 | else 112 | self[parent_index] = child_rec 113 | parent_index = child_index 114 | end 115 | end 116 | self[parent_index] = last 117 | return result.key, result.value 118 | end 119 | 120 | 121 | -- checking ------------------------------------------------------------------ 122 | 123 | function heap:check() 124 | local cmp = self.comparison 125 | local i = 1 126 | while true do 127 | if i*2 > self.length then return true end 128 | if cmp(self[i*2].key, self[i].key) then return false end 129 | if i*2+1 > self.length then return true end 130 | if cmp(self[i*2+1].key, self[i].key) then return false end 131 | i = i + 1 132 | end 133 | end 134 | 135 | 136 | -- pretty printing ----------------------------------------------------------- 137 | 138 | function heap:write(f, tostring_func) 139 | f = f or io.stdout 140 | tostring_func = tostring_func or tostring 141 | 142 | local function write_node(lines, i, level, end_spaces) 143 | if self.length < 1 then return 0 end 144 | 145 | i = i or 1 146 | level = level or 1 147 | end_spaces = end_spaces or 0 148 | lines[level] = lines[level] or "" 149 | 150 | local my_string = tostring_func(self[i].key) 151 | 152 | local left_child_index = i * 2 153 | local left_spaces, right_spaces = 0, 0 154 | if left_child_index <= self.length then 155 | left_spaces = write_node(lines, left_child_index, level+1, my_string:len()) 156 | end 157 | if left_child_index + 1 <= self.length then 158 | right_spaces = write_node(lines, left_child_index + 1, level+1, end_spaces) 159 | end 160 | lines[level] = lines[level]..string.rep(' ', left_spaces).. 161 | my_string..string.rep(' ', right_spaces + end_spaces) 162 | return left_spaces + my_string:len() + right_spaces 163 | end 164 | 165 | local lines = {} 166 | write_node(lines) 167 | for _, l in ipairs(lines) do 168 | f:write(l, '\n') 169 | end 170 | end 171 | 172 | 173 | ------------------------------------------------------------------------------ 174 | 175 | return heap 176 | 177 | ------------------------------------------------------------------------------ 178 | 179 | -------------------------------------------------------------------------------- /lua/skew_heap.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2008-2011 Incremental IP Limited. 2 | 3 | --[[ 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | --]] 22 | 23 | -- A good description of a skew heap can be found at 24 | -- http://www.cs.usu.edu/~allan/DS/Notes/Ch23.pdf 25 | 26 | 27 | -- heap construction --------------------------------------------------------- 28 | 29 | 30 | local heap = {} 31 | heap.__index = heap 32 | 33 | 34 | local function default_comparison(k1, k2) 35 | return k1 < k2 36 | end 37 | 38 | 39 | function heap:new(comparison) 40 | return setmetatable({ comparison = comparison or default_comparison }, self) 41 | end 42 | 43 | 44 | -- info ---------------------------------------------------------------------- 45 | 46 | function heap:next_key() 47 | assert(self.left, "The heap is empty") 48 | return self.left.key 49 | end 50 | 51 | 52 | function heap:empty() 53 | return self.left == nil 54 | end 55 | 56 | 57 | -- merging ------------------------------------------------------------------- 58 | 59 | function heap:merge(a, b) 60 | local cmp = self.comparison 61 | local head = self 62 | 63 | if not b then 64 | head.left = a 65 | else 66 | while a do 67 | if cmp(a.key, b.key) then -- a is less (or higher priority) than b 68 | head.left = a -- the lesser tree goes on the left 69 | head = a -- and we work on that in the next round 70 | a = head.right -- by merging its right side with b 71 | head.right = head.left -- and we move the left side to the right 72 | else 73 | head.left = b 74 | head = b 75 | a, b = head.right, a 76 | head.right = head.left 77 | end 78 | end 79 | head.left = b 80 | end 81 | end 82 | 83 | 84 | -- insertion and popping ------------------------------------------------------ 85 | 86 | function heap:insert(k, v) 87 | assert(k, "You can't insert nil into a heap") 88 | self:merge({key=k, value=v}, self.left) 89 | end 90 | 91 | 92 | function heap:pop() 93 | assert(self.left, "The heap is empty") 94 | local result = self.left 95 | self:merge(result.left, result.right) 96 | return result.key, result.value 97 | end 98 | 99 | 100 | -- checking ------------------------------------------------------------------ 101 | 102 | local function _check(h, cmp) 103 | if h == nil then return true end 104 | local k, l, r = h.key, h.left, h.right 105 | if (l and cmp(l.key, k)) or (r and cmp(r.key, k)) then 106 | return false 107 | else 108 | return _check(l, cmp) and _check(r, cmp) 109 | end 110 | end 111 | 112 | 113 | function heap:check(cmp) 114 | return _check(self.left, cmp or self.comparison) 115 | end 116 | 117 | 118 | -- pretty printing ----------------------------------------------------------- 119 | 120 | function heap:write(f, tostring_func) 121 | f = f or io.stdout 122 | tostring_func = tostring_func or tostring 123 | 124 | local function write_node(h, lines, line, start_col) 125 | line = line or 1 126 | start_col = start_col or 0 127 | lines[line] = lines[line] or "" 128 | 129 | local my_string = tostring_func(h.key) 130 | local my_len = my_string:len() 131 | 132 | local left_spaces, right_spaces = 0, 0 133 | if h.left ~= nil then 134 | left_spaces = write_node(h.left, lines, line + 1, start_col) 135 | end 136 | if h.right ~= nil then 137 | right_spaces = write_node(h.right, lines, line + 1, start_col+left_spaces+my_len) 138 | end 139 | lines[line] = lines[line]..string.rep(' ', start_col+left_spaces - lines[line]:len())..my_string..string.rep(' ', right_spaces) 140 | return left_spaces + my_len + right_spaces 141 | end 142 | 143 | local lines = {} 144 | write_node(self.left, lines) 145 | for _, l in ipairs(lines) do 146 | f:write(l, '\n') 147 | end 148 | end 149 | 150 | 151 | ------------------------------------------------------------------------------ 152 | 153 | return heap 154 | 155 | ------------------------------------------------------------------------------ 156 | 157 | -------------------------------------------------------------------------------- /rockspecs/heaps-1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "heaps" 2 | version = "1-1" 3 | source = 4 | { 5 | url = "git://github.com/geoffleyland/lua-heaps.git", 6 | branch = "master", 7 | tag = "v1", 8 | } 9 | description = 10 | { 11 | summary = "Binary and skew heaps", 12 | homepage = "http://github.com/geoffleyland/lua-heaps", 13 | license = "MIT/X11", 14 | maintainer = "Geoff Leyland " 15 | } 16 | dependencies = 17 | { 18 | "lua >= 5.1" 19 | } 20 | build = 21 | { 22 | type = "builtin", 23 | modules = 24 | { 25 | binary_heap = "lua/binary_heap.lua", 26 | skew_heap = "lua/skew_heap.lua", 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rockspecs/heaps-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "heaps" 2 | version = "scm-1" 3 | source = 4 | { 5 | url = "git://github.com/geoffleyland/lua-heaps.git", 6 | branch = "master", 7 | } 8 | description = 9 | { 10 | summary = "Binary and skew heaps", 11 | homepage = "http://github.com/geoffleyland/lua-heaps", 12 | license = "MIT/X11", 13 | maintainer = "Geoff Leyland " 14 | } 15 | dependencies = 16 | { 17 | "lua >= 5.1" 18 | } 19 | build = 20 | { 21 | type = "builtin", 22 | modules = 23 | { 24 | binary_heap = "lua/binary_heap.lua", 25 | skew_heap = "lua/skew_heap.lua", 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /test/heap-test.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2007-2011 Incremental IP Limited. 2 | 3 | --[[ 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | --]] 22 | 23 | 24 | local binary_heap = require("lua/binary_heap") 25 | local skew_heap = require("lua/skew_heap") 26 | local math_random = math.random 27 | local table_sort, table_remove = table.sort, table.remove 28 | 29 | -- tests ----------------------------------------------------------------------- 30 | 31 | local function test_heap_write(h) 32 | io.write("Pretty-printing heap:\n") 33 | for _ = 1, 20 do 34 | h:insert(math_random(100)) 35 | end 36 | h:write() 37 | io.write("\n") 38 | end 39 | 40 | 41 | local function test_heap_integrity(h, runs, insert_count, pop_count, range) 42 | runs = runs or 200 43 | insert_count = insert_count or 100 44 | pop_count = pop_count or 50 45 | range = range or 10000 46 | local cmp = h.comparison 47 | local size = 0 48 | local low 49 | 50 | io.write("Testing heap integrity...\n") 51 | for i = 1,runs do 52 | for _ = 1,math_random(insert_count) do 53 | h:insert(math_random(range), nil) 54 | size = size + 1 55 | if not h:check() then 56 | io.write("\n") 57 | h:write() 58 | error("Heap check failed after insertion") 59 | end 60 | if size % 10 == 0 then io.write("Step: ", i, "/", runs, ": Heap size: ", size,". \r") end 61 | end 62 | low = 0 63 | for _ = 1,math.min(size, math_random(pop_count)) do 64 | local r = h:pop() 65 | size = size - 1 66 | if cmp(r, low) then 67 | io.write("\n") 68 | h:write() 69 | error(string.format("Popped %d after %d", r, low)) 70 | end 71 | low = r 72 | if not h:check() then 73 | io.write("\n") 74 | h:write() 75 | error("Heap check failed after pop") 76 | end 77 | if size % 10 == 0 then io.write("Step: ", i, "/", runs, ": Heap size: ", size,". \r") end 78 | end 79 | end 80 | io.write("\n") 81 | low = 0 82 | while not h:empty() do 83 | local r = h:pop() 84 | size = size - 1 85 | if cmp(r, low) then 86 | io.write("\n") 87 | h:write() 88 | error(string.format("Popped %d after %d", r, low)) 89 | end 90 | low = r 91 | if not h:check() then 92 | io.write("\n") 93 | h:write() 94 | error("Heap check failed while clearing") 95 | end 96 | if size % 10 == 0 then io.write("Clearing: Heap size: ", size,". \r") end 97 | end 98 | io.write("\nDone.\n") 99 | end 100 | 101 | 102 | local function test_heap_speed(h, name, runs, insert_count, pop_count, range) 103 | runs = runs or 100 104 | insert_count = insert_count or 10000 105 | pop_count = pop_count or 7500 106 | pop_count = math.min(pop_count, insert_count) 107 | range = range or 10000 108 | 109 | io.write(("Testing %s speed...\n"):format(name)) 110 | local start = os.clock() 111 | for i = 1,runs do 112 | io.write("Step: ", i, "/", runs, ".\r") 113 | for _ = 1,insert_count do 114 | h:insert(math_random(range), nil) 115 | end 116 | for _ = 1,pop_count do 117 | h:pop() 118 | end 119 | end 120 | io.write("\nClearing heap.\n") 121 | while not h:empty() do 122 | h:pop() 123 | end 124 | 125 | local elapsed = os.clock() - start 126 | io.write("Done. Elapsed time ", elapsed, " seconds (", elapsed / (insert_count * runs), " s/insert+pop).\n") 127 | end 128 | 129 | 130 | local function test_sort_queue_speed(runs, insert_count, pop_count, range) 131 | runs = runs or 100 132 | insert_count = insert_count or 10000 133 | pop_count = pop_count or 7500 134 | pop_count = math.min(pop_count, insert_count) 135 | range = range or 10000 136 | 137 | local function cmp(a, b) return a.key < b.key end 138 | 139 | local h = {} 140 | 141 | io.write("Testing sorted queue speed...\n") 142 | local start = os.clock() 143 | for i = 1,runs do 144 | io.write("Step: ", i, "/", runs, ".\r") 145 | for _ = 1,insert_count do 146 | h[#h+1] = { key=math_random(range), value=nil } 147 | end 148 | table_sort(h, cmp) 149 | for _ = 1,pop_count do 150 | table_remove(h) 151 | end 152 | end 153 | io.write("\nClearing heap.\n") 154 | while h[1] do 155 | table_remove(h) 156 | end 157 | 158 | local elapsed = os.clock() - start 159 | io.write("Done. Elapsed time ", elapsed, " seconds (", elapsed / (insert_count * runs), " s/insert+pop).\n") 160 | end 161 | 162 | 163 | -- main ----------------------------------------------------------------------- 164 | 165 | math.randomseed(1) 166 | test_heap_write(binary_heap:new(function(k1, k2) return k1 > k2 end)) 167 | math.randomseed(1) 168 | test_heap_write(skew_heap:new(function(k1, k2) return k1 > k2 end)) 169 | math.randomseed(1) 170 | test_heap_integrity(binary_heap:new()) 171 | math.randomseed(1) 172 | test_heap_integrity(skew_heap:new()) 173 | math.randomseed(1) 174 | test_heap_speed(binary_heap:new(), "binary heap") 175 | math.randomseed(1) 176 | test_heap_speed(skew_heap:new(), "skew heap") 177 | 178 | if arg[1] == "--with-sort" then 179 | math.randomseed(1) 180 | test_sort_queue_speed() 181 | end 182 | 183 | 184 | -- EOF ------------------------------------------------------------------------- 185 | --------------------------------------------------------------------------------