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

.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();

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

640?wx_fmt=png

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

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

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

曲线: 

640?wx_fmt=png

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

不然呢?

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

错误示例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条数据统计结果如下: 

640?wx_fmt=png

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

对比一下,如果在公司年会抽奖现场,大家 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();

此时结果如下(正确): 

640?wx_fmt=png

这种算法虽然正确,但它消耗了过多的内存,时间复杂度为整个排序的复杂度,即 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;	
}

运行结果如下: 

640?wx_fmt=png

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

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));

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

640?wx_fmt=png

可见, 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没有意义,首先来看看结果: 

640?wx_fmt=png

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

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

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

这个问题在于算法没允许和数字和自己进行交换,只需将 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%): 

640?wx_fmt=png

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

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

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

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313137.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

maximum mean discrepancy

http://blog.csdn.net/a1154761720/article/details/51516273 MMD&#xff1a;maximum mean discrepancy。最大平均差异。最先提出的时候用于双样本的检测&#xff08;two-sample test&#xff09;问题&#xff0c;用于判断两个分布p和q是否相同。它的基本假设是&#xff1a;如…

FineUICore基础版部署到docker实战

文 | 蒙古海军司令 合作者FineUI用了好多年&#xff0c;最近出了FineUICore版本&#xff0c;一直没时间是试一下docker&#xff0c;前几天买了一个腾讯云服务器&#xff0c;1核2g&#xff0c;装了centos7.6&#xff0c;开始的时候主要是整个个人博客&#xff0c;在腾讯云安装了…

2019全球Microsoft 365开发者训练营(北京站)

Microsoft365介绍&#xff1a;Microsoft365不仅仅是Office 365&#xff0c;它还包括Windows 10操作系统&#xff0c;以及诸多企业级移动和安全应用。它是一套可用于从小型到集团化企业的办公、协作、沟通的企业信息化解决方案。在2017年7月11日举行的Inspire年度合作伙伴大会上…

caffe/common.cu error: function atomicadd has already been defined

http://blog.csdn.NET/houqiqi/article/details/46469981 1, 下载matio(http://sourceforge.NET/projects/matio/) 2,&#xff0c;安装 $ tar zxf matio-X.Y.Z.tar.gz $ cd matio-X.Y.Z $ ./configure $ make $ make check $ make install sudo ldconfig (如果不执行&#x…

微软备战 RPA 市场,Power Platform,Ready GO!

最大赌注就在刚刚&#xff0c;微软在 Microsoft Ignite 2019 大会上&#xff0c;首席执行官萨蒂亚纳德拉&#xff08;Satya Nadella&#xff09;宣布了 Microsoft Power Platform 新平台的发布&#xff0c;并且说到&#xff1a;在与Azure合作方面&#xff0c;微软365&#xff0…

C# 8 新特性 - 只读struct成员

从C# 8开始&#xff0c;我们可以在struct的成员上使用readonly修饰符。 为struct的成员添加readonly修饰符就表示告诉编译器和开发者该成员不可以修改struct的状态。 看下面这个例子&#xff1a; 这里的ToString()方法不会修改Point这个struct的状态&#xff0c;所以我们可以在…

.NET Core 3.0 中间件 Middleware

中间件官网文档解释&#xff1a;中间件是一种装配到应用管道以处理请求和响应的软件 每个中间件&#xff1a;选择是否将请求传递到管道中的下一个组件。可在管道中的下一个组件前后执行工作。使用 IApplicationBuilder 创建中间件管道ASP.NET Core 请求管道包含一系列请求委托&…

重磅!微软发布 Visual Studio Online:Web 版 VS Code + 云开发环境

今天&#xff08;北京时间 2019 年 11 月 4 日&#xff09;&#xff0c;在 Microsoft Ignite 2019 大会上&#xff0c;微软正式发布了 Visual Studio Online 公开预览版&#xff01;概览Visual Studio Online 提供了由云服务支撑的开发环境。无论是一个长期项目&#xff0c;或是…

Ubuntu Linux将支持所有树莓派设备

Canonical 近期公开了对 Raspberry Pi 4 的支持计划&#xff0c;并表示将支持所有 Raspberry Pi 设备。随着 Ubuntu Server 19.10 版本的发布&#xff0c;Canonical 宣布正式支持 Raspberry Pi 4&#xff0c;Raspberry Pi 4 性能强大&#xff0c;但成本较低&#xff0c;可以在边…

面试官:你连RESTful都不知道我怎么敢要你?

加个“星标★”&#xff0c;每天11.50&#xff0c;好文必达全文约4000字&#xff0c;预计阅读时间8分钟面试官&#xff1a;了解RESTful吗&#xff1f;01 前言回归正题&#xff0c;看过很多RESTful相关的文章总结&#xff0c;参齐不齐&#xff0c;结合工作中的使用&#xff0c;非…

深入理解.NET Core的基元(二) - 共享框架

原文&#xff1a;Deep-dive into .NET Core primitives, part 2: the shared framework作者&#xff1a;Nate McMaster[1] 译文&#xff1a;深入理解.NET Core的基元&#xff08;二&#xff09; - 共享框架 作者&#xff1a;Lamond Lu本篇是之前翻译过的《深入理解.NET Core的基…

net core WebApi——使用xUnits来实现单元测试

前言从开始敲代码到现在&#xff0c;不停地都是在喊着记得做测试&#xff0c;记得自测&#xff0c;测试人员打回来扣你money之类的&#xff0c;刚开始因为心疼钱&#xff08;当然还是为了代码质量&#xff09;&#xff0c;就老老实实自己写完自己跑一遍&#xff0c;没有流程没有…

python利用opencv标注bounding box

http://blog.csdn.net/xieqiaokang/article/details/60780608 1. 函数 用 OpenCV 标注 bounding box 主要用到下面两个工具——cv2.rectangle() 和 cv2.putText()。用法如下&#xff1a; # cv2.rectangle() # 输入参数分别为图像、左上角坐标、右下角坐标、颜色数组、粗细 cv2…

微软发布 SQL Server 2019 新版本

2019 年 11 月 4 日&#xff0c;微软在美国奥兰多举办的 Ignite 大会上发布了关系型数据库 SQL Server 的新版本。与之前版本相比&#xff0c;新版本的 SQL Server 2019 具备以下重要功能&#xff1a;在 Linux 和容器中运行的能力&#xff0c;连接大数据存储系统的 PolyBase 技…

AdminLTE 3.0发布了

点击蓝字关注我们前言在11月2日&#xff0c;作者正式发布了AdminLTE 3.0版本。该版本基于Bootstrap 4.x。使用Bootstrap 4.x的小伙伴可以愉快的使用AdminLTE。GithubAdminLTE是一个完全响应的管理模板。基于Bootstrap 4框架。高度可定制且易于使用。适合从小型移动设备到大型台…

这位优秀的.NET开发者是怎样炼成的?

本文来自DotNET技术圈作者&#xff1a;邹溪源一&#xff0c;社区的小圈子今年3月的一次技术交流活动上&#xff0c;那是我们.NET技术社区第一次组织线下活动&#xff0c;由于没什么经验&#xff0c;所以活动组织得比较仓促&#xff0c;内容也比较一般&#xff0c;效果还是有点欠…

求知无限,刷新.NET 中国社区

2019 Microsoft Ignite The Tour 2020年1月13日至14日深圳会展中心举办&#xff0c;今年的大会是免费的哦&#xff0c;所以也很火爆&#xff0c;我们为您开通专属报名渠道&#xff0c;,扫下方二位码 请在注册时务必填写RSVPCode: MITTCE。大会全面解锁微软黑科技&#xff1a;&g…

使用ASP.NET Core 3.x 构建 RESTful API - 1. 开始

以前写过ASP.NET Core 2.x的REST API文章&#xff0c;今年再更新一下到3.0版本。预备知识&#xff1a;ASP.NET Core 和 C# 工具&#xff1a;Visual Studio 2019最新版&#xff08;VSCode、VS for Mac&#xff0c;Rider等也凑合&#xff09;&#xff0c;POSTMAN Web API Web API…

.NET Core 3.1 编写混合 C++ 程序

前言随着 .NET Core 3.1 的第二个预览版本发布&#xff0c;微软正式将 C/CLI 移植到 .NET Core 上&#xff0c;从此可以使用 C 编写 .NET Core 的程序了。由于目前仅有 MSVC 支持编译此类混合代码&#xff0c;并且由于涉及到非托管代码&#xff0c;因此 C/CLI 目前不能跨平台&a…

在ASP.NET Core中编写合格的中间件

这篇文章探讨了让不同的请求去使用不同的中间件&#xff0c;那么我们应该如何配置ASP.NET Core中间件&#xff1f;其实中间件只是在ASP.NET Core中处理Web请求的管道。所有ASP.NET Core应用程序至少需要一个中间件来响应请求&#xff0c;并且您的应用程序实际上只是中间件的集合…