From 71f859af9bd3e4fa3b6aad833f467e6191a4bd1a Mon Sep 17 00:00:00 2001 From: Heiko Schaefer Date: Fri, 14 Jul 2023 21:05:29 +0200 Subject: [PATCH] Implement PairPriorityQueue --- .../wot/dijkstra/PairPriorityQueue.kt | 44 ++++++ .../wot/dijkstra/PairPriorityQueueTest.kt | 146 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueue.kt create mode 100644 wot-dijkstra/src/test/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueueTest.kt diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueue.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueue.kt new file mode 100644 index 00000000..3e1c7eef --- /dev/null +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueue.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Heiko Schaefer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.dijkstra + +import java.util.PriorityQueue + +/** + * A de-duplicating min-priority queue for key-value pairs. + * + * When an element is popped, the queue entry with the *most desirable + * value* (that is: low cost) is popped (if there are multiple elements + * with the same minimal value, one of them is returned.) + * + * When inserting an element, if there is already an element with the same + * key, the element with the smaller value is kept. + */ +internal class PairPriorityQueue>() { + + // NOTE: This implementation is not optimized for efficient inserts! + // - Each insert() involves a linear search by key + // - Each insert() sorts eagerly (via j.u.PriorityQueue.add()) + + private val pq: PriorityQueue> = PriorityQueue { o1, o2 -> + // Order priority queue entries by value (min first) + o1.second.compareTo(o2.second) + } + + fun insertOrUpdate(key: K, value: V) { + when (val element = pq.find { it.first == key }) { + null -> pq.add(Pair(key, value)) // Add as a new element + else -> { + // If the new value is "cheaper": replace the element + if (value < element.second) { + pq.remove(element) + pq.add(Pair(key, value)) + } + } + } + } + + fun pop(): Pair? = pq.poll() +} \ No newline at end of file diff --git a/wot-dijkstra/src/test/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueueTest.kt b/wot-dijkstra/src/test/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueueTest.kt new file mode 100644 index 00000000..9e384baf --- /dev/null +++ b/wot-dijkstra/src/test/kotlin/org/pgpainless/wot/dijkstra/PairPriorityQueueTest.kt @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2023 Heiko Schaefer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.dijkstra + +import kotlin.test.Test +import kotlin.test.assertEquals + +class PairPriorityQueueTest { + + @Test + fun simple1() { + val pq: PairPriorityQueue = PairPriorityQueue(); + + pq.insertOrUpdate(0, 0); + pq.insertOrUpdate(1, 1); + pq.insertOrUpdate(2, 2); + pq.insertOrUpdate(3, 3); + pq.insertOrUpdate(4, 4); + pq.insertOrUpdate(5, 5); + + assertEquals(Pair(0, 0), pq.pop()); + assertEquals(Pair(1, 1), pq.pop()); + assertEquals(Pair(2, 2), pq.pop()); + assertEquals(Pair(3, 3), pq.pop()); + assertEquals(Pair(4, 4), pq.pop()); + assertEquals(Pair(5, 5), pq.pop()); + + assertEquals(null, pq.pop()); + assertEquals(null, pq.pop()); + } + + @Test + fun simple2() { + val pq: PairPriorityQueue = PairPriorityQueue(); + + pq.insertOrUpdate(0, 0); + pq.insertOrUpdate(1, -1); + pq.insertOrUpdate(2, -2); + pq.insertOrUpdate(3, -3); + pq.insertOrUpdate(4, -4); + pq.insertOrUpdate(5, -5); + + assertEquals(Pair(5, -5), pq.pop()); + assertEquals(Pair(4, -4), pq.pop()); + assertEquals(Pair(3, -3), pq.pop()); + assertEquals(Pair(2, -2), pq.pop()); + assertEquals(Pair(1, -1), pq.pop()); + assertEquals(Pair(0, 0), pq.pop()); + + assertEquals(null, pq.pop()); + assertEquals(null, pq.pop()); + } + + @Test + fun simple3() { + val pq: PairPriorityQueue = PairPriorityQueue(); + + pq.insertOrUpdate(0, 0); + pq.insertOrUpdate(1, 1); + pq.insertOrUpdate(5, 5); + pq.insertOrUpdate(2, 2); + pq.insertOrUpdate(4, 4); + pq.insertOrUpdate(3, 3); + + assertEquals(Pair(0, 0), pq.pop()); + assertEquals(Pair(1, 1), pq.pop()); + assertEquals(Pair(2, 2), pq.pop()); + assertEquals(Pair(3, 3), pq.pop()); + assertEquals(Pair(4, 4), pq.pop()); + assertEquals(Pair(5, 5), pq.pop()); + assertEquals(null, pq.pop()); + assertEquals(null, pq.pop()); + } + + @Test + fun simple4() { + val pq: PairPriorityQueue = PairPriorityQueue(); + assertEquals(null, pq.pop()); + + pq.insertOrUpdate(0, 0); + pq.insertOrUpdate(0, 0); + assertEquals(Pair(0, 0), pq.pop()); + assertEquals(null, pq.pop()); + } + + @Test + fun simple5() { + val pq: PairPriorityQueue = PairPriorityQueue(); + assertEquals(null, pq.pop()); + + pq.insertOrUpdate(0, 0); + pq.insertOrUpdate(0, 0); + assertEquals(Pair(0, 0), pq.pop()); + pq.insertOrUpdate(0, 0); + assertEquals(Pair(0, 0), pq.pop()); + assertEquals(null, pq.pop()); + } + + + @Test + fun duplicates() { + val pq: PairPriorityQueue = PairPriorityQueue(); + + // Insert different keys with value i. + for (i in 19 downTo 0) { + pq.insertOrUpdate(i, i); + } + // Insert the same keys with lower value `i-1`. + // This should overwrite the old keys. + for (i in 19 downTo 0) { + pq.insertOrUpdate(i, i - 1); + } + + // Insert the same keys with a higher value. + // These should be ignored. + for (i in 19 downTo 0) { + pq.insertOrUpdate(i, i); + } + + for (i in 0 until 20) { + assertEquals(Pair(i, i - 1), pq.pop()); + } + assertEquals(null, pq.pop()); + assertEquals(null, pq.pop()); + } + + @Test + fun insert_pop() { + val pq: PairPriorityQueue = PairPriorityQueue(); + + // Insert different keys with value i+1. + for (i in 9 downTo 0) { + pq.insertOrUpdate(i, i + 1); + } + // Insert the same keys with their own value. This should + // overwrite the old keys. + for (i in 0 until 10) { + pq.insertOrUpdate(i, i); + assertEquals(Pair(i, i), pq.pop()); + } + assertEquals(null, pq.pop()); + assertEquals(null, pq.pop()); + } +} \ No newline at end of file