简体   繁体   中英

How do I implement a queue with a dynamically allocated array?

I want to implement queue using a dynamically allocated array. This presents some problems that I'm unsure of how to deal with. How do I check if the queue is empty? How do I keep track of how many elements that are in the queue at a single instant?

For the second problem, I figure I can create a variable to keep track of the number of elements in the queue which updates anytime I use realloc() . I'm welcome to other suggestions though.

If you have any more considerations I should be thinking about please present them.

Here's a fairly simple array-based FIFO queue:

struct queue {
  T *store;     // where T is the data type you're working with
  size_t size;  // the physical array size
  size_t count; // number of items in queue
  size_t head;  // location to pop from
  size_t tail;  // location to push to
};

struct queue q;
q.store = malloc( sizeof *q.store * SIZE );
if ( q.store )
{
  q.size = SIZE;
  q.count = q.head = q.tail = 0;
}

To push an item, do something like the following:

int push( struct queue q, T new_value )
{
  if ( q.count == q.size )
  {
    // queue full, handle as appropriate
    return 0;
  }
  else
  {
    q.store[q.tail] = new_value;
    q.count++;
    q.tail = ( q.tail + 1 ) % q.size;
  }
  return 1;
}

Pops are similar

int pop( struct queue q, T *value )
{
  if ( q.count == 0 )
  {
    // queue is empty, handle as appropriate
    return 0;
  }
  else
  {
    *value = q.store[q.head];
    q.count--;
    q.head = ( queue.head + 1 ) % q.size;
  }

  return 1;
}

As written, this is a "circular" queue; the head and tail pointers will wrap around as items are pushed and popped from the queue.

As with any approach, this has strengths and weaknesses. It's simple, and it avoids excessive memory management (just allocating the backing store). Just updating count is simpler than trying to compute it from head and tail .

Extending the backing store isn't quite so straightforward; if your tail pointer has wrapped around, you'll have to shift everything after head :

Before:

+---+---+---+---+---+
| x | x | x | x | x |
+---+---+---+---+---+
          ^   ^
          |   |
          |   +---  head
          +-------  tail

After:        
+---+---+---+---+---+---+---+---+---+---+
| x | x | x |   |   |   |   |   | x | x |
+---+---+---+---+---+---+---+---+---+---+
          ^                       ^
          |                       |
          |                       +---  head
          +-------  tail

Also, if you want something more sophisticated than a simple FIFO, you'll probably want to use a different data structure as your backing store.

Typically, you keep a pointer variable to the 'Head' of your queue. When this pointer is null, the list is empty, if not, it points to the first node.

Now when it comes to the number of elements inside the queue at a given time, another solution to what you suggested is to actually run through all nodes and count, but depending on the number of elements, this could be pretty slow.

for your count, just keep a reference count of how many elements have been inserted

INSERT () { 
    //code to insert element
    size++;
}

POP(){
    //code to remove element
    size--;
}

SIZE(){
   return size;
}

Next youre going to have to decide what kind of strategy youre going to use for inserting elements.

Most people just use a list. And since Queues are typically either FILO (First in Last out) or LILO (Last in last out) it might get a little tricky.

A list is just this

struct Node{
    T* Value;
    ptr Next;
 }

where you have a bunch of these in sequence which creates a list. Every insert will spawn a new node and a remove will take out the node and reattach the list.

If you're using realloc the address can change so you'll want your next, prev, head and tail to use indices.

With a fixed sized array you can use a rotary buffer where you need only keep offset and size as well as the array of values, you don't need a node struct as you keep values in order, as long as values are a constant size.

For dynamic size you can on pop swap the one being removed with the last. However this requires storing both previous and next for each node. You need to store the previous as when you swap the node at the end you need to update its location (next) in its parent. Essentially you end up with a doubly linked list.

One benefit of this is that you can get away with using one array for multiple linked lists. However this isn't good for threaded applications as you'll have global contention on a single resource.

The standard approach is to use malloc and free per node. There's not much difference in impact of that other than more memory management. You only need to store the address of next per node. You can also use pointers rather than indices. It is O(N) to destroy the queue though for many use cases that may never happen or hardly ever happen.

The performance characteristics of malloc versus realloc can vary based on a lot of factors. This is something to keep in mind.

As a rule of thumb realloc is good when it naturally replaces things like b = malloc(size + amount);memcopy(a, b, size);free(a);a = b; with a = realloc(a, size + amount); but if you're having to do strange things to get realloc to work then it might be ill conceived. Realloc should solve your problem. If your problem solves realloc then realloc is probably your problem. If you're using realloc to replace code that does the same as realloc then that's good but otherwise ask yourself if you have to bend code around to get it to work with realloc if that's really the simplest thing that could possibly work and if you're using realloc to do what realloc is meant to do. That is if you replace more with less or need less to get it working then it's probably good but if you replace less with more or need more to get it working then it's probably bad. In a nutshell, keep it simple. You'll notice here the realloc implementation means more jumping through hoops so it might be ill conceived.

Data structure examples...

Assume int is uint.

Member refers to what you're actually storing. A void pointer is used for that in this example so that it can accommodate any type. However you can change that to be a typed pointer or even the type itself if it's always the same.

Space is used for when the amount of memory allocated is potentially larger than the amount that is used to store items.

Circular static queue:

struct queue {
 void* members[SPACE];
 int offset;
 int size;
};

Members can consist of a pointer type for arbitrary types of varying lengths. You can have offset, size instead of head, tail.

Circular dynamic initial size:

struct queue {
 void* members[];
 int offset;
 int size;
 int space;
};

It's also possible to ask how much memory the pointer has instead of storing space.

Tail is offset + size - 1. You need to use modulus by space to get the real offsets.

It's possible to change the space after creation or use this as a vector. However the resize operation can be very expensive as you may have to move multiple elements making it O(N) rather than O(1) to shift and push.

Realloc vector queue:

struct queue {
 node* nodes;
 int head;
 int tail;
 int size;
};

struct node {
 void* member;
 int next;
 int prev;
};

Malloc node queue:

struct queue {
 void* head;
 node* head;
 node* tail;
};

struct node {
 void* member;
 node* next;
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM