生成指定范围内互不重复的随机数是编程中非常常见的需求,无论是做抽奖系统、随机分配任务还是生成测试数据,都需要保证生成的随机数在目标区间内且不会重复出现。下面介绍几种常用的实现方案,你可以根据具体的场景选择使用。

方案一:循环判断去重法
这种是最直观的实现思路,先生成一个随机数,然后判断这个随机数是否已经存在于结果集合中,如果不存在就加入,直到结果集合的数量达到要求为止。
这种方案适合生成数量较少、范围不大的场景,如果生成的数量接近范围上限,循环判断的次数会非常多,效率会明显下降。
// 生成[start, end]范围内count个互不重复的随机数,包含start和end
function generateUniqueRandom1(start, end, count) {
// 计算范围大小,判断请求的数量是否超过范围
const rangeSize = end - start + 1;
if (count > rangeSize) {
throw new Error('请求的数量超过指定范围,无法生成互不重复的随机数');
}
const result = [];
while (result.length < count) {
// 生成指定范围内的随机整数
const randomNum = Math.floor(Math.random() * rangeSize) + start;
// 判断是否已经存在,不存在则加入结果数组
if (!result.includes(randomNum)) {
result.push(randomNum);
}
}
return result;
}
// 示例:生成1到10之间3个互不重复的随机数
console.log(generateUniqueRandom1(1, 10, 3));
方案二:数组洗牌法(Fisher-Yates洗牌算法)
这种方案的核心是先生成指定范围内的所有整数,组成一个完整的数组,然后对这个数组进行随机洗牌,最后取前count个元素即可。这种方式不需要反复判断去重,效率更稳定。
洗牌算法的时间复杂度是O(n),适合范围不是特别大,且生成数量可能接近范围上限的场景,只要范围大小在内存可接受范围内就可以使用。
// Fisher-Yates洗牌算法实现,随机打乱数组顺序
function shuffle(arr) {
const newArr = [...arr];
for (let i = newArr.length - 1; i > 0; i--) {
// 生成一个0到i之间的随机索引
const randomIndex = Math.floor(Math.random() * (i + 1));
// 交换当前元素和随机索引位置的元素
[newArr[i], newArr[randomIndex]] = [newArr[randomIndex], newArr[i]];
}
return newArr;
}
// 生成[start, end]范围内count个互不重复的随机数
function generateUniqueRandom2(start, end, count) {
const rangeSize = end - start + 1;
if (count > rangeSize) {
throw new Error('请求的数量超过指定范围,无法生成互不重复的随机数');
}
// 生成完整的范围数组
const fullRange = [];
for (let i = start; i <= end; i++) {
fullRange.push(i);
}
// 洗牌后取前count个元素
const shuffled = shuffle(fullRange);
return shuffled.slice(0, count);
}
// 示例:生成1到10之间3个互不重复的随机数
console.log(generateUniqueRandom2(1, 10, 3));
方案三:Set集合去重法
利用Set集合元素不重复的特性,循环生成随机数并加入Set,直到Set的大小达到要求的数量,最后把Set转成数组返回。这种写法比数组的includes判断更简洁,Set的has方法查找效率也比数组高。
// 生成[start, end]范围内count个互不重复的随机数
function generateUniqueRandom3(start, end, count) {
const rangeSize = end - start + 1;
if (count > rangeSize) {
throw new Error('请求的数量超过指定范围,无法生成互不重复的随机数');
}
const resultSet = new Set();
while (resultSet.size < count) {
const randomNum = Math.floor(Math.random() * rangeSize) + start;
resultSet.add(randomNum);
}
// 把Set转成数组返回
return Array.from(resultSet);
}
// 示例:生成1到10之间3个互不重复的随机数
console.log(generateUniqueRandom3(1, 10, 3));
不同方案对比
我们可以把三种方案的特点整理成表格,方便你根据场景选择:
| 方案 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 循环判断去重法 | 最坏情况O(∞),平均O(n) | O(k),k为生成数量 | 生成数量远小于范围大小,范围较大的场景 |
| 数组洗牌法 | O(n),n为范围大小 | O(n),n为范围大小 | 范围大小适中,生成数量可能接近范围上限的场景 |
| Set集合去重法 | 最坏情况O(∞),平均O(n) | O(k),k为生成数量 | 生成数量远小于范围大小,需要简洁写法的场景 |
注意事项
- 生成随机数前一定要先判断请求的数量是否超过指定范围,避免进入死循环或者抛出无意义的错误。
- 如果范围非常大(比如1到1000000),不建议使用数组洗牌法,因为会占用大量内存,此时优先选择循环判断或者Set去重法。
- 如果需要生成的是浮点型随机数,只需要调整随机数的生成逻辑即可,去重的逻辑和整数场景是一致的。
注意:Math.random()生成的是伪随机数,不适合对随机性要求极高的加密场景,加密场景请使用crypto.getRandomValues等密码学安全的随机数生成接口。