最近在做移動端的營銷活動,其中包含刮刮卡、大轉盤等小游戲,對於用戶來說他們不關心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的朋友們可以留個言留個腳印。