从List中有效地选择N个随机元素(不使用toArray并更改列表)

时间:2014-05-18 08:29:23

标签: java algorithm random

在标题中,我想使用Knuth-Fisher-Yates shuffle算法从List中选择N个随机元素,但不使用List.toArray并更改列表。这是我目前的代码:

public List<E> getNElements(List<E> list, Integer n) {
    List<E> rtn = null;

    if (list != null && n != null && n > 0) {
        int lSize = list.size();
        if (lSize > n) {
            rtn = new ArrayList<E>(n);
            E[] es = (E[]) list.toArray();
            //Knuth-Fisher-Yates shuffle algorithm 
            for (int i = es.length - 1; i > es.length - n - 1; i--) {
                int iRand = rand.nextInt(i + 1);
                E eRand = es[iRand];
                es[iRand] = es[i];
                //This is not necessary here as we do not really need the final shuffle result.
                //es[i] = eRand;
                rtn.add(eRand);
            }

        } else if (lSize == n) {
            rtn = new ArrayList<E>(n);
            rtn.addAll(list);
        } else {
            log("list.size < nSub! ", lSize, n);
        }
    }

    return rtn;
}

它使用list.toArray()创建一个新数组,以避免修改原始列表。但是,我现在的问题是我的列表可能非常大,可以有100万个元素。然后list.toArray()太慢了。而我的n可能在1到100万之间。当n很小(比如2)时,该函数效率非常低,因为它仍然需要对100万个元素的列表进行list.toArray()。

有人可以帮助改进上述代码,以便在处理大型列表时提高效率。感谢。

这里我假设Knuth-Fisher-Yates shuffle是从列表中选择n个随机元素的最佳算法。我对吗?如果有其他算法比Knuth-Fisher-Yates shuffle更好地完成结果的速度和质量(保证真正的随机性),我将非常高兴。

更新

以下是我的一些测试结果:

从1000000个元素中选择n时。

当n <1000000/4时,通过使用Daniel Lemire的Bitmap函数首先选择n random id的最快方法,然后获取带有这些id的元素:

public List<E> getNElementsBitSet(List<E> list, int n) {
        List<E> rtn = new ArrayList<E>(n);
        int[] ids = genNBitSet(n, 0, list.size());
        for (int i = 0; i < ids.length; i++) {
            rtn.add(list.get(ids[i]));
        }
        return rtn;
    }

genNBitSet正在使用https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2013/08/14/java/UniformDistinct.java

中的代码generateUniformBitmap

当n> 1000000/4时,水库采样方法更快。

所以我构建了一个结合这两种方法的函数。

7 个答案:

答案 0 :(得分:7)

您可能正在寻找类似Resorvoir Sampling的内容。

从包含第一个k元素的初始数组开始,并使用概率递减的新元素对其进行修改:

java like pseudo code:

E[] r = new E[k]; //not really, cannot create an array of generic type, but just pseudo code
int i = 0;
for (E e : list) {
   //assign first k elements:
   if (i < k) { r[i++] = e; continue; }
   //add current element with decreasing probability:
   j = random(i++) + 1; //a number from 1 to i inclusive
   if (j <= k) r[j] = e;
}
return r;

这需要对数据进行单次传递,每次迭代都需要非常便宜的操作,并且空间消耗与所需的输出大小成线性关系。

答案 1 :(得分:5)

如果n与列表的长度相比非常小,请取一组空的整数并继续添加随机索引,直到该集合具有正确的大小。

如果n与列表的长度相当,则执行相同操作,但随后返回列表中没有集合中索引的项目。

在中间位置,您可以遍历列表,并根据您看过的项目数量以及已经返回的项目随机选择项目。在伪代码中,如果你想要来自N的k项:

for i = 0 to N-1
    if random(N-i) < k
        add item[i] to the result
        k -= 1
    end
end

这里random(x)返回0(包括)和x(不包括)之间的随机数。

这产生k个元素的均匀随机样本。您还可以考虑使用迭代器来避免构建结果列表以节省内存,假设列表在迭代时保持不变。

通过分析,您可以确定从朴素的set-building方法切换到迭代方法的转换点。

答案 2 :(得分:3)

假设您可以从m中生成n个随机索引,这些索引是成对不相交的,然后在集合中有效地查找它们。如果您不需要元素的顺序是随机的,那么您可以使用Robert Floyd的算法。

Random r = new Random();
Set<Integer> s = new HashSet<Integer>();
for (int j = m - n; j < m; j++) {
    int t = r.nextInt(j);
    s.add(s.contains(t) ? j : t);
}

如果您确实需要该命令是随机的,那么您可以运行Fisher-Yates,而不是使用数组,您使用HashMap只存储键和值不同的映射。假设散列是恒定时间,这两种算法都是渐近最优的(尽管很明显,如果你想随机抽取大部分数据,那么数据结构会有更好的常量)。

答案 3 :(得分:2)

