【问题标题】:Perform operation on n random distinct elements from Collection using Streams API使用 Streams API 对 Collection 中的 n 个随机不同元素执行操作
【发布时间】:2015-04-23 11:38:59
【问题描述】:

我正在尝试使用 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());

我想尽可能高效地做到这一点。

这个可以吗?

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

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());

【问题讨论】:

  • 为什么是流媒体?如果您想要效率,请使用Collections.shuffle() 并获取子列表
  • 我实际尝试执行此操作的集合是一个队列,因此除非我先将其转换为列表,否则随机播放将不起作用。另外,我想了解有关 Stream API 的更多信息:)
  • 旁白:如果你想要一个包含连续整数的列表,你可以使用IntStream.range(0, last).boxed().collect(toList());
  • 集合/队列中是否存在相等的对象?例如(queue.addAll(Arrays.asList(1,2,3,2,1));) 如果是 - “唯一”是指“不一样”还是“不等于已选择”?
  • 是的,集合可以复制。套不能。但是,在这种情况下,distinct 意味着不会两次在集合中选择相同的索引。

标签: java collections java-8


【解决方案1】:

按照fgecommentZouZou 在另一个answer 中的建议,改组方法效果很好。这是改组方法的通用版本:

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);
}

我会注意到,使用subList 比获取流然后调用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。

【讨论】:

  • 优秀的答案,一如既往! +1 :)
  • 我个人认为依赖可变谓词不是一个好主意。不能用一个不可变的想法来完成同样的想法(我敢肯定在 Scala 中这不会那么困难,但不了解 Java 8)。
  • @habitats 是的,流方法很可能是矫枉过正。我添加了一个传统方法的示例进行比较。
  • @herman “不可变”谓词是指“无状态”还是“纯功能”?如果是这样,无论语言如何,我都不确定如何完成。精确的选择算法要求每次调用时选择的概率都不同,而这个决策会影响下一个决策的概率。对我来说,这是对可变状态的明确要求,尽管我有兴趣了解是否有办法避免这种情况。最后,我添加了一个不使用计数器的“不精确”变体,而是使用固定概率。但它仍然具有随机状态。
  • @habitats 很高兴你欣赏它。尽管 Java 8 已经推出近一年,但它仍然足够新,探索各种不同的技术是有好处的。
【解决方案2】:

您总是可以创建一个“哑”比较器,它会随机比较列表中的元素。调用 distinct() 将确保您的元素是唯一的(来自队列)。

类似这样的:

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 在语义上可能更好,因为元素是不同的。这些方法也被设计为采用Integers,但将它们设计为通用的并不困难。 :)

请注意,Stream API 看起来像是一个工具箱,我们可以将其用于所有事情,但情况并非总是如此。如您所见,第二种方法更具可读性(IMO),可能更高效,并且代码不多(甚至更少!)。

【讨论】:

  • 有趣的方法,而且有点老套。但是,从外观上看,由于排序(无论正确性如何),它会像我的第二次尝试一样运行 O(n*lg n) 与 O(n) 一样。 Shuffle 以线性时间运行。
  • @habitats 是的,我也这么认为。但是您问如何使用 Streams 来做到这一点:-)
  • 您的最后一次编辑与我的第二次尝试类似,但“forEach”除外,这是其背后的全部想法。我想对给定集合中元素的随机子集执行“forEach”:)。但是,是的,我不确定哪个更有效,stream().limit(n) 或 sublist(0,n),但因为我想要一个“forEach”,所以我倾向于极限!
  • @habitats 是的;只是要指出它可能比 hacky 流方法更具可读性(没什么花哨的!)。但是你想用这个forEach 做什么?也许还有另一种方法可以解决这个问题。
  • 我想对 n 个随机选择的对象执行一个方法,这将改变对象的内部状态。
【解决方案3】:

作为已接受答案的shuffle 方法的附录:

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

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);
}

它的作用与shuffle 的作用非常相似,但它的作用是只包含num 随机元素而不是source.size() 随机元素……

【讨论】:

  • 是的,如果源列表包含唯一元素,则返回的列表也将具有唯一元素。如前所述,它的工作方式类似于shuffle,只是进行重新排序;唯一的区别是这个解决方案不会打乱整个列表,所以被排除的部分可能包含(可能很多)未触及的元素仍然在它们的原始位置。
  • 啊,是的,对,我只是花了一些时间来了解 sn-p。好点子!对于我的用例 n 实际上变化很大并且大部分时间都非常小,而集合的长度非常大,所以这实际上是完美的(除了不是流:p)!
【解决方案4】:

你可以使用limit来解决你的问题。

