当前位置: 首页 > 知识库问答 >
问题:

使用Streams API对Collection中的n个随机不同元素执行操作

涂羽
2023-03-14

我正在尝试使用 Java 8 中的 Streams API 从集合中检索 n 个唯一的随机元素以进行进一步处理,但是,没有太多运气。

更准确地说,我想要这样的东西:

Set<Integer> subList = new HashSet<>();
Queue<Integer> collection = new PriorityQueue<>();
collection.addAll(Arrays.asList(1,2,3,4,5,6,7,8,9));
Random random = new Random();
int n = 4;
while (subList.size() < n) {
  subList.add(collection.get(random.nextInt()));
}
sublist.forEach(v -> v.doSomethingFancy());

我想尽可能高效地做这件事。

这可以做到吗?

编辑:我的第二次尝试——虽然不是我的目标:

List<Integer> sublist = new ArrayList<>(collection);
Collections.shuffle(sublist);
sublist.stream().limit(n).forEach(v -> v.doSomethingFancy());

编辑:第三次尝试(受Holger启发),如果coll.size()很大而n很小的话会去掉很多shuffle的开销:

int n = // unique element count
List<Integer> sublist = new ArrayList<>(collection);   
Random r = new Random();
for(int i = 0; i < n; i++)
    Collections.swap(sublist, i, i + r.nextInt(source.size() - i));
sublist.stream().limit(n).forEach(v -> v.doSomethingFancy());

共有3个答案

益银龙
2023-03-14
匿名用户

作为对公认答案的< code >洗牌方法的补充:

如果您只想从一个大列表中选择几个项目,并希望避免杂乱整个列表的开销,您可以按如下方式解决该任务:

public static <T> List<T> getRandom(List<T> source, int num) {
    Random r=new Random();
    for(int i=0; i<num; i++)
        Collections.swap(source, i, i+r.nextInt(source.size()-i));
    return source.subList(0, num);
}

它所做的与< code>shuffle非常相似,但它将它的操作减少到只有< code>num个随机元素,而不是< code>source.size()个随机元素…

邢法
2023-03-14

您总是可以创建一个“哑巴”比较器,它将随机比较列表中的元素。调用

大概是这样的:

static List<Integer> nDistinct(Collection<Integer> queue, int n) {
    final Random rand = new Random();
    return queue.stream()
                .distinct()
                .sorted(Comparator.comparingInt(a -> rand.nextInt()))
                .limit(n)
                .collect(Collectors.toList());
}

但是,我不确定将元素放入列表中,对其进行洗牌并返回子列表会更有效。

static List<Integer> nDistinct(Collection<Integer> queue, int n) {
    List<Integer> list = new ArrayList<>(queue);
    Collections.shuffle(list);
    return list.subList(0, n);
}

哦,从语义上讲,返回 Set 而不是 List 可能更好,因为这些元素是不同的。这些方法也被设计为采用整数s,但将它们设计为泛型并不困难。:)

请注意,Stream API看起来像一个工具箱,我们可以用它来做任何事情,然而情况并非总是如此。如您所见,第二种方法可读性更好(IMO),可能更高效,并且没有太多代码(甚至更少!).

彭浩穰
2023-03-14

正如fge在评论中和ZouZou在另一个答案中建议的那样,洗牌方法效果相当好。以下是洗牌方法的一般版本:

static <E> List<E> shuffleSelectN(Collection<? extends E> coll, int n) {
    assert n <= coll.size();
    List<E> list = new ArrayList<>(coll);
    Collections.shuffle(list);
    return list.subList(0, n);
}

我将注意到,使用< code>subList比获取一个流然后调用< code>limit(n)更好,正如其他一些答案中所示,因为得到的流有一个已知的大小,可以更有效地分割。

洗牌方法有几个缺点。它需要复制出所有元素,然后需要洗牌所有元素。如果元素的总数很大并且要选择的元素的数量很小,那么这可能非常昂贵。

OP和其他几个答案建议的方法是随机选择元素,同时拒绝重复的元素,直到选择了所需数量的唯一元素。如果要选择的元素数量相对于总数来说很少,这种方法会很有效,但是随着要选择的元素数量的增加,这种方法会变慢,因为选择重复元素的可能性也会增加。

如果有一种方法可以在输入元素的空间上进行一次传递,并准确地选择所需的数字,并且随机做出一致的选择,那不是很好吗?事实证明是有的,而且像往常一样,答案可以在Knuth中找到。参见TAOCP第2卷,第3.4.2节,随机采样和洗牌,算法S。

简单地说,算法是访问每个元素,并根据访问的元素数量和选择的元素数量决定是否选择它。在Knuth的表示法中,假设你有N个元素,你想随机选择其中的N个。下一个元素应该有概率地选择

(n-m)/(N-t)

其中t是迄今访问的元素数量,m是迄今选择的元素数量。

这是否会给出所选元素的均匀分布一点都不明显,但显然是这样的。这个证明留给读者作为练习;参见本节的练习3。

给定此算法,通过在“常规”Java 中循环访问集合并根据随机测试添加到结果列表中,实现它非常简单。OP询问了使用流的问题,所以这里有一个镜头。