为方便起见:MCVE执行Resorvoir Sampling proposed by amit可能的赞成票应该发给他(我只是在破解一些代码))

看起来这确实是一个算法,很好地涵盖了与列表大小相比,选择的元素数量的情况,与列表大小相比,元素的数量(假设维基百科页面上声明的结果随机性的属性是正确的。)

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.TreeMap;

public class ReservoirSampling
{
    public static void main(String[] args)
    {
        example();
        //test();
    }

    private static void test()
    {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");
        int size = 2;

        int runs = 100000;
        Map<String, Integer> counts = new TreeMap<String, Integer>();
        for (int i=0; i<runs; i++)
        {
            List<String> sample = sample(list, size);
            String s = createString(sample);
            Integer count = counts.get(s);
            if (count == null)
            {
                count = 0;
            }
            counts.put(s, count+1);
        }
        for (Entry<String, Integer> entry : counts.entrySet())
        {
            System.out.println(entry.getKey()+" : "+entry.getValue());
        }
    }

    private static String createString(List<String> list)
    {
        Collections.sort(list);
        StringBuilder sb = new StringBuilder();
        for (String s : list)
        {
            sb.append(s);
        }
        return sb.toString();
    }

    private static void example()
    {
        List<String> list = new ArrayList<String>();
        for (int i=0; i<26; i++)
        {
            list.add(String.valueOf((char)('A'+i)));
        }

        for (int i=1; i<=26; i++)
        {
            printExample(list, i);
        }
    }
    private static <T> void printExample(List<T> list, int size)
    {
        System.out.printf("%3d elements: "+sample(list, size)+"\n", size);
    }

    private static final Random random = new Random(0);
    private static <T> List<T> sample(List<T> list, int size)
    {
        List<T> result = new ArrayList<T>(Collections.nCopies(size, (T) null));
        int i = 0;
        for (T element : list)
        {
            if (i < size)
            {
                result.set(i, element);
                i++;
                continue;
            }
            i++;
            int j = random.nextInt(i);
            if (j < size)
            {
                result.set(j, element);
            }
        }
        return result;
    }

}

答案 4 :(得分:1)

如果n小于那么大小,你可以使用这个算法,不幸的是,n是二次方的,但是doest完全取决于数组的大小。

size = 100且n = 4。

的示例
choose random number from 0 to 99, lets say 42, and add it to result.
choose random number from 0 to 98, lets say 39, and add it to result.
choose random number from 0 to 97, lets say 41, but since 41 is bigger or equal than 39, increment it by 1, so you have 42, but that is bigger then equal than 42, so you have 43.
...

很快,您可以从剩余的数字中进行选择,然后根据您选择的数字进行比较。我会使用链接列表,但也许有更好的数据结构。

答案 5 :(得分:0)

总结Changwang的更新。如果您想要250,000个以上的项目,请使用amit的答案。否则,请使用Knuth-Fisher-Yates Shuffle,如此处全文所示

  

注意:结果也始终保持原始顺序

public static <T> List<T> getNRandomElements(int n, List<T> list) {
    List<T> subList = new ArrayList<>(n);
    int[] ids = generateUniformBitmap(n, list.size());
    for (int id : ids) {
        subList.add(list.get(id));
    }
    return subList;
}

// https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2013/08/14/java/UniformDistinct.java
private static int[] generateUniformBitmap(int num, int max) {
    if (num > max) {
        DebugUtil.e("Can't generate n ints");
    }
    int[] ans = new int[num];
    if (num == max) {
        for (int k = 0; k < num; ++k) {
            ans[k] = k;
        }
        return ans;
    }
    BitSet bs = new BitSet(max);
    int cardinality = 0;
    Random random = new Random();
    while (cardinality < num) {
        int v = random.nextInt(max);
        if (!bs.get(v)) {
            bs.set(v);
            cardinality += 1;
        }
    }
    int pos = 0;
    for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
        ans[pos] = i;
        pos += 1;
    }
    return ans;
}

如果您希望它们随机化,我可以使用:

public static <T> List<T> getNRandomShuffledElements(int n, List<T> list) {
    List<T> randomElements = getNRandomElements(n, list);
    Collections.shuffle(randomElements);
    return randomElements;
}

答案 6 :(得分:0)

在C#中我需要一些东西,这是在通用列表上工作的解决方案。

它选择列表中的N个随机元素,并将它们放置在列表的前面。

因此,返回时,将随机选择列表的前N个元素。即使您要处理大量元素,它也是快速有效的。

static void SelectRandom<T>(List<T> list, int n)
{
    if (n >= list.Count)
    {
        // n should be less than list.Count
        return;
    }
    int max = list.Count;
    var random = new Random();
    for (int i = 0; i < n; i++)
    {
        int r = random.Next(max);
        max = max - 1;
        int irand = i + r;
        if (i != irand)
        {
            T rand = list[irand];
            list[irand] = list[i];
            list[i] = rand;
        }
    }
}