-
Notifications
You must be signed in to change notification settings - Fork 325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Priority queue #3
Comments
This would be super useful for my current use case! Sorry if this is a beginner question here, but I was wondering, what advantage would this offer over using an OrderedSet and sorting it by the priority in a model (as in, I have a model with a priority property and I sort the OrderedSet by that property)? (Also, would the pqueue auto sort upon insertion?) |
@julianschiavo It takes less computing while you read the biggest few elements. It is more effective when you only use some of the biggest elements but not all collection. It's a
A priority queue is not a
A heap is not stored as sorted sequence, it only guarantees the top elements are bigger or smaller than bottom elements. Heap is a complete binary tree and could be stored in a continuous array. Index
It can be stored as array:
It is partial sorted bu not full sorted upon insertion or deletion. So it take less time to insert or delete an element.
Both operation takes no more than logN times swap operation. |
Got it, thank you so much for your in-depth explanation, really helps with understanding the concept! That's exactly what I'm looking for if I have that right 😄 So to make sure I understand, the idea behind "it only guarantees the top elements are bigger [...]" is that as long as I dequeue from the top (in a PQ of highest priority at the top), each element I dequeue will have the top priority in all the elements? (I understand from your explanation this continues to be the case if I inserted new elements—I guess that's kind of what I meant by sorting on insertion, though I know now it's not technically doing that) |
Yes. See the heap in my reply. If you take
If you dequeue all elements one by one, they would be ordered by priority. |
👍 Thank you! |
I've tried to implement this, and it got me thinking. Firstly, enum Priority {
case high
case low
}
struct Work {
var priority: Priority
var work: () -> ()
} And if some would want to use let w1 = Work(priority: .low) { print("Hello") }
let w2 = Work(priority: .low) { print("World") }
extension Work: Comparable {
static func < (lhs: Work, rhs: Work) -> Bool {
lhs.priority < rhs.priority
}
static func == (lhs: Work, rhs: Work) -> Bool {
return false // that means w1 < w1, contradiction
return true // nonsense, obviously w1 is not equal to w2
return lhs.priority == rhs.priority // same as above
}
} So, there is no correct implementation of protocol Prioritizable {
assotiatedtype Priority: Comparable
var priority: Priority { get }
} And heap will require struct Heap<T, U:Comparable> {
var priority: (T) -> U
...
} But that can have performance implications, as compiler can't inline that getter. Secondly, given that, heap must be "stable", in sense that order of elements with equal priorities must be preserved (FIFO): let w1 = Work(priority: .low) { print("Hello") }
let w2 = Work(priority: .low) { print("World") }
var heap = Heap<Work>()
heap.push(w1)
heap.push(w2)
heap.pop().work() // outputs "Hello"
heap.pop().work() // outputs "World" And that's an implementation challenge as well. ps: for any who is interested, my implementation attempt is here |
@zienag I think it's better to just give the |
@lorentey what do you think? Does performance implication from stored |
This approach would have another problem that there couldn't be a conditional struct Heap<Element, Priority: Comparable> {
var priority: KeyPath<Element, Priority>
...
} I don't know if this use of key paths would optimise better than closures though. |
I've implemented it with copying and comparing poping elements one-by-one:
This is rather inefficient (memory O(n + m)), but correct. I couldn't find any more efficient methods via quick googling. |
More importantly, I think you just proved that |
Another option is to separate priority from Element:
In that case additional protocol is not needed, |
I think I'd prefer to start with an implementation that simply relied on the Closure based approaches suffer from the fact that closures cannot be compared, which makes operations that work on two heap instances (such as merge) impossible to safely implement. They are also generally less efficient than relying on the type system -- with @timvermeulen reminded me today that min-max heaps are a thing, and they get rid of the inconvenience of having to create a wrapper type just to flip the sort order. With all that said, I like @zienag's idea to model the priority as a separate value! It would be interesting to implement that too, to see how it compares in practice. (One potential drawback I can see has to do with memory use -- the priorities would need to be stored in their own storage buffer.) |
Talking about min-max heaps, I would like to point out this article too as some food for thought - it may be that we can have both the functionality and the performance at the same time. https://probablydance.com/2020/08/31/on-modern-hardware-the-min-max-heap-beats-a-binary-heap/ |
@hassila Thanks for sharing, that's a great article! If we're able to observe the same performance benefits, I agree we should skip straight to the min-max heap implementation the article describes! |
Given that min-max heaps let us avoid having to encode the sort order into the type, does it make sense to go with @pyrtsa's suggestion of using key paths to get the priority? public struct MinMaxHeap<Element, Priority: Comparable> {
private var storage: [Element]
public let keyPath: KeyPath<Element, Priority>
public init(keyPath: KeyPath<Element, Priority>) {
…
}
I've found this helpful in cases where the type in the queue has a priority, e.g.: public struct Request {
public let uuid: UUID
public let request: HTTPRequest
public let priority: Float
…
}
public class Client {
private var requestQueue = PriorityQueue(keyPath: \Request.priority)
…
Convenience initializers could be added for types that implement extension MinMaxHeap where Element: Comparable, Priority == Element {
public init() {
self.init(keyPath: \Element.self)
}
} |
I don't think so -- at least not in the first implementation. Like Once we have that, we can then use it to experiment with creating a keypath-based heap variant as a wrapper around the core heap. |
I'm using |
I decided to publish a Swift package with |
By the way, what would it take for |
I'm not sure if this is the right place for this feedback, but I've noticed a couple of minor (nitty) modifications we might make to the
I'd open a PR if these changes are welcome :) |
I also implemented a It does make me wonder though, would we also want a |
This package is severely resource constrained. Experimenting in a separate code base is the right move for now, at least until we manage to unblock the release pipeline.
Fixed-capacity variants of all flat data structures is definitely desirable. However, I'd like to tie their introduction with noncopyable containers, which is the context such variants are most relevant. (For now, fixed-capacity heaps (arrays, deques, ordered sets, etc) can be emulated by embedding the copy-on-write containers in a simple wrapper that traps in advance if any operation will need to resize the container beyond its reserved capacity.) |
|
Priority queues are commonly requested data structures, and are universally useful. A high-quality implementation would be a natural candidate for inclusion in this package. (Priority queues are used in sorting algorithms, various graph algorithms, networking protocol implementations, etc. etc. etc.)
I expect a straightforward array-backed binary heap might make most sense for the initial implementation, but I wouldn't be surprised if a fancier variant proved better overall.
This would likely be mostly an implementation exercise rather than an API design challenge (although API design is never trivial). We'd probably need to experiment with several different data structure implementations, and perhaps even come up with our own variants that's better suited to Swift's unique combination of language features.
The text was updated successfully, but these errors were encountered: