最近在做移动端的营销活动,其中包含刮刮卡、大转盘等小游戏,对于用户来说他们不关心Code只关心我是否中奖了,之前也在群里看到有人要概率的“算法”或者说是计算工具类。
ps:这里不得不提一下,每当自己在做什么东西的时候总会在一些地方发现相似的需求或者文章,来源有很多比如:cnblogs、QQ群等各种渠道,这不刚刚还有人发表随机数相关的文章,哈哈可能是我运气好。
营销活动核心——我是不是中奖了呢?
是不是中奖都有一个概率和巧合性那么肯定少不了随机数了,说到随机数各位Coder们肯定想到了Random这个类,是的这一次的概率计算的实现也是基于随机数的。
下面我们来看概率服务接口:
IProbabilityService /// <summary>
/// 一个抽象的概率服务。
/// </summary>
public interface IProbabilityService
{
/// <summary>
/// 是否命中。
/// </summary>
/// <param name="probabilityPercentage">概率百分比(如概率为30%请传入30)。</param>
/// <param name="getRandomNumber">获取随机数的委托(一般为Random.NextDouble())。</param>
/// <returns>是否命中。</returns>
bool IsHit(double probabilityPercentage, Func<double> getRandomNumber);/// <summary>
/// 是否命中。
/// </summary>
/// <param name="probabilityPercentage">概率百分比(如概率为30%请传入30)。</param>
/// <param name="hasHitCount">命中次数。</param>
/// <param name="noHitCount">没有命中的次数。</param>
/// <param name="getRandomNumber">获取随机数的委托(一般为Random.NextDouble())。</param>
/// <returns>是否命中。</returns>
bool IsHit(double probabilityPercentage, int hasHitCount, int noHitCount, Func<double> getRandomNumber);
}
接口十分的简单一起有两个方法,第一个方法比较纯粹的计算概率,而第二个则添加了一些修正概率所需的数据。
为什么需要“getRandomNumber”参数,而不直接在内部使用Random?
这边就需要引入“随机数是骗人的,.Net、Java、C为我作证”今天的热乎文章了,内部使用Random有很多的不确定性,而且不易于扩展所以这边提供了一个委托提供随机数,而概率服务本身只专注于计算,如果非要高大上点就引入设计原则——单一职责。
服务实现
ProbabilityServiceinternal sealed class ProbabilityService : IProbabilityService
{
#region Implementation of IProbabilityService/// <summary>
/// 是否命中。
/// </summary>
/// <param name="probabilityPercentage">概率百分比(如概率为30%请传入30)。</param>
/// <param name="getRandomNumber">获取随机数的委托(一般为Random.NextDouble())。</param>
/// <returns>是否命中。</returns>
public bool IsHit(double probabilityPercentage, Func<double> getRandomNumber)
{
//如果概率大等于100则每次都命中。
if (probabilityPercentage >= 100)
return true;//得到概率的百分比。
probabilityPercentage = probabilityPercentage / 100;return InternalIsHit(probabilityPercentage, getRandomNumber);
}/// <summary>
/// 是否命中。
/// </summary>
/// <param name="probabilityPercentage">概率百分比(如概率为30%请传入30)。</param>
/// <param name="hasHitCount">命中次数。</param>
/// <param name="noHitCount">没有命中的次数。</param>
/// <param name="getRandomNumber">获取随机数的委托(一般为Random.NextDouble())。</param>
/// <returns>是否命中。</returns>
public bool IsHit(double probabilityPercentage, int hasHitCount, int noHitCount, Func<double> getRandomNumber)
{
//如果概率大等于100则每次都命中。
if (probabilityPercentage >= 100)
return true;//得到概率的百分比。
probabilityPercentage = probabilityPercentage / 100;//得到总计算次数。
var totalCount = (double)(hasHitCount + noHitCount);//得到当前命中的概率。
var currentProbability = hasHitCount / totalCount;//如果当前命中的概率大于传入的概率则不会命中。
if (currentProbability > probabilityPercentage)
return false;return InternalIsHit(probabilityPercentage, getRandomNumber);
}#endregion Implementation of IProbabilityService
#region Private Method
/// <summary>
/// 是否命中。
/// </summary>
/// <param name="probabilityPercentage">不需要处理的百分比(介于 0.0 和 1.0 之间的数)。</param>
/// <param name="getRandomNumber">获取随机数的委托(一般为Random.NextDouble())。</param>
/// <returns>是否命中。</returns>
private static bool InternalIsHit(double probabilityPercentage, Func<double> getRandomNumber)
{
//得到一个随机数。
var randomNumber = getRandomNumber();if (randomNumber < 0 || randomNumber >= 1)
throw new ArgumentException("随机数必须是一个介于 0.0 和 1.0 之间的数。");//取后15位
const int places = 15;//精简小数位,提升概率准确性。
randomNumber = GetNumber(randomNumber, places);return probabilityPercentage > randomNumber;
}/// <summary>
/// 精简数字的小数位。
/// </summary>
/// <param name="number">数字。</param>
/// <param name="places">小数位。</param>
/// <returns>精简小数位后的数字。</returns>
private static double GetNumber(double number, int places)
{
//精简小数位,提升概率准确性。
return Math.Round(number, places);//该方法会提高准确性但会影响性能,适用于高精度场景。
/*var str = number.ToString(CultureInfo.InvariantCulture);
if (!str.Contains("."))
return number;
var t = str.Split(‘.‘);
number = double.Parse(t[0] + "." + string.Join("", t[1].Take(places)));
return number;*/
}#endregion Private Method
}
代码有较详尽的注释这边不再说明了。
有运行Demo吗?
当然,这是我的一贯作风。
运行结果
第一行为接口的第一个方法(纯粹的概率计算),第二个行为接口的第二个方法(带简单修正)。
Code
Programinternal class Program
{
private static void Main()
{
var random = new Random();//每一次执行的测试次数(当前为10w次)。
const int totalCount = 100000;//概率百分比。
double probability;#region GetProbability By Console "probability"
var promptMessage = "请输入概率百分比,如30%:";
string probabilityString;
do
{
Console.WriteLine(promptMessage);
promptMessage = "请输入一个正确的概率百分比:";
probabilityString = Console.ReadLine();
if (probabilityString != null && probabilityString.EndsWith("%"))
probabilityString = probabilityString.TrimEnd(‘%‘);
} while (!double.TryParse(probabilityString, out probability));#endregion GetProbability By Console "probability"
Console.WriteLine("测试次数设定为:{0},概率设定为:{1}%", totalCount, GetPercentage(probability, 100));
Console.WriteLine("==================================================");IProbabilityService probabilityService = new ProbabilityService();
while (true)
{
RunTest(totalCount, (i, hitCount) => probabilityService.IsHit(probability, random.NextDouble));
RunTest(totalCount, (i, hitCount) => probabilityService.IsHit(probability, hitCount, i - hitCount, random.NextDouble));
Console.ReadLine();
}
}/// <summary>
/// 执行测试。
/// </summary>
/// <param name="totalCount">测试次数。</param>
/// <param name="hit">是否命中委托。</param>
private static void RunTest(int totalCount, Func<int, int, bool> hit)
{
//总命中次数。
var hitCount = 0;for (var i = 0; i < totalCount; i++)
{
var isHit = hit(i, hitCount);
if (isHit)
hitCount++;
}//概率百分比。
var percentage = GetPercentage(hitCount, totalCount);
Console.WriteLine("总次数:{0},命中次数:{1},概率{2}%", totalCount, hitCount, percentage);
}/// <summary>
/// 获取百分比。
/// </summary>
/// <param name="number1">数字1。</param>
/// <param name="number2">数字2。</param>
/// <returns>百分比。</returns>
private static double GetPercentage(double number1, double number2)
{
return (number1 / number2) * 100;
}
}
Demo下载:http://pan.baidu.com/s/1gdmnH31
写在最后
已经写了一些“散文”做为锻炼,之后准备写一个系列挑战一下,不过最近在做项目,等手上的模块做完之后,开始着手针对 Orchard
Framework
写一个刨析系列,当然中间可能会穿插一些小文章,喜欢Orchard的朋友们可以留个言留个脚印。