Function Repository Resource:

IndexedQueue

Source Notebook

Provide an indexed priority queue data structure with its standard operations

Contributed by: Daniel Lichtblau

ResourceFunction["IndexedQueue"][]

returns an indexed priority queue initialized with default settings.

ResourceFunction["IndexedQueue"][{"InitialSize"size,"NullElement"null,"ComparisonFunction"comp},"Initialize"]

returns an indexed queue initialized to have length size, null element null and comparison function comp.

ResourceFunction["IndexedQueue"][inits,vals,"Initialize"]

returns an indexed queue initialized with parameters in inits and containing values vals.

ResourceFunction["IndexedQueue"][queue,"Dequeue"]

returns a list containing the index and value of the topmost element in queue.

ResourceFunction["IndexedQueue"][queue,value,"Enqueue"]

places value in queue, associating to it an index equal to the current queue length plus one.

ResourceFunction["IndexedQueue"][queue,index,"Remove"]

removes the element indexed by index from queue.

ResourceFunction["IndexedQueue"][queue,index,value,"UpdateValue"]

replaces the current value of element index with value, reordering queue as needed.

ResourceFunction["IndexedQueue"][queue,"EmptyQ"]

gives True if queue has no elements, otherwise False.

ResourceFunction["IndexedQueue"][queue,"Length"]

gives the number of elements in queue.

ResourceFunction["IndexedQueue"][queue,"Peek"]

gives the value of the topmost element in queue.

ResourceFunction["IndexedQueue"][queue,"TopIndex"]

gives the index of the topmost element in queue.

ResourceFunction["IndexedQueue"][queue,keyindex,"ContainsQ"]

gives True if keyindex is in queue, otherwise False.

ResourceFunction["IndexedQueue"][queue,keyindex,"Value"]

gives the value associated to the element indexed by keyindex, or the null value if it is not present.

Details

Indexed queues are also used in the eager implementation of Prim's algorithm for computing minimum spanning trees.
Another application is to find an approximate minimal vertex cover for a graph using an efficient greedy method.
Indexed queues can be used in the same way as ordinary priority queues, with the added capabilities of altering index values dynamically and removing indicies other than the topmost.
The default values for "InitialSize", "NullElement" and "ComparisonFunction" are 16, Missing["empty slot"] and Greater, respectively.
This implementation of priority queues uses binary heaps along with lists from key index to queue position and vice versa.
In binary heap implementations such as this one, each element in a priority queue has either zero, one or two children. If the jthelement has children, they will be located at positions 2j and 2j+1 (with the latter position empty if there is ony one child). Elements are contiguous in the sense that there is never a hole (empty slot) until after the last element.
Each child is ordered less than its parent, but the children do not satisfy a prescribed order with respect to one another.
Generally, an indexed queue is an array of fixed size, so adding and removing elements do not incur memory allocation or deallocation. The one exception is when adding an element to a queue that is at capacity, at which point the heap size is doubled.
ResourceFunction["IndexedQueue"] has the attribute HoldFirst.

Examples

Basic Examples (9) 

Initialize a priority queue (aka ordered heap) with 20 random reals between -1 and 1:

Check the size:

In[1]:=
ResourceFunction["IndexedQueue"][heap, "Length"]
Out[1]=

Check the value of the largest element:

In[2]:=
ResourceFunction["IndexedQueue"][heap, "Peek"]
Out[2]=

Add 80 more random values to the heap:

Recheck length and largest value:

In[3]:=
{ResourceFunction["IndexedQueue"][heap, "Length"], ResourceFunction["IndexedQueue"][heap, "Peek"]}
Out[3]=

When we dequeue elements one-by-one, the result is a sorted list of values; this method is called the "heap-sort" algorithm:

In[4]:=
sorted = Reap[
    While [! ResourceFunction["IndexedQueue"][heap, "EmptyQ"], Sow[ResourceFunction["IndexedQueue"][heap, "Dequeue"]]]][[2, 1]];
sorted[[All, 2]]
Out[4]=

Check that it is indeed empty:

In[5]:=
ResourceFunction["IndexedQueue"][heap, "EmptyQ"]
Out[5]=

An alternative check is that the length is zero:

In[6]:=
ResourceFunction["IndexedQueue"][heap, "Length"] == 0
Out[6]=

Check that the result is correctly sorted largest-to-smallest:

In[7]:=
sorted[[All, 2]] == ReverseSort[Join[inits, rest]]
Out[7]=

Scope (2) 

IndexedQueue can work with any data type for which an ordering exists, such as words with alphabetical order:

Now sort them in dictionary order by dequeuing, returning both original positions and values:

In[8]:=
sortedwords = Reap[While [! ResourceFunction["IndexedQueue"][heap, "EmptyQ"], Sow[ResourceFunction["IndexedQueue"][heap, "Dequeue"]]]][[2, 1]]
Out[8]=

Properties and Relations (4) 

For a queue of size n, we expect speed proportional to n log n for creating and removing all elements. We test this by timing a basic sorting, then doubling size twice.

Use a queue to enqueue 10000 random values:

In[9]:=
defaultNames = {"InitialSize", "NullElement", "ComparisonFunction"};
size = 10^4;
vals = RandomReal[{-1, 1}, size];
Timing[heap = ResourceFunction["IndexedQueue"][
    Thread[defaultNames -> {size, 0., Greater}], vals, "Initialize"];]
Out[9]=

Now dequeue all elements:

