从 C# 中的 List<T> 中选择 N 个随机元素的算法[重复]

Posted

技术标签:

【中文标题】从 C# 中的 List<T> 中选择 N 个随机元素的算法[重复]【英文标题】:algorithm for selecting N random elements from a List<T> in C# [duplicate] 【发布时间】:2017-02-02 12:34:19 【问题描述】:

我需要一个快速算法来从通用列表中选择 4 个随机元素。例如,我想从 List 中获取 4 个随机元素,然后根据一些计算,如果发现元素无效,那么它应该再次从列表中选择接下来的 4 个随机元素。

【问题讨论】:

你不想从一个列表中随机选择 n 个元素,你想随机打乱列表中的项目,然后取前 n 个元素。 (然后是下一个 n,然后是下一个,依此类推)——或者您真的想冒险再次获得以前获得的相同元素吗? 是否允许多次选择相同 @MichalHainc - 事实上,“最佳”方法在很大程度上取决于列表中有多少项目以及预期要提取的百分比。如果有很多元素,但只想随机从中取出几个元素,则可以“记住”哪些索引被取出(可能在 HashSet&lt;int&gt; 中)并随机生成 n 个索引 not 在该集合中(一旦生成就添加它们)。 @MichalHainc 洗牌列表是 O(n) 操作 - 这怎么可能比任何其他从列表中删除随机项目 O(n) 的方法更昂贵? ***.com/questions/48087/… 被建议为原始副本(并且它与标题匹配),但我认为更一般的“随机播放”副本(特别是可枚举版本 ***.com/a/7913534/477420)涵盖更多案例,更适合拍摄4,而不是再拿 4。 【参考方案1】:

你可以这样做

    public static class Extensions
    
        public static Dictionary<int, T> GetRandomElements<T>(this IList<T> list, int quantity)
        
            var result = new Dictionary<int, T>();
            if (list == null)
                return result;
            Random rnd = new Random(DateTime.Now.Millisecond);
            for (int i = 0; i < quantity; i++)
            
                int idx = rnd.Next(0, list.Count);
                result.Add(idx, list[idx]);
            
            return result;
        
    

然后像这样使用扩展方法:

    List<string> list = new List<string>()  "a", "b", "c", "d", "e", "f", "g", "h" ;
    Dictionary<int, string> randomElements = list.GetRandomElements(3);
    foreach (KeyValuePair<int, string> elem in randomElements)
    
        Console.WriteLine($"index in original list: elem.Key value: elem.Value");
    

【讨论】:

如果您在方法内声明Random rnd = new Random(DateTime.Now.Millisecond); 并在一个非常紧密的循环中多次调用该方法,您可能会得到相同的“随机”元素。最好在方法之外将其声明为Extension 的静态字段。此外,使用这种方法存在在多次调用甚至可能在同一次调用中获得相同项目的风险。 -- 问题是,如果这种行为在 OP 的情况下是可以的。 我认为这不会有太大帮助,因为DateTime 的分辨率根本不够高(你会得到>100 个刻度的“跳跃”;这就是为什么建议使用Stopwatch如果需要高精度并且可用)。只需拥有Random 的一个实例并重用它(从不“新建”它)就可以完全规避这个问题。【参考方案2】:

类似的东西:

using System;
using System.Collections.Generic;

        public class Program
        
            public static void Main()
            
                var list = new List<int>();

                list.Add(1);
                list.Add(2);
                list.Add(3);
                list.Add(4);
                list.Add(5);

                int n = 4;

                var rand = new Random();

                var randomObjects = new List<int>();

                for (int i = 0; i<n; i++)
                
                    var index = rand.Next(list.Count);

                    randomObjects.Add(list[index]);
                       

            
        

【讨论】:

【参考方案3】:

您可以将索引存储在某个列表中以获取非重复索引:

List<T> GetRandomElements<T>(List<T> allElements, int randomCount = 4)

    if (allElements.Count < randomCount)
    
        return allElements;
    

    List<int> indexes = new List<int>();

    // use HashSet if performance is very critical and you need a lot of indexes
    //HashSet<int> indexes = new HashSet<int>(); 

    List<T> elements = new List<T>();

    Random random = new Random(); 
    while (indexes.Count < randomCount)
    
        int index = random.Next(allElements.Count);
        if (!indexes.Contains(index))
        
            indexes.Add(index);
            elements.Add(allElements[index]);
        
    

    return elements;

然后你可以做一些计算并调用这个方法:

void Main(String[] args)

    do
    
        List<int> elements = GetRandomelements(yourElements);
        //do some calculations
     while (some condition); // while result is not right

【讨论】:

List.Contains 是 "slow" (O(n)),Set.Contains(例如 HashSet&lt;int&gt;)将是 "faster" (O(1))。 @Corak,好点,但是当列表中有 4 个元素时,不会有严重的性能损失。 这就是我使用引号的原因。 :) @Corak,谢谢,我在回答中添加了评论【参考方案4】:

假设List的长度是N。现在假设你将把这4个数字放到另一个调用出来的List中。然后你可以遍历列表,你被选中的元素的概率是

