容器的无序迭代

Unordered Iteration of a Container

本文关键字:迭代 无序      更新时间:2023-10-16

我的意图是掩盖容器的实现细节,这样客户端就不允许依赖隐式插入顺序。我试图通过某种方式改变迭代发生的顺序来强制执行这一点。

我有一个容器,我想在迭代时随机排序。下面是一些伪代码。

namespace abc
{
template<class T>
class RandomList
{
  void insert(T t);
  T erase(T t);
  iterator begin();
  iterator end();
}
}
namespace test
{
  int main()
  {
    RandomList<int> list;
    list.insert(1);
    list.insert(2);
    list.insert(3);
    for (typename RandomList::iterator iter = list.begin();
         iter != list.end(); ++iter)
    {
       std::cout << iter << std::endl;
    }
  }    
}
输出:

3, 1, 2

我的问题是,什么是最好的方法来实现随机列表。我天真的想法是只持有std::list成员并执行rand()来确定insert是执行前插入还是后插入。

还有其他想法吗?

我主要使用c++ 03,但我可以访问boost。

我不确定我完全理解你的用例,但这是一个有趣的问题。

Tony D使用std::vector的建议看起来不错。我把插入的值放在末尾,然后用一个随机元素交换:

template<typename T>
class RandomList {
  std::vector<T> list;
  RandomIndex    randomIndex;
public:
  using iterator = typename std::vector<T>::const_iterator;
  iterator begin() { return list.begin(); }
  iterator end() { return list.end(); }
  void insert(const T& t) {
    list.push_back(t);
    auto i = randomIndex(list.size());
    using std::swap;
    swap(list[i], list.back());
  }
};

其中RandomIndex是获取随机索引的(非模板化的)辅助函函数:

class RandomIndex {
  std::mt19937 eng;
public:
  RandomIndex() : eng(std::random_device{}()) {}
  size_t operator()(size_t size) {
    auto dist = std::uniform_int_distribution<size_t>{0, size - 1};
    return dist(eng);    
  }
};

我不确定随机性的质量,但它应该足以确保客户端不能对元素的顺序做出任何假设。

我不确定我是否理解了你问题的各个方面。我认为什么是好的解决方案很大程度上取决于应用和T在实践中的类型。无论如何,我建议您更改RandomList的接口(见下文)。

首先,您在内部使用std::list并在前面或后面随机插入的想法似乎是一个可能的解决方案。

我不认为这是一个好主意,如果你的目标是广泛的应用程序使用std::vector。原因是std::vector实现通常在内部做很多额外的事情,这可能会损害容器类型的性能/内存需求。通常std::vector使用比vector的实际大小更大的内存缓冲区,以避免后续调用插入操作(如push_back)时分配内存。因此,插入的性能在随后的调用中可能会发生变化(突然又要增加向量缓冲区……),并且容器可能会使用比实际需要更多的内存。同样,这可能不是问题,这取决于您的应用程序和T实际是什么。但是作为RandomList接口的用户,我会听到它在内部使用std::vector而感到烦恼…

所以,如果你只是要求"另一个想法",也希望允许多次插入相同的值到容器中-这里有一个:本质上,使RandomList成为std::map包装器。随机化应该通过适当地使用map键来实现。要迭代容器,只需使用std::map的迭代器。这实际上为您提供了对(键,值)对的引用。您可以使用std::map键来实现可能感兴趣的其他功能。首先,您可以通过引入typedef RandomList::handle来隐藏键类型——从而隐藏实现的细节。我建议方法insert实际上返回一个RandomList::handle。通过这种方式,您可以通过在接口中添加方法access来允许用户直接访问特定的容器值。accessRandomList::handle作为参数,并返回对相应映射值的引用。如果以这种方式修改接口,则RandomList迭代器(即映射迭代器)引用RandomList::handleT对是一致的。这是否有用很大程度上取决于T将是什么。

erase应将RandomList::handle作为参数。再次说明:如果不欢迎多次插入,那么基于std::map实现的想法是有问题的,或者至少应该以不同的方式进行处理。

这种方法允许对"随机化实现"进行简洁的控制。例如,如果您使用std::list在前面或后面随机插入,则随机化的实现与您的内部存储/容器实现密切相关。基于std::map的实现可以将随机化的细节与其他实现分离开来,因为随机化完全由地图键选择控制。例如,如果您使用int作为键,则可以在内部保留一个计数器,该计数器在后续插入时递增。计数器的当前值用作下一次映射插入的键。由于这将导致完全不平衡的树结构,因此RandomList在迭代时的行为与普通std::list一样。我建议以这样一种方式选择新的键值,即树是完美平衡的。通过这种方式,直接访问特性是最有效的实现(因为搜索操作很快),并且通过begin()/end()直接迭代RandomList/std::map应该会产生足够的"随机"结果。

最后,关于接口:我建议您对insert操作使用完美转发,而不是直接复制。通过这种方式,在向std::map插入元素时可以避免不必要的复制操作(当然std::list也是如此),并且还允许移动操作。如果您不想使用完全转发,至少将T更改为const T&,因为将对象插入std::list/std::map容器需要另一个复制操作。

我会避免随机插入,随机迭代。大容器的想法是正确的,将很难从视觉上看到,并且很难理解插入问题