随机返回n个不重复数字数组

本文探讨了两种方法:一是通过圆周分布函数生成圆内的随机点,适用于数学建模;二是利用Fisher-Yates洗牌算法实现数组的随机排列,常用于数据处理和随机化操作。通过实例展示了如何在不同场景下运用这些技术,如生成均匀分布的点集和随机数数组。

方法一:给定圆的半径和圆心的位置,在圆中产生均匀随机点。利用分布函数实

class RandomResult
    {
        double rad, xc, yc;
        Random rand = new Random();
        public RandomResult(double radius, double x_center, double y_center)
        {
            rad = radius;
            xc = x_center;
            yc = y_center;
        }
        public (double, double) randPoint()
        {
            double d = rad * Math.Sqrt(rand.NextDouble());
            double theta = rand.NextDouble() * 2 * Math.PI;
            return (d * Math.Cos(theta) + xc, d * Math.Sin(theta) + yc);
        }
       public double[] GetPoints()
            {
                int maxValue = (int)Math.Ceiling(rad);
                double[] result = new double[maxValue];
                maxValue -=2;
                for (int i = 0; i < maxValue; i++)
                {
                    (result[i], result[i + 1]) = randPoint();
                }
                return result;
            }
}

方法二:Fisher-Yates洗牌算法,思路是:从原始数组中随机选择一个元素,将其与数组的第一个元素交换位置,然后从剩余的n-1个元素中随机选择一个元素,将其与数组的第二个元素交换位置,以此类推,直到遍历完所有的n个元素为止。

定义一个源数组,将指定范围数字放入源数组,随机源数组的索引,每次随机范围减1,每次随机到的索引元素值放入结果集数组中,然后将随机到的索引元素值替换为当前随机的最大索引位置元素

public static int[] randomNums3(int min, int max, int n)
        {
            int range = max - min + 1;
            if (min > max || n < 0 || n > range)
            {
                return null;
            }
            int[] nums = new int[n];
            int[] sources = new int[range];
            for (int i = 0; i < range; i++)
            {
                sources[i] = min + i;
            }
            Random random = new Random();
            for (int i = 0; i < n; i++)
            {
                int index = random.Next(range - i);
                nums[i] = sources[index];

                if (index != range - i - 1)
                {
                    sources[index] = sources[range - i - 1];
                }
            }
            return nums;
        }

结果调用:

static void Main(string[] args)
        {
            int min = 1, max = 1_000_000, n = max - min+1;

            DateTime t1 = DateTime.Now;
            Console.WriteLine(randomNums3(min, max, n).Distinct().Count());
            DateTime t2 = DateTime.Now;
            Console.WriteLine($"用时:{t2 - t1}");

            t1=DateTime.Now;
            RandomResult randomResult = new RandomResult(max,1,1);
            Console.WriteLine(randomResult.GetPoints().Distinct().Count());
            t2 =DateTime.Now;
            Console.WriteLine($"用时:{t2 - t1}");
        }

