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