In[10]:=
Timing[sorted = Reap[While [! ResourceFunction["IndexedQueue"][heap, "EmptyQ"], Sow[ResourceFunction["IndexedQueue"][heap, "Dequeue"]]]][[2, 1]];]
Out[10]=
In[11]:=
sorted[[All, 2]] === ReverseSort[vals]
Out[11]=

Double the size and check enqueing and dequeing timings:

In[12]:=
(* Evaluate this cell to get the example input *) CloudGet["https://www.wolframcloud.com/obj/bf59a93d-868c-468c-a16b-081ec72c4fa2"]
Out[12]=

Again double the size and check enqueing and dequeing timings:

In[13]:=
(* Evaluate this cell to get the example input *) CloudGet["https://www.wolframcloud.com/obj/35105f07-1ea6-4c80-87f4-ee09c9d5df6b"]
Out[13]=

Possible Issues (5) 

Attempting to dequeue from an empty queue gives the default element:

Out[13]=

IndexedQueue has the same asymptotic complexity as the resource function PriorityQueue, but it has a larger constant factor due to the extra work in handling enqueuing and dequeuing.

Define a timing test for purposes of comparing IndexedQueue with PriorityQueue:

Create a set of 10000 random real values:

Time enqueuing and dequeuing using IndexedQueue:

In[14]:=
timingTest[
ResourceFunction["IndexedQueue"]]
Out[14]=

Compare to enqueuing and dequeuing using PriorityQueue:

In[15]:=
timingTest[ResourceFunction["PriorityQueue", "Function"]]
Out[15]=

Neat Examples (16) 

Minimum Spanning Trees: Prim's Method (lazy version) (8) 

A minimum spanning tree for a connected graph is a tree that connects every vertex, has no cycles and is of minimal total edge weight. The "eager" version of Prim's method for computing the minimum spanning tree of a connected, weighted undirected graph works as follows. Start with any vertex. For any edge from that vertex, enqueue the vertex it connects to with value set to the edge weight. Iteratively remove from the queue the vertex of lowest edge weight and add it to the spanning tree under construction. Then iterate over the vertices connected to that vertex. Ignore those that already are in the tree. Enqueue with edge weight those that are not already in the queue. For a vertex currently in the queue, update the value if the edge weight is less than that which was previously associated to that vertex. For a graph of V vertices and E edges the complexity is O(E log V).

Code for the eager form of Prim's algorithm, specialized to a complete graph with edge weights as pairwise distances between planar points:

Create an example set of points:

In[16]:=
SeedRandom[1234];
nverts = 250;
ListPlot[pts = RandomReal[{-10, 10}, {nverts, 2}]]
Out[16]=

Find the minimal spanning tree and total edge weight:

In[17]:=
Timing[{tdist, treegraph} = eagerPrim[pts];]
Out[17]=

Check the total edge weight:

In[18]:=
tdist
Out[18]=

Plot the spanning tree:

In[19]:=
treegraph
Out[19]=

Double the vertex count and repeat:

In[20]:=
nverts *= 2;
pts = RandomReal[{-10, 10}, {nverts, 2}];
Timing[{tdist, treegraph} = eagerPrim[pts]]
Out[20]=

Double again:

In[21]:=
nverts *= 2;
pts = RandomReal[{-10, 10}, {nverts, 2}];
Timing[{tdist, treegraph} = eagerPrim[pts]]
Out[21]=

Using the built-in Prim method is an order of magnitude faster:

In[22]:=
edges = Flatten[
   Table[UndirectedEdge[i, j], {i, nverts - 1}, {j, i + 1, nverts}]];
edgeweights = Flatten[Table[
    EuclideanDistance[pts[[i]], pts[[j]]], {i, nverts - 1}, {j, i + 1,
      nverts}]];
gr = Graph[Range[nverts], edges, EdgeWeight -> edgeweights, VertexCoordinates -> pts];
Timing[msp = FindSpanningTree[gr, Method -> "Prim"]]
Out[22]=

Approximate vertex covers (8) 

A vertex cover for a graph is a subset of vertices that hit all edges. Stated differently, every edge must have at least one endpoint in the cover. A cover with the minimal number of vertices is called a minimal cover. An approximate minimal cover can be found efficiently using a greedy algorithm as follows. First choose a vertex that hits the most edges, and remove those edges from consideration. This means we decrement the edge counts of all vertices connected to the one that was selected. Iteratively remove a vertex hitting the greatest number of remaining edges, and decrement edge counts of vertices connected to the one selected. Continue until no edges remain unhit. This can be implemented efficiently using an indexed priority queue.

Implement a greedy-with-updates approximate vertex cover using an indexed queue:

Create a random graph with 30 vertices and 80 edges:

In[23]:=
SeedRandom[1234];
gr = RandomGraph[{30, 80}]
Out[23]=

Find an approximate vertex cover for this graph:

In[24]:=
ac = approximateVertexCover[gr]
Out[24]=

Check that it is a cover:

In[25]:=
VertexCoverQ[gr, ac]
Out[25]=

Compare the length to the actual minimal cover length:

In[26]:=
{Length[ac], Length[FindVertexCover[gr]]}
Out[26]=

Time this for a much larger random graph:

In[27]:=
SeedRandom[1234];
gr2 = RandomGraph[{3000, 12000}];
Timing[ac2 = approximateVertexCover[gr2];]
Out[27]=

Check that it is a cover and find the length:

Out[27]=
Out[27]=

A method used in the resource function ApproximateVertexCover is substantially similar to the one above:

In[28]:=
Timing[ac3 = ResourceFunction["ApproximateVertexCover"][gr2];]
Out[28]=

Version History

  • 1.0.0 – 10 May 2021

Related Resources

License Information