【发布时间】:2016-09-10 03:31:44
【问题描述】:
我想使用动态分配的数组来实现队列。这提出了一些我不确定如何处理的问题。如何检查队列是否为空?如何一次跟踪队列中有多少元素?
对于第二个问题,我想我可以创建一个变量来跟踪队列中的元素数量,该数量在我使用realloc() 时随时更新。不过,我欢迎其他建议。
如果您还有其他需要考虑的事项,请提出。
【问题讨论】:
-
那么为什么是数组而不是列表呢?
我想使用动态分配的数组来实现队列。这提出了一些我不确定如何处理的问题。如何检查队列是否为空?如何一次跟踪队列中有多少元素?
对于第二个问题,我想我可以创建一个变量来跟踪队列中的元素数量,该数量在我使用realloc() 时随时更新。不过,我欢迎其他建议。
如果您还有其他需要考虑的事项,请提出。
【问题讨论】:
这是一个相当简单的基于数组的 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;
}
正如所写,这是一个“循环”队列; head 和 tail 指针将在项目被推送和从队列中弹出时环绕。
与任何方法一样,这有优点也有缺点。它很简单,并且避免了过多的内存管理(只是分配后备存储)。仅更新count 比尝试从head 和tail 计算更简单。
扩展后备存储并不是那么简单;如果你的尾指针已经环绕,你将不得不在head 之后移动所有内容:
Before:
+---+---+---+---+---+
| x | x | x | x | x |
+---+---+---+---+---+
^ ^
| |
| +--- head
+------- tail
After:
+---+---+---+---+---+---+---+---+---+---+
| x | x | x | | | | | | x | x |
+---+---+---+---+---+---+---+---+---+---+
^ ^
| |
| +--- head
+------- tail
此外,如果您想要比简单的 FIFO 更复杂的东西,您可能希望使用不同的数据结构作为后备存储。
【讨论】:
通常,您会保留一个指向队列“头”的指针变量。当该指针为空时,链表为空,如果不为空,则指向第一个节点。
现在,当涉及到在给定时间队列中的元素数量时,您建议的另一种解决方案是实际运行所有节点并进行计数,但是根据元素的数量,这可能会很慢。
【讨论】:
如果您使用 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;
};
【讨论】:
为了您的计数,只需保留已插入元素的引用计数
INSERT () {
//code to insert element
size++;
}
POP(){
//code to remove element
size--;
}
SIZE(){
return size;
}
接下来,您必须决定要使用哪种策略来插入元素。
大多数人只是使用列表。而且由于队列通常是 FILO(先进后出)或 LILO(后进后出),因此可能会有些棘手。
列表就是这样
struct Node{
T* Value;
ptr Next;
}
你有一堆按顺序创建的列表。每个插入都会产生一个新节点,而删除会取出节点并重新附加列表。
【讨论】: