【问题标题】:Getting the nth element of a set several times in Java在Java中多次获取集合的第n个元素
【发布时间】:2017-01-12 14:25:22
【问题描述】:

有没有什么有效的方法可以在 Java 中获取集合的第 n 个元素? 我知道两种方法: - 通过遍历它直到我到达所需的元素 - 通过将其转换为 ArrayList 并从该 ArrayList 中获取元素 问题是有没有其他方法可以快速获得它的第 n 个元素。对于 TreeSet,我主要需要一个类似的功能。

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

【问题讨论】:

  • 除非集合是有序集合(例如TreeSet),否则没有“nth”的概念。另一方面,TreeSet 不提供索引访问,因为无论如何都需要迭代(就像访问链表中的第 n 个元素需要迭代一样),因此我只会继续使用迭代方法或跟踪哪个在设置修改操作期间元素是第 n 个。
  • 有没有办法通过使用某种特殊的键来获取它,它有一个自定义的 compareTo、equals 和 hashCode 方法?
  • 即使这是可能的(并且几乎所有事情都是可能的)你不会真正获得任何东西:你必须知道树的确切布局,哪个索引对应于根节点,树是否反转等等,你必须跟踪查找步骤并从中计算当前索引 - 最后你可能会做尽可能多的事情,如果不是更多的话,就像你在迭代时所做的那样。另一方面,使用像这样的特殊键很容易出错并且实际上会破坏你的树 - 所以我不会这样做。
  • 我想先看看它的解决方案,然后我们可以考虑它会获得什么:) 顺便说一句,例如,如果我想从 10 000 000 个元素长中选择 1000 个随机元素treeset,非常频繁,然后一直克隆到arraylist是非常低效的,遍历这么多元素也是低效的。
  • “我想先看看它的解决方案” - 我宁愿不投资一个肯定会比解决任何问题造成更大伤害的解决方案 :) 我会添加一个关于什么的答案你会面临的挑战(至少是其中的一些),但最好的选择可能是退后一步,考虑一个系列是否是正确的结构。您可以编辑您的问题并添加更多要求,例如您需要什么,为什么需要使用集合(或为什么您认为这样做)等?

标签: java list collections set element


【解决方案1】:

如果您确定需要来自集合中随机位置的 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]

更新

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

/**
 * 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);

防止过采样

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

防止欠采样

还要注意,即使使用过采样,也存在非零概率 即,在人口的一次迭代结束时,我们仍然 获得比预期更少的样本。如果发生这种情况(不太可能),我们将递归调用 再次尝试相同的方法。 (此递归的概率接近 1 终止,因为它非常不同,在重复递归中 调用该方法,我们总是得到欠采样。)

以下代码实现了新的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);
    }
}

【讨论】:

  • 这是一个很好的解决方案;) 只是样本量可能不是想要的,而是多一些或少一些。你能改进你的代码来有效地解决这个问题吗?
  • 您能否详细说明“多一些或少一些”是什么意思?
  • @gyurix -- 好的,我明白你的意思了。我已经更新了上面的答案来解决这个问题。
【解决方案2】:

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

如果您想使用内置套装,您将面临几个挑战。

树集

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

如果您只想选择一个随机元素,有几个选项:

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

哈希集

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

【讨论】:

  • 有什么方法可以从默认的 java HashSets 访问这些存储桶,或者我需要为它做一个自定义的 HashSet 实现?
  • @gyurix 您可以查看源代码,但我假设您无法从外部访问它们,因此您至少必须创建 HashSet 的子类(除非存储桶是私有的,在这种情况下你需要另一个实现)。
  • 我检查过了,我需要另一个
猜你喜欢
  • 1970-01-01
  • 2014-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-08
  • 2010-10-16
  • 2012-02-16
  • 2017-03-11
相关资源
最近更新 更多