为什么这个简单的洗牌算法(通过 random() 排序)存在偏差?

2024-07-04

In 这个线程 https://stackoverflow.com/a/18650169/17102262我们看到这个简单而漂亮的算法来洗牌数组:

function shuffle<T>(array: T[]): T[] {
  return array.sort(() => Math.random() - 0.5);
}

我们可以看到评论说这个算法有偏见。但我制作了一个简单的脚本来创建数组最后一个元素在洗牌后结束的索引的经验概率分布:

function shuffle(array) {
  return array.sort(() => Math.random() - 0.5);
}

function generateDistribution(iterations = 10_000, arrayLength = 10) {
  const testArray = Array(arrayLength - 1).fill("test");
  const testTarget = "target";
  testArray.push(testTarget);

  const results = {};

  for (let index = 0; index < iterations; index++) {
    countTargetPosition();
  }

  return generateResult();

  function countTargetPosition() {
    const shuffled = shuffle(testArray);
    shuffled.forEach((value, index) => {
      if (value === testTarget) {
        results[index] = results[index] + 1 || 1;
      }
    });
  }

  function generateResult() {
    return Object.entries(results).map(([index, count]) => {
      return {
        [index]: count / iterations,
      };
    });
  }
}

const result = generateDistribution()
document.write(`<h1>Result</h1>`)

document.write(JSON.stringify(result))

我们期望无偏算法具有均匀分布,并且结果非常接近该分布,即使对于具有 100 个元素的数组也是如此。那么为什么这个算法会有偏差呢?


JavaScript 没有指定特定的算法sort,并且根据所使用的特定排序算法,这种洗牌算法可能会给出非常有偏差的结果。下面,我描述了一些简单的、众所周知的排序算法,这些算法给出了非常有偏差的结果;我证明 Firefox 和 Chrome 对于长度为 4 的数组都给出了非常有偏差的结果;我给出了一个一般性的理由来解释为什么any排序算法会给出有偏差的结果(尽管不一定as这些明确的例子有偏见)。


示例#1 —选择排序 https://en.wikipedia.org/wiki/Selection_sort.在选择排序中,我们首先找到最小的元素并将其放在索引 0 处,然后找到第二小的元素并将其放在索引 1 处,依此类推。需要注意的是,使用比较函数() => Math.random() - 0.5,比较的每个参数都有相同的机会被视为“较少”。因此,如果您通过迭代数组并将每个元素与之前的最小元素进行比较来找到最小元素,那么您有 50% 的机会认为最后一个元素是最小的,有 25% 的机会认为最后一个元素是最小的。认为倒数第二个元素是最少的,您有 12.5% 的机会认为倒数第三个元素是最少的,等等,因此给出了哪个元素先出现的有偏分布。


示例 2 —插入排序 https://en.wikipedia.org/wiki/Insertion_sort.在插入排序中,我们通过依次取出每个元素并将其插入到该排序部分中的正确位置(将所有较大的元素移动一位以为其腾出空间)来构建数组的“已排序”部分。这意味着最后一个元素有 50% 的机会被视为最少,25% 的机会被视为第二少,12.5% 的机会被视为第三少,等等。


示例 #3 和 #4 — Firefox 和 Chrome 使用的四元素数组。

现在,实际上,我不期望任何实施sort to use exactly选择排序或插入排序,因为还有其他算法对于大输入更有效。但复杂的现代排序算法,例如Timsort https://en.wikipedia.org/wiki/Timsort,结合多种不同的排序算法,根据输入(或部分输入,因为它们可以以复杂的方式组合这些算法)的大小和特征在它们之间进行自适应选择。因此,作为实验,我在数组上尝试了这种洗牌算法[1, 2, 3, 4]- 一个足够短的数组,看起来很可能sort实现可能只对整个数组使用插入排序。

这是我使用的代码:

const counts = {};
for (let i = 0; i < 1_000_000; ++i) {
  const permutation = [1, 2, 3, 4].sort(() => Math.random() - 0.5).join('');
  counts[permutation] = (counts[permutation]||0) + 1;
}

const result = [];
for (let permutation in counts) {
  result.push(permutation + ': ' + counts[permutation]);
}

result.join('\n')

我在 Firefox 和 Chrome 中都尝试过这个。

在 Firefox 中,我得到了这样的结果:

1234: 125747
1243: 62365
1324: 62299
1342: 31003
1423: 31320
1432: 15635
2134: 125380
2143: 62216
2314: 62615
2341: 31255
2413: 31509
2431: 15608
3124: 62377
3142: 31166
3214: 62194
3241: 31293
3412: 15631
3421: 15782
4123: 31056
4132: 15672
4213: 31231
4231: 15319
4312: 15727
4321: 15600

这与我对插入排序的期望不符,所以它必须做一些不同的事情,但无论如何,它显示出非常明显的偏差。有些排列发生的时间为 1/64(一百万次中有 15,625 次,加上/减去随机噪声),有些排列发生的时间为 1/32 (31,250),有些排列的发生时间为 1/16 (62,500),有些排列发生的时间为 1/16 (62,500)。发生次数为 1/8 (125,000);因此,某些排列的出现频率是其他排列的八倍。

在 Chrome 中,我得到了这样的结果:

1234: 187029
1243: 62380
1324: 15409
1342: 15679
1423: 62476
1432: 15368
2134: 31280
2143: 31291
2314: 15683
2341: 15482
2413: 31482
2431: 15732
3124: 15786
3142: 15692
3214: 47186
3241: 47092
3412: 15509
3421: 46600
4123: 62825
4132: 15595
4213: 31091
4231: 15763
4312: 15624
4321: 171946

which also与我对插入排序的期望不符,并且比 Firefox 中的分布要复杂一些(我想我在那里看到了一些 3/16(187,500)和 3/64(46,875)?),但是实际上,偏差甚至更大,最常见的排列和最不常见的排列之间存在十二倍的差异。


Example #5 — any deterministic sorting algorithm. I've given various examples above of fairly extreme biases; but really, any sorting algorithm would be expected to produce some bias, because if the algorithm does worst-case k comparisons on an array of length n, and each comparison has a 50–50 split, then the probability of any given permutation has to be a multiple of 1/2k, whereas an unbiased shuffler has to give each permutation the probability 1/n!, which won't be a multiple of 1/2k if n ≥ 3 (because then n! will be a multiple of 3).

也就是说,我应该承认这些偏见可能足够小,以至于无关紧要。毕竟,即使1.0 / 3.0不计算exactly1/3,而是将其四舍五入为二进制近似值。更直接的相关性是,一个典型的实现Math.random()保存 64 或 128 位内部状态,这意味着它甚至没有 21 位!或 35!不同的内部状态,这意味着no使用的算法Math.random()打乱 21 个或 35 个或更多元素的数组可以possibly以非零概率产生每个排列。所以我想some偏见是不可避免的!


即使您使用的是sort给出您考虑的结果的实施够好了,没有理由这样做,因为费舍尔-耶茨洗牌 https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle编码简单and比任何基于比较的排序算法都要快。


但我制作了一个简单的脚本来创建数组最后一个元素在洗牌后结束的索引的经验概率分布:[...]

请注意,可能存在更微妙的偏差,因此并非所有排列的可能性都相同even if最后一个元素同样有可能出现在任何位置。即使sort实现保持固定,您需要在依赖此改组算法给出无偏差结果之前进行更彻底的分析(可能包括查看其源代码)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么这个简单的洗牌算法(通过 random() 排序)存在偏差? 的相关文章

随机推荐

  • 可编辑列表视图

    我希望在 C winforms 应用程序中创建一个可编辑的 ListView 用户可以双击单元格来更改其内容 如果有人能为我提供一些指导和 或示例 那就太好了 我不打算使用任何商业产品 你问错了问题 ListView 不是正确的控件 使用数
  • 我应该使用内存数据库而不是模拟我的存储库吗?

    我喜欢在测试时使用内存数据库 例如 SQLite 的想法 而不是为我的存储库创建模拟 这样我还可以测试我的存储库的代码 而不会出现任何性能问题 该策略的优点和缺点是什么 如果我使用 ORM 我通常倾向于在内存数据库中使用 sqlite 来测
  • 如何将 pandas DataFrame 保存到 Excel 文件?

    我正在尝试从 Web 源加载数据并将其保存为 Excel 文件 但不知道该怎么做 我应该怎么办 import requests import pandas as pd import xmltodict url https www kstan
  • Phonegap 应用程序性能与本机应用程序性能

    我们正在考虑构建一个条形码扫描应用程序 我们正在考虑使用 PhoneGap 但我们唯一担心的是速度 该应用程序要做的只是扫描条形码并检查服务器以查看其是否有效 该应用程序非常频繁地使用相机通过图像扫描条形码 我的主要问题是 通过phoneg
  • 从Matlab到C++特征矩阵运算——向量归一化

    将一些 Matlab 代码转换为 C 问题 如何在 C 中 连接矩阵中的两个向量 已经找到解决办法了 标准化每个数组 pts col 除以它的第三个值 1和2的Matlab代码 1 A 3x1 vector d0 d1 double B d
  • 是否可以将附件视图的顺序更改为 drupal 页面视图?

    我有一个页面显示的视图 其中基本上包含内容 A 该内容存在于一列中 此外 我还有另外 6 个附件显示 在另一列中显示内容 B C D E 等 是否可以修改附加视图的显示顺序 现在我已将它们全部设置为附加在内容 A 页面之后 我不是 100
  • 如何在列之间留出空间?

    我有一个文本文件 如下所示 我想在第五列中的字符和数字之间留一个空格 我怎样才能用 awk 做到这一点 cxe 911 bv heg A1029 53 030 bvf 912 cv lya A1030 51 99 Desired outpu
  • 将 HTML 表格导出到 Excel 在 IE 中不起作用

    将 HTML 表格导出到 Excel 在 Chrome 和 Firefox 中工作正常 但在 Internet Explorer 10 中不起作用 var tableToExcel function var uri data applica
  • 定位彩条 - Matplotlib

    我有一个合并两个数据的图 鉴于此 我必须显示两个不同的颜色条 每个数据一个 我目前正在绘制数据如下 plt figure Data 1 fig plt imshow data1 interpolation nearest cmap bina
  • C# 奇怪的精度丢失 int 到浮动和向后

    当尝试从 int 转换为 float 并向后转换时 会发生奇怪的事情 原始示例代码 整数值 28218681 val 28218681 浮点 fVal 浮点 val fVal 2 821868E 07 int val2 int fVal v
  • PHP SQLSRV:sqlsrv_query() 是否可以正确地准备 select 语句?

    TL DR Does sqlsrv query 做同样的工作select陈述比sqlsrv prepare and sqlsrv execute 关于准备好的陈述 做什么 我怎样才能做一个安全的select陈述 一点历史 我是 PHP 开发
  • Opencv 3D 来自立体对中的点

    OpenCV 中是否有一个简单的函数可以从立体相机对中获取对象的 3D 位置和姿态 我用棋盘校准了相机和基线 我现在想要获取一个已知的物体 就像同一个棋盘一样 在它自己的坐标中具有已知的 3D 点 并找到真实世界的位置 在相机坐标中 有一些
  • 在 Visual Studio 中启动 Web API 项目的最简洁方法(无视图)

    删除 Visual Studio API 模板通常附带的所有额外内容 如视图和其他如果您只想拥有 WebAPI 服务则不需要的内容 的最干净方法是什么 我假设 VIEWS 文件夹用于 MVC 视图 也许我错了 它需要有一个正在运行的 API
  • Python就地写入文件任意位置

    我正在尝试在 python 中就地编辑文本文件 它非常大 因此无法将其加载到内存中 我打算替换我在里面找到的逐字节字符串 with f as open filename txt r b if f read 8 01234567 f seek
  • UIWebView 中自动填充用户名和密码

    我正在为我做一个非常简单的应用程序 当我启动这个应用程序时 它只会将我带到这个网页https social tre it expert https social tre it expert 我想自动登录 那么有没有办法自动填写用户名和密码
  • Java中等待的最佳方式

    我有一个应用程序需要等待一段未知的时间 它必须等到服务器完成几个数据字段的填充 服务器的 API 为我提供了一种请求数据的方法 非常简单 服务器的 API 还提供了一种接收回数据的方法 一次接收一个字段 它没有告诉我所有字段何时完成填充 等
  • 如何按字母顺序对单键字典数组进行排序?

    我想对 Facebook 的 Graph API 返回的好友列表进行排序 排序后的结果需要是按好友名字的字母顺序排列 name Joe Smith id 6500000 name Andrew Smith id 82000 name Dor
  • ActiveRecord 触摸导致死锁

    我的应用程序使用touch广泛地利用 Rails 的模板缓存系统 当批量中的许多不同对象之间创建许多关系时 我的应用程序会执行某种类型的工作 有时 其中一些工作会导致级联touches 导致死锁 我可以针对这种情况进行编码 我经常看到这种情
  • Instagram 如何命名文件

    Instagram 如何命名文件 文件名是随机的吗 基于某种哈希 或者它们有什么意义吗 例如 http distilleryimage2 instagram com 21a9ca729bf511e2985c22000a1f9ad3 7 jp
  • 为什么这个简单的洗牌算法(通过 random() 排序)存在偏差?

    In 这个线程 https stackoverflow com a 18650169 17102262我们看到这个简单而漂亮的算法来洗牌数组 function shuffle