【问题标题】:How to efficiently select a random element from a std::set如何有效地从 std::set 中选择随机元素
【发布时间】:2012-08-30 14:08:06
【问题描述】:

如何有效地从std::set 中选择随机元素?

std::set::iterator 不是随机访问迭代器。所以我不能像 std::dequestd::vector 那样直接索引随机选择的元素

可以获取从 std::set::begin() 返回的迭代器,并在 [0,std::set::size()) 范围内随机增加它的次数,但这似乎做了很多不必要的工作。对于接近集合大小的“索引”,我最终会遍历内部树结构的整个前半部分,即使已经知道不会在那里找到该元素。

有更好的方法吗?

以效率的名义,我愿意将“随机”定义为 更少随机,而不是我可能用来在向量中选择随机索引的任何方法。称之为“合理随机”。

编辑...

下面有许多有见地的答案。

简短的版本是,即使你可以在 log(n) 时间内找到一个 特定 元素,你也找不到一个 任意 元素在那个时候通过std::set 接口。

【问题讨论】:

  • 将集合的所有迭代器都扔到std::vector 中并从中随机选择?
  • 问:如果你想“访问一个随机值”,那么你一开始就不应该使用集合,对吗?我喜欢 Xeo 的建议:如果您想以不同方式访问元素,请使用不同的访问器(例如“将所有迭代器放入 std::vector”)。
  • @Xeo 这很聪明。你应该发布那个。
  • @Xeo 这会比在列表中递增直到达到随机索引更快吗?
  • 如果随机性不是很重要,只要使用集合中的第一个元素即可。

标签: c++ algorithm stl


【解决方案1】:

导致随机树遍历的find(或lower_bound)的谓词怎么样?您必须告诉它集合的大小,以便它可以估计树的高度,并且有时会在叶节点之前终止。

编辑:我意识到这个问题是std::lower_bound 采用谓词但没有任何类似树的行为(在内部它使用std::advance,这在另一个答案的 cmets 中进行了讨论)。 std::set<>::lower_bound 使用集合的谓词,它不能是随机的,仍然具有类似集合的行为。

啊哈,您不能使用不同的谓词,但可以使用可变谓词。由于std::set 通过值传递谓词对象,您必须使用predicate & 作为谓词,以便您可以访问并修改它(将其设置为“随机化”模式)。

这是一个准工作示例。不幸的是,我无法将我的大脑包裹在正确的随机谓词上,所以我的随机性不是很好,但我相信有人可以解决这个问题:

#include <iostream>
#include <set>
#include <stdlib.h>
#include <time.h>

using namespace std;

template <typename T>
struct RandomPredicate {
    RandomPredicate() : size(0), randomize(false) { }
    bool operator () (const T& a, const T& b) {
        if (!randomize)
            return a < b;

        int r = rand();
        if (size == 0)
            return false;
        else if (r % size == 0) {
            size = 0;
            return false;
        } else {
            size /= 2;
            return r & 1;
        }
    }

    size_t size;
    bool randomize;
};

int main()
{
    srand(time(0));

    RandomPredicate<int> pred;
    set<int, RandomPredicate<int> & > s(pred);
    for (int i = 0; i < 100; ++i)
        s.insert(i);

    pred.randomize = true;
    for (int i = 0; i < 100; ++i) {
        pred.size = s.size();
        set<int, RandomPredicate<int> >::iterator it = s.lower_bound(0);
        cout << *it << endl;
    }
}

我的半生不熟的随机性测试是./demo | sort -u | wc -l,看看我得到了多少个唯一整数。使用更大的样本集尝试./demo | sort | uniq -c | sort -n 寻找不需要的模式。

【讨论】:

  • 您不能在 find/lower_bound 调用中更改集合/映射的排序谓词(除非您使用 std::find,它是线性的并且在 OP 中要求避免使用)。
  • @MarkB 啊,当您发表评论时,我在编辑中添加了相同的内容。我希望有人会知道类似的替代方案...
  • 我想知道你是否可以欺骗一个集合,让它表现得像一个具有不同谓词的集合。
  • 这非常聪明——我唯一建议的是在进行测试之前将随机数的低八位移开(换句话说,不要使用位 0 作为您的开/关闭随机检查)。
【解决方案2】:

您可以使用std::advance 方法:

set <int> myset;
//insert some elements into myset
int rnd = rand() % myset.size();
set <int> :: const_iterator it(myset.begin());
advance(it, rnd);
//now 'it' points to your random element

另一种方法,可能不太随机:

int mini = *myset().begin(), maxi = *myset().rbegin();
int rnd = rand() % (maxi - mini + 1) + mini;
int rndresult = *myset.lower_bound(rnd);

【讨论】:

  • std::advance 具有与使用增量运算符 rnd 次相同的性能特征,这是 OP 试图避免的。
  • @IronMensan 是的。不幸的是,我认为除了构建自己的平衡二叉树然后随机遍历它之外,没有人可以避免这样做。
  • @IronMensan 我又试了一次,如果您有兴趣,请查看我的新答案以供参考。
【解决方案3】:

如果您可以访问底层红黑树(假设存在一个),那么您可以访问 O(log n) 中的随机节点,选择 L/R 作为 @ 的连续位987654321@位随机整数。但是,您不能,因为标准没有公开底层数据结构。

Xeo 在向量中放置迭代器的解决方案是 O(n) 时间和空间来设置,但总体上摊销常数。这与 std::next 相比是有利的,后者是 O(n) 时间。

【讨论】:

    【解决方案4】:

    改用boost::container::flat_set

    boost::container::flat_set<int> set;
    // ...
    auto it = set.begin() + rand() % set.size();
    

    虽然插入和删除变成了 O(N),但我不知道这是否是个问题。您仍然需要进行 O(log N) 次查找,并且容器是连续的这一事实带来的整体改进通常超过了 O(log N) 次插入和删除的损失。

    【讨论】:

      【解决方案5】:

      如果该集合不经常更新或者您不需要经常运行此算法,请将数据的镜像副本保存在 vector 中(或者只需将集合复制到需要的向量)并随机从中选择。

      另一种方法,如评论中所见,是在集合中保留一个迭代器向量(它们仅在删除 sets 的元素时失效)并随机选择一个迭代器。

      最后,如果您不需要基于树的集合,您可以使用 vectordeque 作为底层容器,并在需要时进行排序/唯一化。

      【讨论】:

        【解决方案6】:

        您可以通过维护一个正常的值数组来做到这一点;当你插入到集合中时,你将元素附加到数组的末尾(O(1)),然后当你想生成一个随机数时,你可以从 中的数组中获取它>O(1) 也是。

        当您想从数组中删除元素时,问题就出现了。最简单的方法会采用 O(n),这可能足以满足您的需求。但是,可以使用以下方法将其改进为 O(log n)

        保留,对于数组中的每个索引iprfx[i],它表示数组中0...i范围内未删除元素的数量。保留一个分段树,您可以在其中保留每个范围中包含的最大 prfx[i]

        每次删除都可以在 O(log n) 内完成更新段树。现在,当您想要访问随机数时,您可以查询段树以找到该数字的“真实”索引(通过查找最大值prfx 等于随机索引的最早范围)。这使得随机数生成的复杂度O(log n)

        【讨论】:

          猜你喜欢
          • 2011-03-04
          • 2010-09-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-08
          • 2011-02-01
          相关资源
          最近更新 更多