简体   繁体   English

如何使用动态分配的数组实现队列?

[英]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() . 对于第二个问题,我认为我可以创建一个变量来跟踪队列中的元素数量,该变量在我使用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: 这是一个非常简单的基于数组的FIFO队列:

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. 当项目从队列中被推送和弹出时, headtail指针将环绕。

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 . 仅仅更新count要比headtail计算要简单。

Extending the backing store isn't quite so straightforward; 扩展后备存储并不是那么简单。 if your tail pointer has wrapped around, you'll have to shift everything after head : 如果您的尾巴指针已缠绕,则必须在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. 另外,如果您想要比简单的FIFO更复杂的内容,则可能需要使用其他数据结构作为后备存储。

Typically, you keep a pointer variable to the 'Head' of your queue. 通常,您将指针变量保留到队列的“ Head”。 When this pointer is null, the list is empty, if not, it points to the first node. 当此指针为null时,列表为空,否则为空,它指向第一个节点。

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. 而且由于队列通常是FILO(后进先出)或LILO(后进先出),因此可能会有些棘手。

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. 如果您使用的是realloc,则地址可能会更改,因此您希望下一个,上一个,首尾使用索引。

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. 标准方法是对每个节点使用malloc和free。 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. 尽管对于很多可能永远不会发生或几乎不会发生的用例,销毁队列是O(N)。

The performance characteristics of malloc versus realloc can vary based on a lot of factors. malloc与realloc的性能特征可能会因许多因素而异。 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; 根据经验,重新分配自然可以替换b = malloc(size + amount);memcopy(a, b, size);free(a);a = b; with a = realloc(a, size + amount); 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. 如果您的问题解决了realloc,那么realloc可能就是您的问题。 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. 如果您使用realloc替换与realloc相同的代码,那很好,但否则请问一下自己,如果那确实是最简单的可行方法,并且是否需要修改代码以使其与realloc一起使用,并且重新使用realloc来完成realloc的工作。 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. 您会在这里注意到realloc实现意味着更多的跳跃,因此可能构思不当。

Data structure examples... 数据结构示例...

Assume int is uint. 假设int是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. 尾部为偏移量+大小-1.您需要按空间使用模数来获取实际偏移量。

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. 但是,调整大小操作可能会非常昂贵,因为您可能必须移动多个元素以使其移动(而不是O(1)才能移动和推动)。

Realloc vector queue: 重新分配向量队列:

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

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

Malloc node queue: Malloc节点队列:

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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