【问题标题】:How do I implement a queue with a dynamically allocated array?如何使用动态分配的数组实现队列?
【发布时间】:2016-09-10 03:31:44
【问题描述】:

我想使用动态分配的数组来实现队列。这提出了一些我不确定如何处理的问题。如何检查队列是否为空?如何一次跟踪队列中有多少元素?

对于第二个问题,我想我可以创建一个变量来跟踪队列中的元素数量,该数量在我使用realloc() 时随时更新。不过,我欢迎其他建议。

如果您还有其他需要考虑的事项,请提出。

【问题讨论】:

  • 那么为什么是数组而不是列表呢?

标签: c queue dma


【解决方案1】:

这是一个相当简单的基于数组的 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;
}

要推送项目,请执行以下操作:

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;
}

流行音乐是相似的

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;
}

正如所写,这是一个“循环”队列; headtail 指针将在项目被推送和从队列中弹出时环绕。

与任何方法一样,这有优点也有缺点。它很简单,并且避免了过多的内存管理(只是分配后备存储)。仅更新count 比尝试从headtail 计算更简单。

扩展后备存储并不是那么简单;如果你的尾指针已经环绕,你将不得不在head 之后移动所有内容:

Before:

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

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

此外,如果您想要比简单的 FIFO 更复杂的东西,您可能希望使用不同的数据结构作为后备存储。

【讨论】:

    【解决方案2】:

    通常,您会保留一个指向队列“头”的指针变量。当该指针为空时,链表为空,如果不为空,则指向第一个节点。

    现在,当涉及到在给定时间队列中的元素数量时,您建议的另一种解决方案是实际运行所有节点并进行计数,但是根据元素的数量,这可能会很慢。

    【讨论】:

      【解决方案3】:

      如果您使用 realloc,地址可以更改,因此您希望 next、prev、head 和 tail 使用索引。

      使用固定大小的数组,您可以使用旋转缓冲区,您只需要保留偏移量和大小以及值数组,您不需要节点结构,因为您可以按顺序保持值,只要值是一个恒定的大小。

      对于动态大小,您可以在弹出时将要删除的那个与最后一个交换。但是,这需要为每个节点存储上一个和下一个。您需要存储上一个,因为当您最后交换节点时,您需要在其父节点中更新其位置(下一个)。从本质上讲,您最终会得到一个双向链表。

      这样做的一个好处是您可以摆脱对多个链表使用一个数组的情况。但是,这对线程应用程序不利,因为您将在单个资源上进行全局争用。

      标准方法是对每个节点使用 malloc 和 free。除了更多的内存管理之外,它的影响没有太大差异。您只需要存储每个节点的下一个地址。您还可以使用指针而不是索引。尽管对于许多可能永远不会发生或几乎不会发生的用例,销毁队列是 O(N)。

      malloc 与 realloc 的性能特征可能因许多因素而异。这是需要牢记的。

      根据经验,realloc 很好,当它自然地将 b = malloc(size + amount);memcopy(a, b, size);free(a);a = b; 替换为 a = realloc(a, size + amount); 时,但如果你不得不做一些奇怪的事情来让 realloc 工作,那么它可能是错误的。 Realloc 应该可以解决您的问题。如果您的问题解决了 realloc 那么 realloc 可能是您的问题。如果您正在使用 realloc 来替换与 realloc 执行相同操作的代码,那很好,但是如果这真的是最简单的可能可行的方法,并且如果您“重新使用 realloc 来做 realloc 应该做的事情。也就是说,如果你更换更多 更少或需要更少才能使其工作,那么它可能是好的,但如果你用更多代替更少或需要更多才能让它工作,那么它可能很糟糕。简而言之,保持简单。你会注意到这里 realloc 实现意味着更多的跳跃,所以它可能是错误的。

      数据结构示例...

      假设 int 是 uint。

      成员是指您实际存储的内容。在此示例中使用了一个 void 指针,以便它可以容纳任何类型。但是,如果它始终相同,您可以将其更改为类型化指针,甚至类型本身。

      空间用于分配的内存量可能大于用于存储项目的量。

      循环静态队列:

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

      成员可以由一个指针类型组成,用于任意长度的类型。您可以使用偏移量、大小来代替头部、尾部。

      圆形动态初始尺寸:

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

      也可以询问指针有多少内存而不是存储空间。

      尾巴是偏移量 + 大小 - 1。您需要使用空间模数来获得真正的偏移量。

      可以在创建后更改空间或将其用作矢量。然而,调整大小操作可能非常昂贵,因为您可能必须移动多个元素,使其 O(N) 而不是 O(1) 才能移动和推送。

      Realloc 向量队列:

      struct queue {
       node* nodes;
       int head;
       int tail;
       int size;
      };
      
      struct node {
       void* member;
       int next;
       int prev;
      };
      

      Malloc 节点队列:

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

      【讨论】:

        【解决方案4】:

        为了您的计数,只需保留已插入元素的引用计数

        INSERT () { 
            //code to insert element
            size++;
        }
        
        POP(){
            //code to remove element
            size--;
        }
        
        SIZE(){
           return size;
        }
        

        接下来,您必须决定要使用哪种策略来插入元素。

        大多数人只是使用列表。而且由于队列通常是 FILO(先进后出)或 LILO(后进后出),因此可能会有些棘手。

        列表就是这样

        struct Node{
            T* Value;
            ptr Next;
         }
        

        你有一堆按顺序创建的列表。每个插入都会产生一个新节点,而删除会取出节点并重新附加列表。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-08-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-06-02
          • 2012-01-30
          相关资源
          最近更新 更多