.NET如何写正确的“抽奖”——数组乱序算法

数组乱序算法常用于抽奖等生成临时数据操作。就拿年会抽奖来说,如果你的算法有任何瑕疵,造成了任何不公平,在年会现场 code review时,搞不好不能活着走出去。

这个算法听起来很简单,简单到有时会拿它做面试题去考候选人,但它实际又很不容易,因为细节很重要,稍不留神就错了。

首先来看正确的做法:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r)
{var arr = data.ToArray();for (var i = arr.Length - 1; i > 0; --i){int randomIndex = r.Next(i + 1);T temp = arr[i];arr[i] = arr[randomIndex];arr[randomIndex] = temp;}return arr;
}

可以在 LINQPad6中,使用如下代码,测试随机打乱 0-10的数列,进行 50万条次模拟统计:

int[] Measure(int n, int maxTime)
{var data = Enumerable.Range(1, n);var sum = new int[n];var r = new Random();for (var times = 0; times < maxTime; ++times){var result = ShuffleCopy(data, r);for (var i = 0; i < n; ++i){sum[i] += result[i] != i ? 1 : 0;}}return sum;
}

然后可以使用 LINQPad特有的报表函数,将数据展示为图表:

Util.Chart(Measure(10, 50_0000).Select((v, i) => new { X = i, Y = v}), x => x.X, y => y.Y, Util.SeriesType.Bar).Dump();

运行效果如下(记住这是正确的示例):

可见 50万次测试中,曲线基本平稳, 0-10的分布基本一致,符合统计学上的概率相等。

再来看看如果未做任何排序的代码:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r) => data.ToArray();

曲线:

记住这两条曲线,它们将作为我们的参考曲线。

不然呢?

其实正确的代码每一个标点符号都不能错,下面我将演示一些错误的示例

错误示例1

多年前我看到某些年会抽奖中使用了代码(使用 JavaScript错误示例):

[0,1,2,3,4,5,6,7,8,9].sort((a, b) => Math.random() - 0.5)
// 或者
[0,1,2,3,4,5,6,7,8,9].sort((a, b) => Math.random() - Math.random())

返回结果如下:

(10) [8, 4, 3, 6, 2, 1, 7, 9, 5, 0]

看起来“挺”正常的,数据确实被打乱了,这些代码在 C#中也能轻易写出来:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r) => data.OrderBy(v => r.NextDouble() < 0.5).ToArray();

50万条数据统计结果如下:

可见,排在两端的数字几乎没多大变化,如果用于公司年会抽奖,那么排在前面的人将有巨大的优势

对比一下,如果在公司年会抽奖现场,大家 CodeReview时在这时“揭竿而起”,是不是很正常?

为什么会这样?

因为排序算法的本质是不停地比较两个值,每个值都会比较不止一次。因此要求比较的值必须是稳定的,在此例中明显不是。要获得稳定的结果,需要将随机数固定下来,像这样:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r) => data.Select(v => new { Random = r.NextDouble(), Value = v}).OrderBy(v => v.Random).Select(x => x.Value).ToArray();

此时结果如下(正确):

这种算法虽然正确,但它消耗了过多的内存,时间复杂度为整个排序的复杂度,即 O(N logN)

乱个序而已,肯定有更好的算法。

错误示例2

如果将所有值遍历一次,将当前位置的值与随机位置的值进行交换,是不是也一样可以精准打乱一个数组呢?

试试吧,按照这个想法,代码可写出如下:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r)
{var arr = data.ToArray();for (var i = 0; i < arr.Length; ++i){int randomIndex = r.Next(arr.Length);T temp = arr[i];arr[i] = arr[randomIndex];arr[randomIndex] = temp;}return arr;
}

运行结果如下:

有一点点不均匀,我可以保证这不是误差,因为多次测试结果完全一样,咱们拿数据说话,通过以下代码,可以算出所有值的变化比例:

Measure(10, 50_0000).Select(x => (x / 50_0000.0).ToString("P2")).Dump();

结果如下:

0 90.00%
1 90.54%
2 90.97%
3 91.29%
4 91.41%
5 91.38%
6 91.31%
7 90.97%
8 90.60%
9 90.01%

按道理每个数字偏离本值比例应该是 90.00%的样子,本代码中最高偏离值高了 1.41%,作为对比,可以看看正确示例的偏离比例数据:

0 90.02%
1 90.05%
2 90.04%
3 89.98%
4 90.05%
5 90.04%
6 90.07%
7 90.03%
8 89.97%
9 90.02%

可见最大误差不超过 0.05%,相比高达 1%的误差,这一定是有问题的。

其实问题在于随机数允许移动多次,如果出现多次随机,可能最终的值就不随机了,可以见这个示例,如果一个窗口使用这样的方式随机画点:坐标x两个随机数相加、坐标y仅一个随机数,示例代码如下:

// 安装NuGet包:FlysEngine.Desktop
using var form = new RenderWindow();
var r = new Random();
var points = Enumerable.Range(0, 10000).Select(x => (x: r.NextDouble() + r.NextDouble(), y: r.NextDouble())).ToArray();
form.Draw += (o, ctx) =>
{ctx.Clear(Color.CornflowerBlue);foreach (var p in points){ctx.FillRectangle(new RectangleF((float)p.x / 2 * ctx.Size.Width, (float)p.y * ctx.Size.Width, ctx.Size.Width / 100, ctx.Size.Height / 100), form.XResource.GetColor(Color.Black));}
};
RenderLoop.Run(form, () => form.Render(0, PresentFlags.None));

那么画出来的点是这个样子:

可见, 1万条数据, x坐标两个随机数相加之后,即使下方代码中除以 2了,结果已经全部偏向中间值了(和本例代码效果一样),而只使用一次的 y坐标,随机程度正常。想想也能知道,就像扔色子一样,两次扔色子平均是 6的机率远比平均是 3的机率低。

因此可以得出一个结论:随机函数不能随意叠加

错误示例3

如何每个位置的点只交换一次呢?没错,我们可以倒着写这个函数,首先来看这样的代码:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r)
{var arr = data.ToArray();for (var i = arr.Length - 1; i > 0; --i){int randomIndex = r.Next(i);T temp = arr[i];arr[i] = arr[randomIndex];arr[randomIndex] = temp;}return arr;
}

注意循环终止条件是 i>0,而不是直接遍历的 i>=0,因为 r.Next(i)的返回值一定是 小于i的,用 >=0没有意义,首先来看看结果:

用这个算法,每个数字出来都一定不是它自己本身,这合理吗?听起来感觉也合理,但真的如此吗?

假设某公司年会使用该算法抽奖,那结论就是第一个人不可能中奖,如果恰好你正好是抽奖名单列表的第一个人,你能接受吗?

据说当年二战时期德国的通讯加密算法,就是因为加密之前一定和原先的数据不一样,导致安全性大大降低,被英国破解的。

这个问题在于算法没允许和数字和自己进行交换,只需将 r.Next(i)改成 r.Next(i+1),问题即可解决。

总结

所以先回顾一下文章最初算法:

T[] ShuffleCopy<T>(IEnumerable<T> data, Random r)
{var arr = data.ToArray();for (var i = arr.Length - 1; i > 0; --i){int randomIndex = r.Next(i + 1);T temp = arr[i];arr[i] = arr[randomIndex];arr[randomIndex] = temp;}return arr;
}

然后重新体会一下它性感的测试数据( 10条数据,标准的 90%):

只有写完很多个不正确的版本,才能体会出写出正确的代码,每一个标点符号都很重要的感觉。

微信不能评论,各位可以前往博客园:https://www.cnblogs.com/sdflysha/p/20191103-shuffle-array-with-dotnet.html

喜欢的朋友请关注我的微信公众号:【DotNet骚操作】

.NET抽奖算法的设计和实现相关推荐

  1. [小设计] 之 抽奖算法

    文章目录 一.概述 二.抽奖算法 (1)所有数 `random` (2)抽样算法 (3)`shuffle` 算法 (4)分区算法 一.概述 应用场景:抽奖活动 开奖,可以是手动开奖和定时开奖. 抽奖这 ...

  2. php整点抽奖活动,php,抽奖算法_PHP抽奖算法思路?,php,抽奖算法,算法 - phpStudy

    PHP抽奖算法思路? 网上有个烂大街的算法,具体请看链接 大概原理是: 1,生成一个随机数: 2,循环对比第N个奖品的概率: 3,确定奖品: 如果按照这个算法,主观上是抽一次奖,但客观上,把每个奖品都 ...

  3. c语言排序教学过程,C语言中冒泡排序算法教学设计

    排序是计算机科学中一项重要的技术,其应用范围广.使用频率高,因此对于排序算法的研究一直是计算机专家的重点.高校的程序设计类课程也将排序算法作为重难点进行教学. 1常见的几种排序算法 1.1算法的基本概 ...

  4. 抽奖算法-指定概率的随机

    抽奖模型 普通概率模型 普通概率模型是最常用的一种模型,但是在游戏运营过程中的确发现很多小白玩家不能正确理解--他们认为中奖率 10% 的设定等同于抽 10 次肯定会中一次.这显然是错误的,普通概率模 ...

  5. 转盘抽奖php,使用PHP实现转盘抽奖算法案例解析

    这次给大家带来使用PHP实现转盘抽奖算法案例解析,使用PHP实现转盘抽奖算法的注意事项有哪些,下面就是实战案例,一起来看一下. 流程: 1.拼装奖项数组 2.计算概率 3.返回中奖情况 代码如下: 中 ...

  6. 域对抗自适应算法的设计、不足与改进(Domain Adversarial Learning)

    ©作者 | 江俊广 单位 | 清华大学 研究方向 | 迁移学习 本文主要介绍域自适应(Domain Adaptation)中的对抗域自适应方法(Domain Adversarial Learning) ...

  7. php 打乱数组顺序_PHP实现大转盘抽奖算法

    php中文网最新课程 每日17点准时技术干货分享 本文通过具体的实例向大家介绍了PHP语言实现大转盘抽奖算法,希望对大家学习PHP抽奖有所帮助. 流程: 1.拼装奖项数组: 2.计算概率: 3.返回中 ...

  8. java实现抽奖游戏_Java实现游戏抽奖算法

    常用抽奖算法对比 基础的游戏抽奖算法通常要求实现在指定奖品的集合中,每个奖品根据对对应概率进行抽取.个人了解的主要有以下几中抽奖算法: 随机数一一对应 算法思想 这种算法思想最为简单.将n个奖品编号0 ...

  9. php中奖概率算法,可用于刮刮卡,大转盘等抽奖算法

    php中奖概率算法,可用于刮刮卡,大转盘等抽奖算法.用法很简单,代码里有详细注释说明,一看就懂 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...

最新文章

  1. python3 cPickle
  2. boost::container_hash模块实现哈希图
  3. 织梦自定义图片字段和缩略图一样_织梦图片集模型自定义图片字段调用
  4. delay 芯片时序output_【第二章 STA概念 上】静态时序分析圣经翻译计划
  5. Python实现抓取CSDN博客首页文章列表
  6. 04.Android之动画问题
  7. 【转】如何解决:Android中 Error generating final archive: Debug Certificate expired on 10/09/18 16:30 的错误...
  8. 植入式医疗电子设备供电电源原理与设计
  9. 《Go圣经》章三:基本数据
  10. get请求中文乱码问题解决
  11. Vue实战 POS系统
  12. Java设计模式超级详细分析(包含代码)
  13. 西门子PLC中DB与DI有什么区别
  14. Winform从入门到精通(17)——PictureBox(史上最全)
  15. android sip协议栈,基于Android平台及SIP协议的软电话系统的研究
  16. pythonnet调用python tsne算法
  17. 什么是0day漏洞,1day漏洞和nday漏洞?
  18. 快速学会!关于Android程序员最近的状况,已拿offer入职
  19. 史诗级学术骗局!一博士狂编 200 多篇论文,被揭发后畏罪自杀....
  20. 前端基础(八)_盒子模型(标准盒子模型和怪异盒子模型)

热门文章

  1. UnicodeDecodeError: 'utf8' codec can't decode byte 0xc9 in position 0: inval
  2. java毕业生设计学校旧书交易网站计算机源码+系统+mysql+调试部署+lw
  3. 视频合并分割软件如何合并视频
  4. 【华为OD机试真题 python】几何平均值最大子数组【2023 Q1 | 100分】
  5. 江哥html阶段测试,来自江哥的H5+跨平台开发实战课程 跟着江哥从零狂虐H5+跨平台开发视频教程...
  6. cornerstone出现 Unable to connect to a repository at URL
  7. 与狗尾草一起探寻人机交互的更多可能性|白洞战报
  8. 并发型服务器响应方式,基于Java NIO 开发高性能并发型服务器程序的研究
  9. WebSocket实现一个聊天室
  10. 应用化工技术学计算机不,应用化工技术专业主要学什么?