Compare commits
No commits in common. "b4ca806d768e94506565ff6e56d315bfb282f3c1" and "661e1d220ae71002d66d2ba2d3dfddfe4ff1035d" have entirely different histories.
b4ca806d76
...
661e1d220a
12 changed files with 78 additions and 173 deletions
|
@ -9,4 +9,4 @@ name = "pyority_queue"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.23.3", features = ["py-clone"] }
|
pyo3 = "0.23.3"
|
||||||
|
|
19
main.py
Executable file
19
main.py
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/env python
|
||||||
|
from pyority_queue import PureQueue, KeyedQueue
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pure = PureQueue()
|
||||||
|
pure["second"] = 3.4
|
||||||
|
pure["first"] = 1.2
|
||||||
|
pure["third"] = 5.6
|
||||||
|
print(*pure)
|
||||||
|
|
||||||
|
keyed = KeyedQueue({"third": 5.6, "second": 3.4, "first": 7.8})
|
||||||
|
del keyed["third"]
|
||||||
|
keyed["first"] = 1.2
|
||||||
|
print(*keyed)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -1,14 +1,12 @@
|
||||||
from abc import ABC
|
from typing import Self
|
||||||
from typing import Hashable, Iterable, Self
|
|
||||||
|
|
||||||
|
|
||||||
type Comparable = float | int
|
class PureQueue[T]:
|
||||||
|
"""A min-queue that allows duplicates and provides a minimal API allowing insertions and the ability to iterate over the queue in-order"""
|
||||||
|
def __init__(self, items: dict[T, float] | None = None) -> None:
|
||||||
class Queue[Data](ABC):
|
"""
|
||||||
"""
|
:param items: An optional mapping of items to priorities to initialize the queue with
|
||||||
Common queue methods providing a minimal API for iteration.
|
"""
|
||||||
"""
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -20,44 +18,12 @@ class Queue[Data](ABC):
|
||||||
:return: An iterator over the queue
|
:return: An iterator over the queue
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __next__(self) -> Data:
|
def __next__(self) -> T:
|
||||||
"""
|
"""
|
||||||
:return: The next item in the queue
|
:return: The next item in the queue
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __setitem__(self, key: T, value: float) -> None:
|
||||||
class PureQueue[Data: Comparable](Queue[Data]):
|
|
||||||
"""
|
|
||||||
A min-queue that directly orders its items, allowing duplicates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items: Iterable[Data] | None = None) -> None:
|
|
||||||
"""
|
|
||||||
:param items: An optional list of priorities with which to initialize the queue
|
|
||||||
"""
|
|
||||||
|
|
||||||
def pop(self) -> Data:
|
|
||||||
"""
|
|
||||||
:return: The next item in the queue
|
|
||||||
"""
|
|
||||||
|
|
||||||
def insert(self, item: Data) -> None:
|
|
||||||
"""
|
|
||||||
:param item: Item to insert into the queue
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class KeyedQueue[Data, Priority: Comparable](Queue[Data]):
|
|
||||||
"""
|
|
||||||
A min-queue that allows arbitrary data associated with some priority, allowing duplicates of both data and priority.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items: dict[Data, Priority] | Iterable[tuple[Data, Priority]] | None = None) -> None:
|
|
||||||
"""
|
|
||||||
:param items: An optional mapping/list of items to priorities with which to initialize the queue
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __setitem__(self, key: Data, value: Priority) -> None:
|
|
||||||
"""
|
"""
|
||||||
Inserts a new item into the queue
|
Inserts a new item into the queue
|
||||||
:param key: The item to insert
|
:param key: The item to insert
|
||||||
|
@ -65,25 +31,16 @@ class KeyedQueue[Data, Priority: Comparable](Queue[Data]):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class IndexedQueue[Data: Hashable, Priority: Comparable](Queue[Data]):
|
class KeyedQueue[T](PureQueue[T]):
|
||||||
"""
|
"""A min-queue that disallows duplicates but offers the ability to update priorities and delete arbitrary items"""
|
||||||
A min-queue that allows arbitrary data associated with some priority.
|
def __setitem__(self, key: T, value: float) -> None:
|
||||||
Disallows duplicate data but offers the ability to update priorities and delete arbitrary items.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items: dict[Data, Priority] | Iterable[tuple[Data, Priority]] | None = None) -> None:
|
|
||||||
"""
|
|
||||||
:param items: An optional mapping/list of items to priorities with which to initialize the queue
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __setitem__(self, key: Data, value: Priority) -> None:
|
|
||||||
"""
|
"""
|
||||||
Inserts an item into the queue, or updates its priority if it already exists
|
Inserts an item into the queue, or updates its priority if it already exists
|
||||||
:param key: The item to insert or update
|
:param key: The item to insert or update
|
||||||
:param value: The priority of the item
|
:param value: The priority of the item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __delitem__(self, key: Data) -> None:
|
def __delitem__(self, key: T) -> None:
|
||||||
"""
|
"""
|
||||||
:param key: The item to delete from the queue
|
:param key: The item to delete from the queue
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,2 @@
|
||||||
iniconfig==2.0.0
|
|
||||||
maturin==1.8.1
|
maturin==1.8.1
|
||||||
packaging==24.2
|
-e git+ssh://git@git.mmbradley.ca:222/MichaelBradley/pyority_queue.git@a09b71cdb32e2637971629623200cd639f5dcccb#egg=pyority_queue
|
||||||
pluggy==1.5.0
|
|
||||||
-e git+ssh://git@git.mmbradley.ca:222/MichaelBradley/pyority_queue.git@661e1d220ae71002d66d2ba2d3dfddfe4ff1035d#egg=pyority_queue
|
|
||||||
pytest==8.3.4
|
|
||||||
|
|
|
@ -2,13 +2,12 @@ use std::cmp::Ordering;
|
||||||
|
|
||||||
/// Helper struct to associate an item with its priority
|
/// Helper struct to associate an item with its priority
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
// I mean I guess P should be Ord but I want to use f64 so whatever
|
pub struct Item<D, P: Ord> {
|
||||||
pub struct Item<D: Clone, P: PartialOrd + Clone> {
|
|
||||||
data: D,
|
data: D,
|
||||||
priority: P,
|
priority: P,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Clone, P: PartialOrd + Clone> Item<D, P> {
|
impl<D, P: Ord> Item<D, P> {
|
||||||
/// Creates a new instance
|
/// Creates a new instance
|
||||||
fn new(data: D, priority: P) -> Self {
|
fn new(data: D, priority: P) -> Self {
|
||||||
Self { data, priority }
|
Self { data, priority }
|
||||||
|
@ -21,26 +20,22 @@ impl<D: Clone, P: PartialOrd + Clone> Item<D, P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The relevant Ord implementations are based just on the priority
|
// The relevant Ord implementations are based just on the priority
|
||||||
impl<D: Clone, P: PartialOrd + Clone> Ord for Item<D, P> {
|
impl<D, P: Ord> Ord for Item<D, P> {
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
// Yeah this is bad design
|
self.priority.cmp(&other.priority)
|
||||||
// My excuse is that i'm still learning Rust
|
|
||||||
self.priority
|
|
||||||
.partial_cmp(&other.priority)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Clone, P: PartialOrd + Clone> PartialOrd for Item<D, P> {
|
impl<D, P: Ord> PartialOrd for Item<D, P> {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
self.priority.partial_cmp(&other.priority)
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Clone, P: PartialOrd + Clone> PartialEq for Item<D, P> {
|
impl<D, P: Ord> PartialEq for Item<D, P> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.priority == other.priority
|
self.priority == other.priority
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Clone, P: PartialOrd + Clone> Eq for Item<D, P> {}
|
impl<D, P: Ord> Eq for Item<D, P> {}
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
use super::{item::Item, pure::PureBacking};
|
use super::{item::Item, pure::PureBacking};
|
||||||
|
|
||||||
/// A data structure usable for backing a "keyed" queue
|
/// A data structure usable for backing a "keyed" queue
|
||||||
pub trait KeyedBacking<D: Clone + Send + Sync, P: Ord + Clone + Send + Sync>:
|
pub trait KeyedBacking<D, P: Ord>: PureBacking<Item<D, P>> {
|
||||||
PureBacking<Item<D, P>>
|
|
||||||
{
|
|
||||||
/// Update an item's priority
|
/// Update an item's priority
|
||||||
fn update(data: D, priority: P) -> Result<(), ()>;
|
fn update(data: D, priority: P) -> Result<(), ()>;
|
||||||
/// Remove an item from the queue
|
/// Remove an item from the queue
|
||||||
|
|
|
@ -4,7 +4,7 @@ use super::PureBacking;
|
||||||
|
|
||||||
/// A binary min-heap backed by an array
|
/// A binary min-heap backed by an array
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BinaryHeap<T: Ord + Clone + Send + Sync> {
|
pub struct BinaryHeap<T: Ord + Copy> {
|
||||||
data: Vec<T>,
|
data: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,23 +33,18 @@ impl fmt::Display for SiftError {
|
||||||
/// Whether a sift operation succeeded
|
/// Whether a sift operation succeeded
|
||||||
type SiftResult = Result<(), SiftError>;
|
type SiftResult = Result<(), SiftError>;
|
||||||
|
|
||||||
impl<T: Ord + Clone + Send + Sync> BinaryHeap<T> {
|
impl<T: Ord + Copy> BinaryHeap<T> {
|
||||||
/// Instantiates a new (empty) binary heap
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { data: vec![] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fix an index representing a node with valid children but that may violate the heap property compared to its immediate parent
|
/// Fix an index representing a node with valid children but that may violate the heap property compared to its immediate parent
|
||||||
fn sift_up(&mut self, i: usize) -> SiftResult {
|
fn sift_up(&mut self, i: usize) -> SiftResult {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
// Base case, at root so nothing to do
|
// Base case, at root so nothing to do
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if let Some(child) = self.data.get(i).cloned() {
|
} else if let Some(child) = self.data.get(i).copied() {
|
||||||
let parent_index = (i - 1) / 2;
|
let parent_index = (i - 1) / 2;
|
||||||
// Check if the heap property is violated
|
// Check if the heap property is violated
|
||||||
if child < self.data[parent_index] {
|
if child < self.data[parent_index] {
|
||||||
// Swap child with parent
|
// Swap child with parent
|
||||||
self.data[i] = self.data[parent_index].clone();
|
self.data[i] = self.data[parent_index];
|
||||||
self.data[parent_index] = child;
|
self.data[parent_index] = child;
|
||||||
|
|
||||||
// Repeat process with parent
|
// Repeat process with parent
|
||||||
|
@ -69,12 +64,12 @@ impl<T: Ord + Clone + Send + Sync> BinaryHeap<T> {
|
||||||
// Tried to sift a non-existent index
|
// Tried to sift a non-existent index
|
||||||
Err(SiftError::new(i, self.data.len()))
|
Err(SiftError::new(i, self.data.len()))
|
||||||
} else {
|
} else {
|
||||||
if let Some(first_child) = self.data.get(i * 2 + 1).cloned() {
|
if let Some(first_child) = self.data.get(i * 2 + 1).copied() {
|
||||||
let smaller_child_index;
|
let smaller_child_index;
|
||||||
let smaller_child;
|
let smaller_child;
|
||||||
|
|
||||||
// Find the smallest child and its index
|
// Find the smallest child and its index
|
||||||
if let Some(second_child) = self.data.get(i * 2 + 2).cloned() {
|
if let Some(second_child) = self.data.get(i * 2 + 2).copied() {
|
||||||
// Both children, use the smaller one
|
// Both children, use the smaller one
|
||||||
if first_child < second_child {
|
if first_child < second_child {
|
||||||
smaller_child = first_child;
|
smaller_child = first_child;
|
||||||
|
@ -91,7 +86,7 @@ impl<T: Ord + Clone + Send + Sync> BinaryHeap<T> {
|
||||||
|
|
||||||
if smaller_child < self.data[i] {
|
if smaller_child < self.data[i] {
|
||||||
// Swap parent with child
|
// Swap parent with child
|
||||||
self.data[smaller_child_index] = self.data[i].clone();
|
self.data[smaller_child_index] = self.data[i];
|
||||||
self.data[i] = smaller_child;
|
self.data[i] = smaller_child;
|
||||||
|
|
||||||
// Repeat process with child
|
// Repeat process with child
|
||||||
|
@ -108,19 +103,23 @@ impl<T: Ord + Clone + Send + Sync> BinaryHeap<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Ord + Clone + Send + Sync> FromIterator<T> for BinaryHeap<T> {
|
impl<T: Ord + Copy> FromIterator<T> for BinaryHeap<T> {
|
||||||
fn from_iter<U: IntoIterator<Item = T>>(iter: U) -> Self {
|
fn from_iter<U: IntoIterator<Item = T>>(iter: U) -> Self {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
data: Vec::from_iter(iter),
|
data: Vec::from_iter(iter),
|
||||||
};
|
};
|
||||||
for i in (0..=(this.data.len() / 2)).rev() {
|
for i in (0..=(this.data.len() / 2)).rev() {
|
||||||
this.sift_down(i).expect("Index error during heapify");
|
this.sift_down(i);
|
||||||
}
|
}
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Ord + Clone + Send + Sync> PureBacking<T> for BinaryHeap<T> {
|
impl<T: Ord + Copy> PureBacking<T> for BinaryHeap<T> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { data: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
fn add(&mut self, item: T) {
|
fn add(&mut self, item: T) {
|
||||||
// Append item
|
// Append item
|
||||||
self.data.push(item);
|
self.data.push(item);
|
||||||
|
@ -133,18 +132,22 @@ impl<T: Ord + Clone + Send + Sync> PureBacking<T> for BinaryHeap<T> {
|
||||||
// No extra processing
|
// No extra processing
|
||||||
0 | 1 => self.data.pop(),
|
0 | 1 => self.data.pop(),
|
||||||
_ => {
|
_ => {
|
||||||
// Get the original root
|
let last = self
|
||||||
let root = self.data[0].clone();
|
|
||||||
|
|
||||||
// Move final item to the root and sift down to regain heap property
|
|
||||||
self.data[0] = self
|
|
||||||
.data
|
.data
|
||||||
.pop()
|
.pop()
|
||||||
.expect("Vector claimed not to be empty but was");
|
.expect("Vector claimed not to be empty but was");
|
||||||
|
let root = self
|
||||||
|
.data
|
||||||
|
.get_mut(0)
|
||||||
|
.expect("Vector claimed to have multiple items but didn't");
|
||||||
|
|
||||||
|
// Move final item to the root and sift down to regain heap property
|
||||||
|
let best = *root;
|
||||||
|
*root = last;
|
||||||
self.sift_down(0).unwrap();
|
self.sift_down(0).unwrap();
|
||||||
|
|
||||||
// Return original root
|
// Return original root
|
||||||
Some(root)
|
Some(best)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
pub mod binary_heap;
|
pub mod binary_heap;
|
||||||
|
|
||||||
/// A data structure usable for backing a "pure" queue
|
/// A data structure usable for backing a "pure" queue
|
||||||
pub trait PureBacking<T: Ord + Send + Sync>: Send + Sync {
|
pub trait PureBacking<T: Ord>: FromIterator<T> {
|
||||||
|
/// Instantiates a new data structure
|
||||||
|
fn new() -> Self;
|
||||||
/// Places an item into the queue
|
/// Places an item into the queue
|
||||||
fn add(&mut self, item: T);
|
fn add(&mut self, item: T);
|
||||||
/// Removes the item with minimum priority, if it exists
|
/// Removes the item with minimum priority, if it exists
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,12 +1,16 @@
|
||||||
pub mod backing;
|
pub mod backing;
|
||||||
pub mod queue;
|
|
||||||
|
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use queue::pure::PureQueue;
|
|
||||||
|
|
||||||
/// Bindings for the Rust queue implementations
|
/// Formats the sum of two numbers as string.
|
||||||
|
#[pyfunction]
|
||||||
|
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
|
||||||
|
Ok((a + b).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Python module implemented in Rust.
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn pyority_queue(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
fn pyority_queue(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_class::<PureQueue>()?;
|
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod pure;
|
|
|
@ -1,25 +0,0 @@
|
||||||
// A "pure" priority queue that supports duplicates, but not arbitrary deletions or weight updates
|
|
||||||
use crate::backing::{
|
|
||||||
item::Item,
|
|
||||||
pure::{binary_heap::BinaryHeap, PureBacking},
|
|
||||||
};
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
#[pyclass]
|
|
||||||
pub struct PureQueue {
|
|
||||||
backing: Box<dyn PureBacking<Item<Py<PyAny>, f64>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl PureQueue {
|
|
||||||
#[new]
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
backing: Box::new(BinaryHeap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __len__(self_: PyRef<'_, Self>) -> usize {
|
|
||||||
self_.backing.len()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from pyority_queue import PureQueue
|
|
||||||
|
|
||||||
|
|
||||||
type PureQueueInitializer = list[int | float] | tuple[int | float, ...]
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_creation():
|
|
||||||
queue = PureQueue()
|
|
||||||
assert len(queue) == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("items", ([], [0, 1, 2], (0, 1, 2), (0.0, 1.0, 2.0), range(100)))
|
|
||||||
def test_creation(items: PureQueueInitializer):
|
|
||||||
queue = PureQueue()
|
|
||||||
assert len(queue) == len(items)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("items", ([], (0,), (-1, 3), range(100)))
|
|
||||||
def test_iteration(items: PureQueueInitializer):
|
|
||||||
queue = PureQueue(items)
|
|
||||||
assert len(list(queue)) == len(items)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("items", ([], (-3, 5), (3.0, 2.0, 1.0), (3, 6, 8, 5, 7, 4, 2, 0), range(100)))
|
|
||||||
def test_sorting(items: PureQueueInitializer):
|
|
||||||
queue = PureQueue(items)
|
|
||||||
assert list(queue) == sorted(items)
|
|
||||||
|
|
||||||
|
|
||||||
def test_insertion():
|
|
||||||
queue = PureQueue[int]((4, 2, 8, 6))
|
|
||||||
queue.insert(7)
|
|
||||||
queue.insert(0)
|
|
||||||
queue.insert(3)
|
|
||||||
assert list(queue) == [0, 2, 3, 4, 6, 7, 8]
|
|
||||||
|
|
||||||
|
|
||||||
def test_duplicates():
|
|
||||||
queue = PureQueue[int]((0, 0, 0, 5, 5))
|
|
||||||
queue.insert(3)
|
|
||||||
queue.insert(3)
|
|
||||||
assert list(queue) == [0, 0, 0, 3, 3, 5, 5]
|
|
Loading…
Add table
Add a link
Reference in a new issue