我应该使用哪个容器进行随机访问,廉价的添加和删除(没有取消/分配),具有已知的最大大小

Which container should I use for random access, cheap addition and removal (without de/allocation), with a known maximum size?

本文关键字:取消 删除 分配 添加 我应该 访问 随机      更新时间:2023-10-16

我需要更轻的容器,它必须存储到 128 个无符号的 int。它必须快速添加、编辑和删除访问它的每个元素,而无需每次都分配新内存(我已经知道它最多会是 128 个)。

如:

add int 40 at index 4 (1/128 item used)
add int 36 at index 90 (2/128 item used)
edit to value 42 the element at index 4
add int 36 at index 54 (3/128 item used)
remove element with index 90 (2/128 item used)
remove element with index 4 (1/128 item used)

。等等。因此,每次我可以迭代时,仅trought添加到容器中的实际元素数量,而不是全部,并检查是否为NULL。

在此过程中,正如我所说,它一定不能分配/重新分配新内存,因为我使用的是管理"音频"数据的应用程序,这意味着每次触摸内存时都会出现故障。

哪个容器是正确的候选者?这听起来像一个"索引"队列。

据我了解,您有两个操作

在单元格索引
处插入/替换元素值删除单元格索引
处的元素

和一个谓词

单元格索引当前是否被占用?

这是一个数组和一个位图。 插入/替换时,将值粘贴到数组单元格中并设置位图位。 删除时,将清除位图位。 询问时,查询位图位。

你可以只使用 std::vector<int> 并执行vector.reserve(128);来防止向量分配内存。但是,这不允许您跟踪特定索引。

如果您需要跟踪"索引",您可以使用std::vector<std::pair<int, int>>。不过,这不允许随机访问。

如果你只需要便宜的设置和擦除值,只需使用数组。你可以通过在另一个数组(或位图)中标记它们来跟踪使用了哪些单元格。或者仅将一个值(例如 0 或 -1)定义为"未使用"的值。

当然,如果您需要遍历所有使用的单元格,则需要扫描整个数组。但这是您需要做出的权衡:要么在添加和擦除期间执行更多工作,要么在搜索期间执行更多工作。(请注意,vector<>中间的.insert()将移动数据。

无论如何,128 个元素是如此之少,以至于扫描整个数组的工作可以忽略不计。坦率地说,我认为任何比vector更复杂的事情都是矫枉过正。:)

大约:

unsigned data[128] = {0}; // initialize
unsigned used[128] = {0};
data[index] = newvalue; used[index] = 1; // set value
data[index] = used[index] = 0;           // unset value
// check if a cell is used and do something 
if (used[index]) { do something } else { do something else } 

我建议使用串联向量,一个用于保存活动索引,另一个用于保存数据:

class Container
{
  std::vector<size_t> indices;
  std::vector<int> data;
  size_t index_worldToData(size_t worldIndex) const
  {
    auto it = std::lower_bound(begin(indices), end(indices), worldIndex);
    return it - begin(indices);
  }
public:
  Container()
  {
    indices.reserve(128);
    data.reserve(128);
  }
  int& operator[] (size_t worldIndex)
  {
    return data[index_worldToData(worldIndex)];
  }
  void addElement(size_t worldIndex, int element)
  {
    auto dataIndex = index_worldToData(worldIndex);
    indices.insert(it, worldIndex);
    data.insert(begin(data) + dataIndex, element);
  }
  void removeElement(size_t worldIndex)
  {
    auto dataIndex = index_worldToData(worldIndex);
    indices.erase(begin(indices) + dataIndex);
    data.erase(begin(indices) + dataIndex);
  }
  class iterator
  {
    Container *cnt;
    size_t dataIndex;
  public:
    int& operator* () const { return cnt.data[dataIndex]; }
    iterator& operator++ () { ++dataIndex; }
  };
  iterator begin() { return iterator{ this, 0 }; }
  iterator end() { return iterator{ this, indices.size() }; }
};

(免责声明:编译器未触及代码,省略前提条件检查)

这个具有对数时间元素访问,线性时间插入和删除,并允许迭代非空元素。

您可以使用双向链表和节点指针数组。预分配 128 个列表节点并将它们保留在 freelist 上。创建一个空itemlist。分配一个由 128 个节点指针组成的数组,称为 items

  • 插入 i :从 freelist 弹出头节点,将其添加到 itemlist ,将items[i]设置为指向它。
  • 若要访问/更改值,请使用 items[i]->value
  • 要在 i 处删除,删除 items[i] 指向的节点,将其重新插入"自由列表"
  • 要迭代,只需走itemlist

一切都是 O(1),除了迭代,它是 O(Nactive_items)。 唯一需要注意的是,迭代不是按索引顺序排列的。

自由列表可以是单链接的,甚至可以是节点数组,因为您所需要的只是弹出和推送。

class Container {
  private:
    set<size_t> indices;
    unsigned int buffer[128];
  public:
    void set_elem(const size_t index, const unsigned int element) {
      buffer[index] = element;
      indices.insert(index);
    }
    // and so on -- iterate over the indices if necessary
};

您可以使用多种方法,我将按花费的努力顺序引用它们。


最实惠的解决方案是使用Boost非标准容器,特别感兴趣的是flat_map。从本质上讲,flat_map通过动态数组提供的存储提供map接口。

您可以在开始时调用其reserve成员,以避免之后的内存分配。


稍微复杂的解决方案是编写自己的内存分配器。

分配器的

接口相对容易处理,因此对分配器的编码非常简单。创建一个永远不会释放任何元素的池分配器,预热它(分配 128 个元素),您就可以开始使用了:它可以插入任何集合以使其无需内存分配。

这里特别感兴趣的当然是std::map.


最后,是自己动手的道路。很明显,涉及更多:标准容器支持的操作数量只是......巨大

不过,如果您有时间或只能接受这些操作的子集,那么这条路有一个不可否认的优势:您可以根据您的需求专门定制容器。

这里特别令人感兴趣的是拥有 128 个元素std::vector<boost::optional<int>>的想法......除了由于这种表示在空间上效率很低,我们使用面向数据的设计来代替它成为两个向量:std::vector<int>std::vector<bool> ,这更紧凑,甚至......

struct Container {
    size_t const Size = 128;
    int array[Size];
    std::bitset<Size> marker;
}

既紧凑无分配。

现在,迭代需要迭代当前元素的位集,乍一看似乎很浪费,但所述位集只有 16 个字节长,所以轻而易举!(因为在这样的规模下,内存局部性胜过大O复杂性)

为什么不使用std::map<int, int>,它提供随机访问并且是稀疏的。

如果

vector(预先保留)不够方便,请查看 Boost.Container 以获取各种"扁平"索引集合。这将把所有内容存储在一个向量中,不需要内存操作,但在上面添加一个层,使其成为一个集合或映射,可通过哪些元素存在来索引,并能够分辨哪些元素不存在。