关于列表:在Java中多次获取集合的第n个元素

Getting the nth element of a set several times in Java

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

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


如果您确定需要来自集合中随机位置的n个元素(类似于统计抽样),那么您可能需要考虑只迭代集合一次并在迭代时按所需概率选取样本通过集合。这种方式更有效,因为您只需迭代一次该集合。

以下程序演示了这个想法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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());
    }
}

该计划的输出:

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

更新

然而,上述计划有一个警告 - 自收拾以来
通过概率完成那一个走过集合的样本,
返回的sample可能,取决于你当天的运气,
比指定的样本少或多。
但是,这个问题可以通过略微改变程序来弥补,
它使用略有不同的方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 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()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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);
    }
}


如果你的要求是从一个相当大的集合中选择随机元素,那么你应该问自己一个集合是否最适合它。

如果您想使用内置套件,您将面临一些挑战。

TreeSet中

TreeSet是一个有序集,因此允许您访问第n个元素。但是,您必须迭代到位置n,因为没有像ArrayList那样允许随机访问的数组。顾名思义,TreeSet中的节点形成树,节点很可能存储在内存中的任何位置。因为要获得第n个元素,你必须从第一个节点开始并从一个节点跳到另一个节点,直到你到达位置n - 这与你在LinkedList中的方式类似。

如果你想要做的就是选择一个随机元素,有几个选项:

  • 如果集合没有改变(或不经常),您可以创建匹配的数组或ArrayList并使用随机访问。
  • 迭代该集合随机数次。
  • 生成随机密钥并查找下一个更高/更低的元素,例如通过使用tailSet(randomKey)并获取该尾部集的第一个元素。当然,您必须处理元素范围之外的随机键。这样查找基本上就是二进制搜索。

HashSet的

HashSets基本上由两部分组成:一组桶和一个用于冲突的链表或树,即2个元素是否映射到同一个桶。然后可以通过访问随机存储桶(这将是随机访问)然后在该存储桶中的元素上迭代随机次数来获得随机元素。