Creating a Priority Queue: An Adventure

DarthCthulhu via Digitalmars-d-learn digitalmars-d-learn at puremagic.com
Fri Aug 7 06:54:54 PDT 2015


Okay, so, I decided to scrap the BinaryHeap version of the 
priority queue, going back to basics and utilizing a simple 
array. It works, huzzah!

Code:

module data_structures.priority_queue;

import std.array;
import std.range: assumeSorted;
import std.typecons: Tuple;

/*
	Templated Priority Queue

	Usage: 	PriorityQueue!(PRIORITY_TYPE, VALUE_TYPE, 
OPTIONAL_PREDICATE)

*/
struct PriorityQueue(P, V, alias predicate = "a < b") {

	// To make the code a bit more readable
	alias Tuple!(P, V) PV;

	PV _q[];

	// Forward most function calls to the underlying array.
     	PV[]* opDot() {
         	return &_q;
     	}

	// Determine if the queue is empty
	@property bool empty () {

		return (_q.length == 0);
	}

	// Needed so foreach can work
	@property PV front() {

		return _q.front;
	}

	// Chop off the front of the array
	@property void popFront() {

		_q = _q[1 .. $];
	}

	// Insert a record via a template tuple
	void insert(ref PV rec) {

		// Empty queue?
		if (_q.length == 0 ) {
			// just put the record into the queue
			_q ~= rec;

			return;
		}

		// Assume the queue is already sorted according to PREDICATE
		auto a = assumeSorted!(predicate)(_q);

		// Find a slice containing records with priorities less than 
the insertion rec
		auto p = a.lowerBound(rec);
		
		int location = p.length;
		
		// Insert the record
		_q.insertInPlace(location, rec);

	}

	void insert(PV rec) {
		insert(rec);
	}

	// Insert a record via decomposed priority and value
	void insert(P priority, V value) {

		PV rec = PV(priority, value);

		// Insert the record
		insert(rec);

	}

	// Merge two Priority Queues, returning the merge.
	// The two queues must obviously be of the same type in Priority 
and Value, and predicate;
	ref PriorityQueue!(P, V, predicate) merge(ref PriorityQueue!(P, 
V, predicate) qmerge) {
		
		// Make a copy of this PriorityQueue
		PriorityQueue!(P, V, predicate)* qreturn = new 
PriorityQueue!(P, V, predicate);
		qreturn._q = _q.dup;

		// Add in all the elements of the merging queue
		foreach(rec; qmerge) {
			qreturn.insert(rec);
		}

		// Return the resulting merged queue
		return *qreturn;

	}

}

unittest {

	alias P = int;
	alias V = string;
	alias PV = Tuple!(P, V);
	alias PQ = PriorityQueue!(P, V, "a < b");
	PQ pq, pq2, pq3;

	import std.typecons: tuple;

	// Test basic insertion
	pq.insert(10, "HELLO10");
	pq.insert(11, "HELLO11");
	pq.insert(3, "HELLO3");
	pq.insert(31, "HELLO31");
	pq.insert(5, "HELLO5");
	pq.insert(10, "HELLO10-2");

	assert(pq.length == 6);

	foreach (const e; pq) {}    // iteration
	assert(!pq.empty);          // shouldn't consume queue

	// Copy by value
	pq2 = pq;

	foreach (priority, value; pq) {
		pq.popFront();
	}

	// pq and pq2 should be independent
	assert( !pq2.empty);
	assert( pq.empty );

	// Test merging
	pq3.insert(tuple(12, "HELLO12"));
	pq3.insert(Tuple!(int, string)(17, "HELLO17"));
	pq3.insert(tuple(7, "HELLO7"));

	pq = pq2.merge(pq3);

	assert ( !pq.empty);

	assert(pq.front == tuple(3, "HELLO3"));
	pq.popFront;
	assert(pq.front == tuple(5, "HELLO5"));
	pq.popFront;
	assert(pq.front == tuple(7, "HELLO7"));
	pq.popFront;

	assert( pq.length == 6 );
}

And a little driver main() to illustrate the queue a bit better:

    main() {
	
			PriorityQueue!(int, string) pq, pq2, pq3;
	
			pq.insert(10, "HELLO10");
			pq.insert(11, "HELLO11");
			pq.insert(Tuple!(int, string)(3, "HELLO3"));
			pq.insert(5, "HELLO5");
			pq.insert(Tuple!(int, string)(12, "HELLO12"));
			pq.insert(Tuple!(int, string)(17, "HELLO17"));

			pq2.insert(Tuple!(int, string)(15, "HELLO15"));
			pq2.insert(Tuple!(int, string)(21, "HELLO21"));

			writefln("\tPQ: %s \n\tPQ2: %s \n\tPQ3: %s", pq, pq2, pq3);

			pq3 = pq.merge(pq2);

			foreach(priority, value; pq3) {

				writefln("Priority: %s \tValue: %s \tLength: %s", priority, 
value, pq3.length);
				pq3.popFront();
			}

		}


I think I like this version better than the BinaryHeap one. It's 
a bit simpler and can be extended easier. For instance, I think 
it would be pretty easy to use mixins to make a seperate 
PriorityQueueChangable version which allows the alteration of 
priorities. It's also pretty easy to search for priorities or 
values of certain requirements.

I don't know enough about D to really say if its performant in 
any way, but it does do the job. It took about a week to get to 
this final form, coming from someone familiar with programming 
but not D.

A quick review of this project from my perspective:

The first day was mostly taken up with reading about D, then 
doing an abortive attempt with associative arrays until I 
realized that associative arrays are not and can not be sorted.

The next day involved finding BinaryHeap and puzzling out how to 
use it. I think I got really hung up on the template syntax for a 
bit. It took a little to get used to. I also over engineered the 
thing with a separate helper struct until I embarrassingly 
discovered tuples would do the job easier.

The third day had me hitting a wall with not thinking that the 
BinaryHeap eats its elements on traversal. Coming here was a 
great help.

The fourth day was mostly cleaning up the BinaryHeap version and 
becoming increasingly dissatisfied with it.

And now here we are today, with a new version written with just 
standard arrays. The big problems I had were screwing up the 
sorting predicate (I couldn't figure out why everything would 
work correctly in one direction, but not the other until I 
realized that assumeSorted needed to be told which predicate to 
use). The predicate was also needed for the merge portion 
otherwise changing the direction of the queue would cause a type 
mismatch.

Those two problems plus a few other minor ones later, and the 
queue is complete. I have to say, I really like D a lot!

Thanks to you all for the assistance!


More information about the Digitalmars-d-learn mailing list