Add pairing heap module

This commit is contained in:
K. Lange 2023-12-22 08:26:57 +09:00
parent 2685f49d24
commit 80754593ad
2 changed files with 419 additions and 0 deletions

104
modules/pheap.krk Normal file
View File

@ -0,0 +1,104 @@
'''
@brief Pairing heap.
@author K. Lange <klange@toaruos.org>
Provides a simple min-heap with insert, pop, peek, and visit.
A Kuroko implementation is provided as a backup alongside a
faster C implementation in a shared object module.
'''
def __make_pheap():
def pheap_meld(left, right, comp):
if left is None:
return right
if right is None:
return left
if comp(left[0],right[0]):
if left[1]:
right[2] = left[1]
left[1] = right
return left
else:
if right[1]:
left[2] = right[1]
right[1] = left
return right
def pheap_merge_pairs(lst, comp):
if lst is None:
return None
else if lst[2] is None:
return lst
else:
let next = lst[2]
lst[2] = None
let rest = next[2]
next[2] = None
return pheap_meld(pheap_meld(lst,next,comp), pheap_merge_pairs(rest, comp), comp)
def pheap_delete_min(heap, comp):
let subs = heap[1]
return pheap_merge_pairs(subs, comp)
def pheap_visit_heap(heap, func):
if not heap: return
func(heap)
pheap_visit_heap(heap[1], func)
pheap_visit_heap(heap[2], func)
def pheap_visit_heap_after(heap, func):
if not heap: return
pheap_visit_heap(heap[1], func)
pheap_visit_heap(heap[2], func)
func(heap)
class PHeap:
def __init__(self, comp):
'''Create a new pairing heap governed by the given comparator function.'''
self.heap = None
self.comp = comp
self.count = 0
def insert(self, value):
'''Insert a new element into the heap.'''
self.heap = pheap_meld(self.heap, [value, None, None], self.comp)
self.count += 1
def peek(self):
'''Retrieve the root (smallest) element of the heap, or None if it is empty.'''
return self.heap[0] if self.heap else None
def pop(self):
'''Remove and return the root (smallest) element of the heap. If the heap is empty, IndexError is raised.'''
let out = self.heap
if not out:
raise IndexError('pop from empty heap')
self.heap = pheap_delete_min(self.heap, self.comp)
self.count -= 1
return out[0] if out else None
def __bool__(self):
return self.heap is not None
def __len__(self):
return self.count
def visit(self, func, after=False):
'''Call a function for each element of the heap.'''
(pheap_visit_heap_after if after else pheap_visit_heap)(self.heap, func)
# Clean up qualified name.
PHeap.__qualname__ = 'PHeap'
return PHeap
# Keep the Kuroko version available for testing.
let PHeap_krk = __make_pheap()
let PHeap = PHeap_krk
# Try to load the C implementation.
try:
import _pheap
PHeap = _pheap.PHeap

315
src/modules/module__pheap.c Normal file
View File

@ -0,0 +1,315 @@
/**
* @brief Pairing heap.
* @file module__pheap.c
* @author K. Lange <klange@toaruos.org>
*
* A very simple pairing heap.
*
* Provides a min-heap with insert, peek, and pop.
*
* While heap entries may be mutable, care should be taken not to modify
* any values used for comparison, as the heap can not update ordering.
*
* This could likely be improved by the implementation of parent pointers,
* which can allow for elements of the heap other than the root to be
* removed or updated (removal + reinsertion), but that requires the
* ability to quickly reference specific elements - which requires the
* heap "nodes" to also be accessible or addressible in some way, such
* as by making them Kuroko objects. Such a change would likely result
* in performance impacts, so a parent-pointer pairing heap should be
* a separate class.
*
* The implementation here is based strongly on the pseudocode found in
* the Wikipedia article "Paring heap".
*/
#include <assert.h>
#include <stdlib.h>
#include <kuroko/vm.h>
#include <kuroko/util.h>
static KrkClass * PHeapClass;
/**
* @brief Heap node.
*
* Represents one element in the heap. Each element potentially has
* a pointer to more elements (a "right" or "next" pointer) and a
* pointer to a subheap (a "left" pointer).
*/
typedef struct PHeap PHeap;
struct PHeap {
struct PHeap_Obj * owner;
KrkValue value;
PHeap * subheaps; /* Left pointer to first child, if any. */
PHeap * next; /* Right pointer to next sibling, if any. */
};
/**
* @brief Heap comparator function.
*
* The heap is ostensibly a min-heap, but the comparison behavior is left
* entirely to the user. A comparator function should return true if the
* left (first) argument has priority (is less than) the right (second)
* argument, and 0 otherwise. This comparison must be consistent or the
* heap will not work correctly.
*
* Generally, this module only uses one comparison function: a wrapper that
* calls a Kuroko callable. As Kuroko callables may hold their own state,
* no facility was necessary to pass user data to the comparator - only
* the left and right heap nodes to compare are given.
*/
typedef int (*pheap_comparator_func)(PHeap *, PHeap *);
/**
* @brief meld - Combine two heaps.
*
* Combines two heaps and returns the result. The heaps are "destroyed" in the process.
*
* @param left One heap. Can be @c NULL for an empty heap.
* @param right The other heap. Can be @c NULL for an empty heap.
* @param comparator Function that should return true if @p left has priority (is less than) @p right.
* @returns A pointer to the new root, which will be either of @p left or @p right.
*/
static PHeap * pheap_meld(PHeap * left, PHeap * right, pheap_comparator_func comparator) {
/*
* If either of the heaps is "empty" (represented by NULL),
* then simply return the other one.
*/
if (!left) {
return right;
}
if (!right) {
return left;
}
/*
* Otherwise, pull the 'smaller' of the two up and add the 'larger'
* to the front of the subheap list of the smaller one. We use
* intrusive lists within our Heap struct, so each Heap is also
* a List node (with a `next` pointer).
*/
if (comparator(left, right)) {
/* Turns `left` into Heap(left→value, right :: left→subheaps) */
if (left->subheaps) {
right->next = left->subheaps;
}
left->subheaps = right;
return left;
} else {
/* Turns `right` into Heap(right→value, left :: right→subheaps) */
if (right->subheaps) {
left->next = right->subheaps;
}
right->subheaps = left;
return right;
}
}
/**
* @brief merge_pairs - Perform left-to-right/right-to-left merge on lists of subheaps.
*
* The core of the heap.
*
* @param list List of pairs to merge.
* @param comparator Comparator function as described in @c pheap_meld.
* @returns the resulting heap.
*/
static PHeap * pheap_merge_pairs(PHeap * list, pheap_comparator_func comparator) {
if (!list) {
/* An empty list is represented by NULL, and yields an empty Heap,
* which is also represented by NULL... */
return NULL;
} else if (list->next == NULL) {
/* If a list entry doesn't have a next, it has a size of one,
* and we can just return this heap directly. */
return list;
} else {
/* Otherwise we meld the first two, then meld them with the result of
* recursively melding the rest, which performs our left-right /
* right-left two-stage merge. */
PHeap * next = list->next;
list->next = NULL;
PHeap * rest = next->next;
next->next = NULL;
return pheap_meld(pheap_meld(list, next, comparator), pheap_merge_pairs(rest, comparator), comparator);
}
}
/**
* @brief delete_min - Remove the 'smallest' value from the heap.
*
* Removes the root node of the heap, rebalancing the remainder
* of the heap. Should only be used when the heap is not empty.
*
* @param heap Heap to remove the root of.
* @param comparator Comparator function as described in @c pheap_meld.
* @returns the resulting heap.
*/
static PHeap * pheap_delete_min(PHeap * heap, pheap_comparator_func comparator) {
PHeap * subs = heap->subheaps;
return pheap_merge_pairs(subs, comparator);
}
/**
* @brief visit_heap - Call a user function for every node in the heap.
*
* The function is called before recursing.
*
* @param heap Heap to walk.
* @param func Function to call.
* @param extra User data to pass to the function.
*/
static void pheap_visit_heap(PHeap * heap, void (*func)(PHeap *, void*), void* extra) {
if (!heap) return;
func(heap, extra);
pheap_visit_heap(heap->subheaps, func, extra);
pheap_visit_heap(heap->next, func, extra);
}
/**
* @brief visit_heap_after - Call a user function for every node in the heap.
*
* The function is called after recursing, so this is suitable for freeing the
* entirety of a heap.
*
* @param heap Heap to walk.
* @param func Function to call.
* @param extra User data to pass to the function.
*/
static void pheap_visit_heap_after(PHeap * heap, void (*func)(PHeap *, void*), void* extra) {
if (!heap) return;
pheap_visit_heap_after(heap->subheaps, func, extra);
pheap_visit_heap_after(heap->next, func, extra);
func(heap, extra);
}
struct PHeap_Obj {
KrkInstance inst;
KrkValue comparator;
PHeap * heap;
size_t count;
};
#define IS_PHeap(o) (krk_isInstanceOf(o,PHeapClass))
#define AS_PHeap(o) ((struct PHeap_Obj*)AS_OBJECT(o))
#define CURRENT_CTYPE struct PHeap_Obj *
#define CURRENT_NAME self
KRK_Method(PHeap,__init__) {
KrkValue comparator;
if (!krk_parseArgs(".V:PHeap", (const char*[]){"comp"}, &comparator)) return NONE_VAL();
self->comparator = comparator;
return NONE_VAL();
}
static int run_comparator(PHeap * left, PHeap * right) {
assert(left->owner == right->owner);
krk_push(left->owner->comparator);
krk_push(left->value);
krk_push(right->value);
KrkValue result = krk_callStack(2);
if (!IS_BOOLEAN(result)) return 0;
return AS_BOOLEAN(result);
}
KRK_Method(PHeap,insert) {
KrkValue value;
if (!krk_parseArgs(".V",(const char*[]){"value"}, &value)) return NONE_VAL();
struct PHeap * node = calloc(sizeof(struct PHeap), 1);
node->owner = self;
node->value = value;
self->heap = pheap_meld(self->heap, node, run_comparator);
self->count += 1;
return NONE_VAL();
}
KRK_Method(PHeap,peek) {
if (self->heap) return self->heap->value;
return NONE_VAL();
}
KRK_Method(PHeap,pop) {
PHeap * old = self->heap;
if (!old) return krk_runtimeError(vm.exceptions->indexError, "pop from empty heap");
self->heap = pheap_delete_min(self->heap, run_comparator);
self->count -= 1;
KrkValue out = old->value;
free(old);
return out;
}
KRK_Method(PHeap,__bool__) {
return BOOLEAN_VAL(self->heap != NULL);
}
KRK_Method(PHeap,__len__) {
return INTEGER_VAL(self->count);
}
static void run_visitor(PHeap * heap, void * visitor) {
krk_push(*(KrkValue*)visitor);
krk_push(heap->value);
krk_callStack(1);
}
KRK_Method(PHeap,visit) {
KrkValue func;
int after = 0;
if (!krk_parseArgs(".V|p",(const char*[]){"func","after"},
&func, &after)) return NONE_VAL();
(after ? pheap_visit_heap_after : pheap_visit_heap)(self->heap, run_visitor, &func);
return NONE_VAL();
}
static void _scan_one(PHeap * heap, void * unused) {
krk_markValue(heap->value);
}
static void _pheap_scan(KrkInstance * _self) {
struct PHeap_Obj * self = (void*)_self;
krk_markValue(self->comparator);
pheap_visit_heap(self->heap, _scan_one, NULL);
}
static void _free_one(PHeap * heap, void * unused) {
free(heap);
}
static void _pheap_sweep(KrkInstance * _self) {
struct PHeap_Obj * self = (void*)_self;
pheap_visit_heap_after(self->heap,_free_one, NULL);
}
KRK_Method(PHeap,comp) {
return self->comparator;
}
KRK_Module(_pheap) {
KRK_DOC(module, "Pairing heap with simple insert and pop-min operations.");
KrkClass * PHeap = krk_makeClass(module, &PHeapClass, "PHeap", vm.baseClasses->objectClass);
KRK_DOC(PHeap,"Pairing heap with simple insert and pop-min operations.");
PHeap->allocSize = sizeof(struct PHeap_Obj);
PHeap->_ongcscan = _pheap_scan;
PHeap->_ongcsweep = _pheap_sweep;
KRK_DOC(BIND_METHOD(PHeap,__init__),
"@arguments comp\n\n"
"Create a new pairing heap governed by the given comparator function.");
KRK_DOC(BIND_METHOD(PHeap,insert),
"@arguments value\n\n"
"Insert a new element into the heap.");
KRK_DOC(BIND_METHOD(PHeap,peek),
"Retrieve the root (smallest) element of the heap, or None if it is empty.");
KRK_DOC(BIND_METHOD(PHeap,pop),
"Remove and return the root (smallest) element of the heap. If the heap is empty, IndexError is raised.");
BIND_METHOD(PHeap,__bool__);
BIND_METHOD(PHeap,__len__);
KRK_DOC(BIND_METHOD(PHeap,visit),
"@arguments func,after=False\n\n"
"Call a function for each element of the heap.");
BIND_PROP(PHeap,comp);
krk_finalizeClass(PHeapClass);
}