(4 - (out.Count)) / (N - currentIndex)

【讨论】:

【参考方案5】:
funcion (list)
(
loop i=0 i < 4
  index = (int) length(list)*random(0 -> 1)  
  element[i] = list[index] 
return element
) 

while(check  == false)
(
   elements = funcion (list)

    Do some calculation which returns check == false /true
)

这是伪代码,但我认为你应该自己想出这个。 希望对你有帮助:)

【讨论】:

【参考方案6】:

到目前为止,所有答案都有一个根本缺陷;您正在要求一种算法,该算法将生成n 元素的随机组合,并且这种组合遵循一些逻辑规则,是否有效。如果不是,则应产生新的组合。显然,这种新组合应该是之前从未生产过的组合。所有提出的算法都没有强制执行这一点。例如,如果在1000000 可能的组合中,只有一个是有效的,那么您可能会浪费大量资源,直到生成该特定的唯一组合。

那么,如何解决这个问题?嗯,答案很简单,创建所有个可能的唯一解决方案,然后简单地以随机顺序生成它们。警告:我会假设输入流没有重复元素,如果有,那么某些组合将不是唯一的。

首先,让我们自己编写一个方便的不可变堆栈:

class ImmutableStack<T> : IEnumerable<T>

    public static readonly ImmutableStack<T> Empty = new ImmutableStack<T>();
    private readonly T head;
    private readonly ImmutableStack<T> tail;
    public int Count  get; 

    private ImmutableStack()
    
        Count = 0;
    

    private ImmutableStack(T head, ImmutableStack<T> tail)
    
        this.head = head;
        this.tail = tail;
        Count = tail.Count + 1;
    

    public T Peek()
    
        if (this == Empty)
            throw new InvalidOperationException("Can not peek a empty stack.");

        return head;
    

    public ImmutableStack<T> Pop()
    
        if (this == Empty)
            throw new InvalidOperationException("Can not pop a empty stack.");

        return tail;
    

    public ImmutableStack<T> Push(T item) => new ImmutableStack<T>(item, this);

    public IEnumerator<T> GetEnumerator()
    
        var current = this;

        while (current != Empty)
        
            yield return current.head;
            current = current.tail;
        
    

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

这将使我们的生活更轻松,同时通过递归生成所有组合。接下来,让我们正确获取我们的 main 方法的签名:

public static IEnumerable<IEnumerable<T>> GetAllPossibleCombinationsInRandomOrder<T>(
    IEnumerable<T> data, int combinationLength)

好的,看起来差不多。现在让我们来实现这个东西:

    var allCombinations = GetAllPossibleCombinations(data, combinationLength).ToArray();
    var rnd = new Random();
    var producedIndexes = new HashSet<int>();

    while (producedIndexes.Count < allCombinations.Length)
    
        while (true)
        
            var index = rnd.Next(allCombinations.Length);

            if (!producedIndexes.Contains(index))
            
                producedIndexes.Add(index);
                yield return allCombinations[index];
                break;
            
        
    

好的,我们在这里所做的只是生成随机索引,检查我们尚未生成它(为此我们使用 HashSet&lt;int&gt;),然后返回该索引处的组合。

简单,现在我们只需要处理GetAllPossibleCombinations(data, combinationLength)

这很简单,我们将使用递归。我们的救助条件是当我们当前的组合是指定的长度时。另一个警告:我在整个代码中省略了参数验证,应该注意检查null 或指定长度是否大于输入长度等。

只是为了好玩,我将在这里使用一些小的 C#7 语法:嵌套函数。

public static IEnumerable<IEnumerable<T>> GetAllPossibleCombinations<T>(
    IEnumerable<T> stream, int length)

    return getAllCombinations(stream, ImmutableStack<T>.Empty);

    IEnumerable<IEnumerable<T>> getAllCombinations<T>(IEnumerable<T> currentData, ImmutableStack<T> combination)
    
        if (combination.Count == length)
            yield return combination;

        foreach (var d in currentData)
        
            var newCombination = combination.Push(d);

            foreach (var c in getAllCombinations(currentData.Except(new[]  d ), newCombination))
            
                yield return c;
            
        
    

现在我们可以使用这个了:

var data = "abc";
var random = GetAllPossibleCombinationsInRandomOrder(data, 2);

foreach (var r in random)

    Console.WriteLine(string.Join("", r));

果然,输出是:

bc
cb
ab
ac
ba
ca

【讨论】:

以上是关于从 C# 中的 List<T> 中选择 N 个随机元素的算法[重复]的主要内容,如果未能解决你的问题,请参考以下文章

C# 从 List<T> 中选择存在于另一个列表中的所有元素

从 C# 中的 List<T> 中删除重复项

如何从 C# 中的通用 List<T> 中获取元素? [复制]

从 List<OwnStruct> 返回 List<T> 的方法,其中 List<T> 仅包含 List 中所有 OwnStructs 的一个属性(C#)[重复]

在 List<T> 中选择一周内没有休息日的记录 - C#

请教C#中的List<T>,筛选list中特定元素的方法