<think>我们被要求生成一组重复随机数字。在TypeScript中,我们可以使用类似于JavaScript的方法,但要注意类型安全。 一种常见的方法是: 1. 生成一个包含所有可能数字数组(例如,从1到100)。 2. 打乱这个数组(洗牌算法)。 3. 如果需要n个重复随机数字,就取打乱后数组的前n个。 我们可以参考引用[1]中提到的shuffle方法,这是一个泛型函数,用于打乱数组。我们可以使用这个方法。 但是,注意:如果我们需要生成的数字范围很大(比如从0到999999),而我们只需要很少几个(比如10个),那么生成整个数组再打乱就会很浪费。 因此,我们可以考虑另一种方法:使用Set来记录已经生成的数字,每次生成一个随机数,如果已存在就重新生成,直到得到重复数字。 然而,当需要的数量接近范围的总数时,使用Set的方法可能会因为重复冲突而变慢。因此,我们需要根据需求选择合适的方法。 这里我们提供两种方法: 方法一:使用洗牌算法(适合需要生成的数量接近总数,或者总数太大的情况) 方法二:使用Set和循环(适合需要生成的数量远小于总数的情况) 我们分别实现: 方法一(洗牌算法): 步骤: a. 创建一个包含所有可能数字数组(例如,从min到max)。 b. 打乱这个数组(使用洗牌函数,如引用[1]中的shuffle函数)。 c. 取前n个。 方法二(使用Set): a. 创建一个空的Set。 b. 循环直到Set中有n个元素: - 生成一个[min, max]范围内的随机整数。 - 如果这个数在Set中,就加入Set。 我们分别写两个函数: 注意:我们使用TypeScript,所以要指定类型。 首先,实现洗牌函数(可以直接使用引用[1]中的,但注意它使用了readonly数组返回一个新数组): 我们将其复制过来,并稍作调整(因为引用[1]的代码是JavaScript,我们加上TypeScript类型)。 然后,实现两个生成函数。 另外,我们还可以考虑第三种方法:当范围很大且需要数量较时,可以使用Fisher-Yates洗牌算法的部分版本,即只洗牌我们需要的部分。但这里我们先实现前两种。 我们来写代码: 方法一:使用洗牌 注意:如果范围很大(比如1到1000000)而我们只需要10个,那么创建这么大的数组然后完全打乱再取前10个,显然效率高。因此,这种方法适用于范围是特别大的情况。 方法二:使用Set,当范围很大而需要数量很少时效率高,但如果需要数量接近范围总数,则冲突会很,效率下降。 因此,我们可以根据情况选择,或者我们实现一个优化的方法:使用Fisher-Yates洗牌算法但只洗我们需要的前n个元素(即部分洗牌)。 方法三(优化):部分洗牌(Fisher-Yates的变种) a. 创建一个长度为max-min+1的数组(如果范围很大,我们可以创建整个数组,而是使用一个对象或Map来模拟已选索引?但是这样还是需要存储整个数组的索引?) b. 实际上,我们可以使用一个数组来存储0到size-1的索引(size为总数),然后我们只进行n次交换,然后取前n个。这样我们只需要创建大小为size的数组,但只进行n次操作。如果n远小于size,那么这样还是浪费空间。 另一种部分洗牌:使用Map或对象来存储已经交换的位置,但实际上我们只需要前n个,我们可以这样做: 初始化一个空数组result(用于存放结果)和一个空Map(用于存放已经交换过的位置)。 但通常,我们使用Fisher-Yates算法时,是从整个数组的前面开始,然后随机交换到后面。如果我们只需要n个,那么我们可以: 创建一个长度为n的数组,然后我们使用一个循环,从0到n-1,每次从[i, size-1]中随机选一个位置,然后与i位置交换,然后取这个值。 但是,这样我们仍然需要创建整个数组(大小为size),当size很大时,内存消耗大。 因此,当范围很大(比如1到1000000)而我们只需要10个,我们可以使用方法二(Set)或者使用一个对象记录已经选中的数字(类似于方法二),或者使用一个数组来存储0到size-1,然后只交换前10次(这样需要创建1000000大小的数组,显然好)。 所以,我们推荐: - 如果范围大(比如小于100000),使用方法一(洗牌整个数组)或方法三(部分洗牌整个数组,但只取前n个)。 - 如果范围很大且需要数量n远小于范围大小(比如n小于范围大小的1%),使用方法二(Set)。 - 如果范围很大且需要数量n接近范围大小,我们可以使用改进的Fisher-Yates算法,但只进行n次交换,这样我们只需要一个长度为n的数组,然后利用一个Map来存储已经交换过的位置?但是这样比较复杂。 实际上,我们可以使用“惰性数组”的方法,即我们只存储实际被选中的位置,然后使用一个Map来记录已经交换的位置,这样我们可以避免创建整个数组。具体如下: 方法四(适用于大范围且需要数量较的情况): 我们使用一个Map(或普通对象)来存储已经交换过的位置,同时我们有一个结果数组。 我们进行n次循环,每次: 生成一个随机位置r,范围从0到size-1(或者min到max,但注意我们实际上是在虚拟的数组下标0到size-1上操作)。 检查这个位置r是否已经被交换过?如果被交换过,那么它已经被换到了前面某个位置,那么我们就取这个位置r对应的值(注意:在虚拟数组中,我们并没有实际存储整个数组,所以我们需要记录每个位置的值?) 但是,如果我们创建整个数组,那么如何知道位置r的值呢?实际上,我们可以这样:每个位置的值就是min+r(因为数组下标0对应min,1对应min+1,...)。 然而,在交换过程中,我们可能会把后面的某个值换到前面,这样位置r的值可能已经被换走了?所以我们需要记录每个位置当前的值。 这变得复杂了。实际上,我们可以使用一个更简单的方法:使用一个对象(或Map)来存储已经生成过的随机数,但这样和方法二类似。 因此,我们回到方法二:使用Set,当范围很大而需要数量n很小时,效率是可以接受的。 总结: 我们提供两个函数: 1. 使用洗牌整个数组(适合范围小) 2. 使用Set(适合范围大且需要数量少) 另外,我们还可以提供第三种:使用Fisher-Yates算法部分洗牌(只洗前n个),但需要创建整个数组(所以只适合范围太大的情况)。我们这里实现。 我们实现前两种。 注意:我们还需要考虑范围,比如min和max。 函数签名: function generateUniqueRandomNumbersByShuffle(min: number, max: number, n: number): number[] function generateUniqueRandomNumbersBySet(min: number, max: number, n: number): number[] 但是,注意:n能大于范围大小(max-min+1),否则会陷入死循环(方法二)或者取到足够的数(方法一)。所以我们要检查。 我们先写一个洗牌函数(引用[1]的): */</think>在 TypeScript 中生成一组重复随机数字,可以通过以下几种方法实现。以下是具体实现方案和代码示例: ### 方法一:使用 Fisher-Yates 洗牌算法(推荐) ```typescript /** * 生成指定范围内的重复随机数 * @param min 最小值 * @param max 最大值 * @param count 生成数量 */ const generateUniqueRandoms = (min: number, max: number, count: number): number[] => { // 创建有序数字池 const pool = Array.from({ length: max - min + 1 }, (_, i) => i + min); // Fisher-Yates 洗牌算法 for (let i = pool.length - 1; i > 0 && count > 0; i--, count--) { const j = Math.floor(Math.random() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } // 返回洗牌后的尾部元素 return pool.slice(-count); }; // 示例:生成 10 个 1-100 的重复随机数 const randoms = generateUniqueRandoms(1, 100, 10); console.log(randoms); ``` #### 算法说明 1. **时间复杂度**:$O(n)$,只需单次遍历 2. **空间复杂度**:$O(n)$,需存储数字池 3. **优势**:无重复冲突问题,结果严格随机 ### 方法二:使用 Set 去重(适合小规模数据) ```typescript const generateBySet = (min: number, max: number, count: number): number[] => { const set = new Set<number>(); while (set.size < count) { const num = Math.floor(Math.random() * (max - min + 1)) + min; set.add(num); } return Array.from(set); }; ``` ### 方法三:使用 Map 记录(优化版) ```typescript const generateByMap = (min: number, max: number, count: number): number[] => { const result: number[] = []; const map = new Map<number, boolean>(); while (result.length < count) { const num = Math.floor(Math.random() * (max - min + 1)) + min; if (!map.has(num)) { map.set(num, true); result.push(num); } } return result; }; ``` ### 性能对比 | 方法 | 适合场景 | 时间复杂度 | 空间复杂度 | |------------|------------------------------|---------------------|------------| | Fisher-Yates | 大范围取数 | $O(n)$ | $O(n)$ | | Set | 小范围取少数 | $O(k)$ (k为尝试次数)| $O(m)$ | | Map | 中等范围数据 | $O(m)$ | $O(m)$ | > **选择建议**: > 1. 当需要生成数量 > 30% 范围总数时,优先使用 Fisher-Yates > 2. 生成数量较少时(< 1000),可使用 Set/Map 简化实现 > 3. 超大范围(如 1-1000000)取少量数时,推荐线性同余法[^1] ### 注意事项 1. **范围校验**:需添加参数验证 ```typescript if (count > max - min + 1) throw new Error("请求数量超过范围"); ``` 2. **随机性安全**:加密场景应使用 `crypto.getRandomValues()` 3. **大数据优化**:超百万级数据可使用位图压缩[^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值