算法S显然不适合Java流操作。它完全是按顺序描述的,关于是否选择当前元素的决定取决于从所有先前决定中得出的随机决定加状态。这可能会让它看起来有内在的顺序性,但我以前就错了。我只想说,如何使这个算法并行运行还不是很明显。

不过,有一种方法可以使此算法适应流。我们需要的是一个有状态的谓词。此谓词将根据当前状态确定的概率返回随机结果,并且状态将根据此随机结果进行更新 - 是的,突变。这似乎很难并行运行,但至少很容易使线程安全,以防它从并行流运行:只需使其同步即可。但是,如果流是并行的,它将降级为按顺序运行。

实现非常简单。Knuth的描述使用0到1之间的随机数,但是Java Random类让我们在半开区间内选择一个随机整数。因此,我们所需要做的就是保存计数器,记录还有多少元素需要访问,还有多少元素需要选择,瞧:

/**
 * A stateful predicate that, given a total number
 * of items and the number to choose, will return 'true'
 * the chosen number of times distributed randomly
 * across the total number of calls to its test() method.
 */
static class Selector implements Predicate<Object> {
    int total;  // total number items remaining
    int remain; // number of items remaining to select
    Random random = new Random();

    Selector(int total, int remain) {
        this.total = total;
        this.remain = remain;
    }

    @Override
    public synchronized boolean test(Object o) {
        assert total > 0;
        if (random.nextInt(total--) < remain) {
            remain--;
            return true;
        } else {
            return false;
        }
    }
}

现在我们有了谓词,很容易在流中使用:

static <E> List<E> randomSelectN(Collection<? extends E> coll, int n) {
    assert n <= coll.size();
    return coll.stream()
        .filter(new Selector(coll.size(), n))
        .collect(toList());
}

在Knuth的同一部分中还提到了一种替代方法,建议以n / N的恒定概率随机选择一个元素。如果您不需要恰好选择n个元素,这很有用。它平均会选择n个元素,但是当然会有一些变化。如果这是可以接受的,那么有状态谓词就变得简单多了。我们可以简单地创建随机状态并从局部变量中捕获它,而不是编写整个类:

/**
 * Returns a predicate that evaluates to true with a probability
 * of toChoose/total.
 */
static Predicate<Object> randomPredicate(int total, int toChoose) {
    Random random = new Random();
    return obj -> random.nextInt(total) < toChoose;
}

要使用此功能,请将上面的流管道中的filter行替换为

        .filter(randomPredicate(coll.size(), n))

最后,为了便于比较,这里有一个使用传统Java编写的选择算法的实现,即使用for循环并添加到集合中:

static <E> List<E> conventionalSelectN(Collection<? extends E> coll, int remain) {
    assert remain <= coll.size();
    int total = coll.size();
    List<E> result = new ArrayList<>(remain);
    Random random = new Random();

    for (E e : coll) {
        if (random.nextInt(total--) < remain) {
            remain--;
            result.add(e);
        }
    }            

    return result;
}

这很简单,也没有什么问题。它比流方法更简单、更独立。尽管如此,流方法展示了一些有趣的技术,可能在其他环境中有用。

参考:

Knuth, Donald E.计算机编程的艺术:第2卷,半数值算法,第2版。版权所有1981,1969 Addison-Wesley。

 类似资料:
  • 我正在尝试使用 Java 8 中的 Streams API 从集合中检索 n 个唯一的随机元素以进行进一步处理,但是,没有太多运气。 更准确地说,我想要这样的东西: 我想尽可能高效地做这件事。 这可以做到吗? 编辑:我的第二次尝试——虽然不是我的目标: 编辑:第三次尝试(受Holger启发),如果coll.size()很大而n很小的话会去掉很多shuffle的开销:

  • 我知道有人问过类似的问题,比如 从包含n个元素的向量中随机选择m个元素 从未知长度的序列中随机选择N个项目 但我越看越困惑。 从N个元素中均匀随机地选择M个元素 所以我需要从N个元素中选择M个元素。我还需要使被选中的概率均匀分布于每个元素: 我的直觉是 随机选择一个元素 把它拿出来 对其余元素重复此过程 我猜这个解决方案错了?所选元素的概率为,<代码>1/(N-M),而不是,对吗? 一个可能的正确

  • 从 array 中获取 n 个唯一键随机元素。 使用Fisher-Yates算法 对数组进行打乱。 使用 Array.slice() 获取第一个 n 元素。 省略第二个参数,n 从数组中随机取得 1 个元素。 const sampleSize = ([...arr], n = 1) => { let m = arr.length; while (m) { const i = Mat

  • 问题内容: 我正在研究“如何从javascript中的数组随机访问元素”。我发现了许多与此有关的链接。 问题: 但是在这种情况下,我们只能从数组中选择一项,如果我们想要多个元素,那么我们将如何实现这一点,所以请仅从该语句中获取一个数组中的多个元素。 问题答案: 尝试以下无损快速功能:

  • srandmember key 同spop,随机取set中的一个元素,但是不删除元素

  • 我试图在元素上执行拖放操作,但它没有发生。 这是我正在处理的页面的片段。在这里,我试图将磁贴“时间”拖动并放置在磁贴“批准”的位置。截图 这是我正在使用的代码。 代码 超文本标记语言 源元素 目标元素 如果你需要更多细节,请告诉我。