java - 在 Java 中多次获取集合的第 n 个元素

标签 java list collections set element

在Java中是否有任何有效的方法来获取集合的第n个元素? 我知道有两种方法: - 迭代它直到到达所需的元素 - 通过将其转换为 ArrayList 并从该 ArrayList 获取元素 问题是有没有其他方法可以快速得到它的第n个元素。我主要需要像 TreeSets 这样的功能。

编辑: 例如,如果我想从 10 000 000 个元素长的 TreeMap 或树集中非常频繁地(即每 2-3 秒)选择 1000 个随机元素,那么一直将其克隆到数组列表的效率非常低,并且迭代这么多elements 的效率也很低。

最佳答案

如果您确定需要集合中随机位置的 n 个元素(有点像统计采样),那么您可能需要考虑仅迭代集合一次并按照所需的 概率,当你迭代集合时。这种方式更有效,因为您只需迭代该集合一次。

下面的程序演示了这个想法:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

public class SamplingFromSet {

    public static void main(String[] args) {
        Set<String> population = new TreeSet<>();

        /*
         * Populate the set
         */
        final int popSize = 17;
        for (int i=0; i<popSize; i++) {
            population.add(getRandomString());
        }

        List<String> sample 
            = sampleFromPopulation(population, 3 /*sampleSize */);

        System.out.println("population is");
        System.out.println(population.toString());
        System.out.println("sample is");
        System.out.println(sample.toString());

    }


    /**
     * Pick some samples for a population
     * @param population
     * @param sampleSize - number of samples
     * @return
     */
    private static <T> 
    List<T> sampleFromPopulation(Set<T> population
                                    , int sampleSize) {
        float sampleProb = ((float) sampleSize) / population.size();
        List<T> sample = new ArrayList<>();
        Iterator<T> iter = population.iterator();
        while (iter.hasNext()) {
            T element = iter.next();
            if (random.nextFloat()<sampleProb) {
                /*
                 * Lucky Draw!
                 */
                sample.add(element);
            }
        }
        return sample;
    }


    private static Random random = new Random();    

    private static String getRandomString() {
        return String.valueOf(random.nextInt());
    }
}

该程序的输出:

population is
[-1488564139, -1510380623, -1980218182, -354029751, -564386445, -57285541, -753388655, -775519772, 1538266464, 2006248253, 287039585, 386398836, 435619764, 48109172, 580324150, 64275438, 860615531]
sample is
[-57285541, -753388655, 386398836]

更新

然而,上述程序有一个警告——因为 通过概率完成一次遍历集合的样本数, 返回的样本可能取决于您当天的运气, 样本数量少于或超过指定数量。 然而,这个问题可以通过稍微改变程序来解决, 它使用稍微不同的方法签名:

/**
 * Pick some samples from a population
 * @param population
 * @param sampleSize - number of samples
 * @param exactSize - a boolean to control whether or not
 *   the returned sample list must be of the exact size as
 *   specified.
 * @return
 */
private static <T> 
List<T> sampleFromPopulation(Set<T> population
                                , int sampleSize
                                , boolean exactSize);

防止过采样

在总体的一次迭代中,我们过度采样一点, 最后,如果样本太多,我们会丢弃一些样本。

防止采样不足

另请注意,即使进行过采样,也存在非零概率 在总体的一次迭代结束时,我们仍然 得到的样本比期望的要少。如果发生这种情况(不太可能),我们将递归调用 第二次尝试时再次使用相同的方法。 (此递归的概率接近一 终止,因为它非常不同,在重复递归中 调用该方法,我们始终会出现欠采样。)

以下代码实现了新的 sampleFromPopulation() 方法:

private static <T> 
List<T> sampleFromPopulation(Set<T> population
                                , int sampleSize
                                , boolean exactSize) {
    int popSize = population.size();
    double sampleProb = ((double) sampleSize) / popSize;

    final double    OVER_SAMPLING_MULIT = 1.2;
    if (exactSize) {
        /*
         * Oversampling to enhance of chance of getting enough
         * samples (if we then have too many, we will drop them 
         * later)
         */
        sampleProb = sampleProb * OVER_SAMPLING_MULIT;
    }
    List<T> sample = new LinkedList<>(); // linked list for fast removal
    Iterator<T> iter = population.iterator();
    while (iter.hasNext()) {
        T element = iter.next();
        if (random.nextFloat()<sampleProb) {
            /*
             * Lucky Draw!
             */
            sample.add(element);
        }
    }
    int samplesTooMany = sample.size() - sampleSize;
    if (!exactSize || samplesTooMany==0) {
        return sample;
    } else if (samplesTooMany>0) {
        Set<Integer> indexesToRemoveAsSet = new HashSet<>();
        for (int i=0; i<samplesTooMany; ) {
            int candidate = random.nextInt(sample.size());
            if (indexesToRemoveAsSet.add(candidate)) {
                /*
                 * add() returns true if candidate was not 
                 * previously in the set
                 */
                i++; // proceed to draw next index
            }
        }
        List<Integer> indexesToRemoveAsList 
            = new ArrayList<>(indexesToRemoveAsSet);
        Collections.sort(indexesToRemoveAsList
                , (i1, i2) -> i2.intValue() - i1.intValue()); // desc order  
        /*
         * Now we drop from the tail of the list
         */
        for (Integer index : indexesToRemoveAsList) {
            sample.remove((int) index); // remove by index (not by element)
        }
        return sample;
    } else { 
        /*
         * we were unluckly that we oversampling we still
         * get less samples than specified, so here we call
         * this very same method again recursively
         */
        return sampleFromPopulation(population, sampleSize, exactSize);
    }
}

关于java - 在 Java 中多次获取集合的第 n 个元素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41615612/

相关文章:

python - 如何创建一个包含子元素的列表(Python 元素树)

java - 这些泛型语句之间有什么区别?

java - 混合 GWT 和 Ext GWT

java - 以特定数字为增量生成双随机数

重命名数据框列表中的所有列

c# - 遍历事件日志条目集合,IndexOutOfBoundsException

Java Android GCM 获取 crash_key

c# - 防止从外部类 c# 访问列表元素

collections - 正确使用 meteor 包中的集合?

java - 如何根据前 10 位数字查找重复值?