当前位置: 首页 > news >正文

手写数组洗牌算法

手写数组洗牌算法

问题描述

实现一个数组洗牌函数,使用Fisher-Yates(费雪-耶茨)洗牌算法,确保数组元素随机排列,每个元素在每个位置上的概率相等。

输入:一个数组
输出:洗牌后的数组(原地洗牌)

问题分析

Fisher-Yates洗牌算法的核心思想是:

  1. 从数组的末尾开始向前遍历
  2. 对于每个元素,随机选择一个在它之前(包括它自己)的元素
  3. 交换这两个元素
  4. 继续遍历,直到处理完所有元素

这种方法可以确保每个元素都有均等的机会被放置在任何位置,并且时间复杂度为O(n),空间复杂度为O(1)。

代码实现

/*** 手写数组洗牌算法(Fisher-Yates算法)* @param {Array} array - 需要洗牌的数组* @returns {Array} 洗牌后的数组(原地修改)*/
function shuffleArray(array) {// 从数组末尾开始遍历for (let i = array.length - 1; i > 0; i--) {// 随机生成一个[0, i]之间的整数const j = Math.floor(Math.random() * (i + 1));// 交换元素[array[i], array[j]] = [array[j], array[i]];}return array;
}

调用示例

// 示例1:基本使用
const arr1 = [1, 2, 3, 4, 5];
shuffleArray(arr1);
console.log(arr1); // 输出可能为:[3, 1, 5, 2, 4]// 示例2:包含不同类型元素
const arr2 = ['a', 'b', 'c', 1, 2, 3];
shuffleArray(arr2);
console.log(arr2); // 输出可能为:[2, 'a', 3, 'c', 1, 'b']// 示例3:空数组或单元素数组
const arr3 = [];
console.log(shuffleArray(arr3)); // 输出:[]const arr4 = [42];
console.log(shuffleArray(arr4)); // 输出:[42]

执行过程解析

以数组[1, 2, 3, 4]为例,执行过程如下:

  1. 初始数组:[1, 2, 3, 4]
  2. i = 3(最后一个元素):
    • 生成j = Math.floor(Math.random() * 4),假设j = 1
    • 交换array[3]和array[1]:[1, 4, 3, 2]
  3. i = 2:
    • 生成j = Math.floor(Math.random() * 3),假设j = 0
    • 交换array[2]和array[0]:[3, 4, 1, 2]
  4. i = 1:
    • 生成j = Math.floor(Math.random() * 2),假设j = 1
    • 交换array[1]和array[1](不改变):[3, 4, 1, 2]
  5. 最终结果:[3, 4, 1, 2]

复杂度分析

  • 时间复杂度:O(n),其中n是数组的长度。只需遍历一次数组。
  • 空间复杂度:O(1),只使用了常数级别的额外空间,实现了原地洗牌。

常见错误与注意事项

  1. 错误的循环条件

    // 错误示例:循环条件错误
    function wrongShuffle(array) {for (let i = 0; i < array.length; i++) {const j = Math.floor(Math.random() * array.length);[array[i], array[j]] = [array[j], array[i]];}return array;
    }
    

    这种实现会导致元素概率不均等,某些元素更可能停留在原位置。

  2. 随机索引范围错误

    • 错误写法Math.random() * i(缺少+1)
    • 正确写法Math.random() * (i + 1)(包含+1)
    • 错误原因
      • Math.random() 返回 [0, 1) 区间的浮点数(包含0但不包含1)
      • 当乘以 i 时,结果范围是 [0, i)(包含0但不包含i)
      • 使用 Math.floor() 后,得到的整数范围是 [0, i-1],永远无法取到i
    • 数学证明
      • 假设当前索引 i = 3(数组第4个元素)
      • 错误写法:Math.random() * 3 → 结果范围 [0, 3) → 取整后可能值:0, 1, 2(共3种可能)
      • 正确写法:Math.random() * 4 → 结果范围 [0, 4) → 取整后可能值:0, 1, 2, 3(共4种可能)
    • 导致后果
      • 最后一个元素(索引i)永远不会被选中交换
      • 数组末尾元素的位置固定,无法被随机化
      • 整体洗牌结果非均匀分布,出现系统性偏差
    • 可视化示例(i=3时):
      错误写法范围:[0,3) → 可能索引:0,1,2(缺少3)
      正确写法范围:[0,4) → 可能索引:0,1,2,3(完整覆盖)
      
  3. 数组越界:确保随机数生成在有效范围内

面试常见问题

问题1:为什么Fisher-Yates算法比简单交换更公平?

答:简单交换(如随机交换任意两个元素)会导致概率不均等,某些排列出现的概率更高。Fisher-Yates算法通过从后向前遍历并限制随机选择范围,确保每个元素有相等的概率被放置在任何位置。数学上可以证明,该算法能产生均匀分布的随机排列。

问题2:如何实现不修改原数组的洗牌算法?

答:可以先创建数组的副本,然后对副本执行洗牌操作:

function immutableShuffle(array) {const newArray = [...array];for (let i = newArray.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[newArray[i], newArray[j]] = [newArray[j], newArray[i]];}return newArray;
}

问题3:Math.random()有什么潜在问题?如何改进?

答:Math.random()生成的是伪随机数,在加密场景下不够安全。可以使用crypto API提供的更安全的随机数生成器:

function secureShuffle(array) {const newArray = [...array];for (let i = newArray.length - 1; i > 0; i--) {const j = Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] / (0xFFFFFFFF + 1) * (i + 1));[newArray[i], newArray[j]] = [newArray[j], newArray[i]];}return newArray;
}

问题4:如何测试洗牌算法的随机性?

答:可以通过大量重复试验,统计每个元素出现在每个位置的频率,看是否接近均匀分布:

function testShuffleAlgorithm(shuffleFunc, arrayLength = 4, trials = 100000) {const counts = Array.from({ length: arrayLength }, () => Array(arrayLength).fill(0));for (let i = 0; i < trials; i++) {const testArray = Array.from({ length: arrayLength }, (_, idx) => idx);const shuffled = shuffleFunc(testArray);shuffled.forEach((val, pos) => counts[val][pos]++);}// 计算每个位置的频率const frequencies = counts.map(row => row.map(count => count / trials));return frequencies;
}

理想情况下,每个频率应接近1/arrayLength。

http://www.dtcms.com/a/298793.html

相关文章:

  • Vue2 element cascader级联选择器懒加载编辑时回显数据
  • 【VLAs篇】06:从动作词元化视角谈VLA模型的综述
  • 异常(全)
  • which soffice soffice not found
  • Wordpress主题配置
  • 2025年7月24日·AI今日头条
  • KNN算法:从原理到实战全解析
  • Execel文档批量替换标签实现方案
  • day33:零基础学嵌入式之网络——TCP并发服务器
  • 基于javaweb的医院挂号系统
  • 动态规划解析:以最小花费爬楼梯为例
  • 纸板留声机:用ESP32和NFC打造会唱歌的复古装置
  • SeaweedFS深度解析(四):裸金属单机部署之配置文件启动master服务
  • IMU的精度对无人机姿态控制意味着什么?
  • [特殊字符] 第9篇:《SQL高阶 SELECT 技巧:DISTINCT、ORDER BY、LIMIT 全家桶》
  • ComfyUI中运行Wan 2.1工作流,电影级视频,兼容Mac, Windows
  • java微操
  • NLP验证自动化脚本优化
  • 硬核接线图+配置步骤:远程IO模块接入PLC全流程详解
  • 前端开发 Vue 状态优化
  • 多场景通用车辆计数算法助力暑期交通管理
  • Java从入门到精通!第十四天,重点!(反射)
  • 20250725-day22
  • Ivanti Endpoint Manager Mobile 远程命令执行漏洞复现(CVE-2025-4427)
  • 壁纸管理 API 文档
  • 测试实时性内核参数配置
  • 如何阅读字节码文件
  • Arrays 工具类详解
  • 在线事务型的业务、实时分析类业务、离线处理类型的业务
  • C语言————原码 补码 反码 (超绝详细解释)