http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#limit-long-

Collections.shuffle(collection); 

int howManyDoYouWant = 10;
List<Integer> smallerCollection = collection
    .stream()
    .limit(howManyDoYouWant)
    .collect(Collectors.toList());

【讨论】:

  • 问题是我没有洗牌的列表流,我有一个 PriorityQueue。
  • 回复您的编辑;为简单起见,使用 int 只是一种任意选择。
  • 我不能使用 shuffle 因为底层的 Collection 不是一个列表,它是一个队列。这就是我想使用流的原因。
  • 我会给你一个使用限制的投票——我不知道!
【解决方案5】:
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int n = 4;
Random random = ThreadLocalRandom.current();

random.ints(0, collection.size())
        .distinct()
        .limit(n)
        .mapToObj(collection::get)
        .forEach(System.out::println);

这当然会产生中间索引集的开销,如果 n > collection.size(),它将永远挂起。

如果你想避免任何非常量的开销,你必须创建一个有状态的Predicate

【讨论】:

  • 我意识到我试图通过使用整数和 List 来简化问题,而不是使用任意对象和队列的实际实现,这让一些人感到困惑。我将再次更新问题。
【解决方案6】:

应该清楚的是,流式传输集合不是您想要的。

使用generate()limit 方法:

Stream.generate(() -> list.get(new Random().nextInt(list.size())).limit(3).forEach(...);

【讨论】:

  • 但是你不能确定从列表中得到不同的元素。正如 OP 所说 “我正在尝试检索 n 个唯一的随机元素”
【解决方案7】:

如果您想轻松处理整个 Stream,您可以使用 Collectors.collectingAndThen() 创建自己的 Collector:

public static <T> Collector<T, ?, Stream<T>> toEagerShuffledStream() {
    return Collectors.collectingAndThen(
      toList(),
      list -> {
          Collections.shuffle(list);
          return list.stream();
      });
}

但是,如果您想limit() 生成的 Stream,这将不会很好。为了克服这个问题,可以创建一个自定义 Spliterator:

package com.pivovarit.stream;

import java.util.List;
import java.util.Random;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class ImprovedRandomSpliterator<T> implements Spliterator<T> {

    private final Random random;
    private final T[] source;
    private int size;

    ImprovedRandomSpliterator(List<T> source, Supplier<? extends Random> random) {
        if (source.isEmpty()) {
            throw new IllegalArgumentException("RandomSpliterator can't be initialized with an empty collection");
        }
        this.source = (T[]) source.toArray();
        this.random = random.get();
        this.size = this.source.length;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        int nextIdx = random.nextInt(size);
        int lastIdx = size - 1;

        action.accept(source[nextIdx]);
        source[nextIdx] = source[lastIdx];
        source[lastIdx] = null; // let object be GCed
        return --size > 0;
    }

    @Override
    public Spliterator<T> trySplit() {
        return null;
    }

    @Override
    public long estimateSize() {
        return source.length;
    }

    @Override
    public int characteristics() {
        return SIZED;
    }
}

然后:

public final class RandomCollectors {

    private RandomCollectors() {
    }

    public static <T> Collector<T, ?, Stream<T>> toImprovedLazyShuffledStream() {
        return Collectors.collectingAndThen(
          toCollection(ArrayList::new),
          list -> !list.isEmpty()
            ? StreamSupport.stream(new ImprovedRandomSpliterator<>(list, Random::new), false)
            : Stream.empty());
    }

    public static <T> Collector<T, ?, Stream<T>> toEagerShuffledStream() {
        return Collectors.collectingAndThen(
          toCollection(ArrayList::new),
          list -> {
              Collections.shuffle(list);
              return list.stream();
          });
    }
}

然后你可以像这样使用它:

stream
  .collect(toLazyShuffledStream()) // or toEagerShuffledStream() depending on the use case
  .distinct()
  .limit(42)
  .forEach( ... );

详细解释可以看here

【讨论】:

    【解决方案8】:

    如果您想从流中随机抽取元素样本,则可以使用基于均匀分布的过滤器来替代洗牌:

    ...
    import org.apache.commons.lang3.RandomUtils
    
    // If you don't know ntotal, just use a 0-1 ratio 
    var relativeSize = nsample / ntotal;
    
    Stream.of (...) // or any other stream
    .parallel() // can work in parallel
    .filter ( e -> Math.random() < relativeSize )
    // or any other stream operation
    .forEach ( e -> System.out.println ( "I've got: " + e ) ); 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-17
      • 1970-01-01
      • 2020-04-01
      • 1970-01-01
      • 2021-12-24
      相关资源
      最近更新 更多