如何无需分配大小为n的数组生成0到n范围的随机排列迭代器
无需大数组生成0..n范围无重复随机整数迭代器方案
传统Fisher-Yates洗牌的空间复杂度为O(n),当n超过百万时,仅数组本身就会占用数MB到数十MB内存,且初始化和洗牌过程需要遍历全量元素,耗时较高。
核心原理
采用**保留格式加密(Format-Preserving Encryption, FPE)**实现,该算法的特性是加密后的输出和输入处于相同的数值范围,且密钥固定时为严格的一一映射关系,天然满足无重复、覆盖0..n全范围的要求,完全不需要提前分配数组存储所有元素,迭代时按需计算即可。
这里我们用轻量的Feistel网络实现FPE,无需引入第三方加密库,性能表现优异:
- 内存占用恒定为O(1),和n的大小完全无关
- 单次迭代仅涉及基础位运算,百万级元素遍历耗时可控制在20ms以内
- 支持固定种子复现随机序列,也支持直接访问第k个随机数,无需遍历前置元素
实现代码(JavaScript)
// 生成0..n范围无重复随机整数的迭代器 function createRandomRangeIterator(n, seed = Date.now()) { // 计算覆盖0..n需要的二进制位数 const totalBits = Math.ceil(Math.log2(n + 1)); const halfBits = Math.ceil(totalBits / 2); const leftMask = ((1 << halfBits) - 1) << halfBits; const rightMask = (1 << halfBits) - 1; // 基于种子生成轮密钥,固定seed可复现相同排列 const roundKeys = new Array(4).fill(0).map((_, idx) => { const rand = Math.sin(seed + idx) * 10000; return rand - Math.floor(rand); }); // 保留格式加密函数 const encrypt = (num) => { let left = (num & leftMask) >> halfBits; let right = num & rightMask; // 4轮Feistel迭代,随机性足够的同时保证性能 for (let i = 0; i < 4; i++) { const temp = right; const roundRes = Math.floor(roundKeys[i] * (1 << halfBits)) ^ right; right = left ^ roundRes; left = temp; } const result = (left << halfBits) | right; // 结果超出范围则递归重加密,一一映射保证不会死循环 return result > n ? encrypt(result) : result; }; let currentIndex = 0; return { next() { if (currentIndex > n) return { done: true }; return { done: false, value: encrypt(currentIndex++) }; }, [Symbol.iterator]() { return this; } }; } // 用法示例 const maxNum = 1000000; const randomIterator = createRandomRangeIterator(maxNum); // 直接遍历即可得到全范围无重复随机数 for (const num of randomIterator) { // 此处处理单个数值,无需预存所有元素 }
可选优化
- 对随机性要求较高的场景,可以把Feistel迭代轮数从4轮提升到8~16轮,性能损失极小
- 如果需要支持更大的n(超过2^32),可以把位运算替换为BigInt实现
内容的提问来源于stack exchange,提问作者Simon Farshid
相关产品推荐
相关产品推荐

