php生成隨機(jī)紅包算法(1.3s生成30W個(gè)隨機(jī)紅包)

一生蚁、背景介紹

前一陣公司業(yè)務(wù)有一個(gè)生成紅包的需求噩翠,分為固定紅包和隨機(jī)紅包兩種,固定紅包沒什么好說的了邦投,隨機(jī)紅包要求指定最小值伤锚,和最大值,必須至少有一個(gè)最大值志衣,可以沒有最小值屯援,但任何紅包不能小于最小值。
  以前從來沒做過這方面念脯,有點(diǎn)懵B狞洋,于是去百度了一番,結(jié)果發(fā)現(xiàn)能找到的紅包算法都有各種各樣的bug和二,要么會(huì)算出負(fù)值徘铝,要么超過最大值,所以決定自己擼一套出來。

<img src="http://upload-images.jianshu.io/upload_images/3389468-9be30da7403eeb5a.png" >

二惕它、基本思路

在隨機(jī)數(shù)生成方面怕午,我借鑒了這位博主@悲慘的大爺的思路:

原文:比如要把1個(gè)紅包分給N個(gè)人,實(shí)際上就是相當(dāng)于要得到N個(gè)百分比數(shù)據(jù) * 條件是這N個(gè)百分比之和=100/100淹魄。這N個(gè)百分比的平均值是1/N郁惜。 * 并且這N個(gè)百分比數(shù)據(jù)符合一種正態(tài)分布(多數(shù)值比較靠近平均值)。
解讀:比如我有1000塊錢甲锡,發(fā)50個(gè) 紅包兆蕉,就先隨機(jī)出50個(gè)數(shù),然后算出這50個(gè)數(shù)的均值$avg缤沦,用$avg/(1/N)虎韵,就得到了一個(gè)基數(shù)$mixrand,然后用隨機(jī)出的那50個(gè)數(shù)分別去除以$mixrand缸废,得到每個(gè)數(shù)相對(duì)基數(shù)的百分比$randVal包蓝,然后用$randVal乘以1000塊錢,就可以得到每個(gè)紅包的具體金額了企量。

還是不太清楚咋回事测萎?沒關(guān)系,我們一起擼代碼届巩!


三硅瞧、Talk is cheap, show me your code!

紅包生成核心算法:
<?php

/*
 * Author:xx_lufei
 * Time:2016年9月14日09:55:36
 * Note:紅包生成隨機(jī)算法
 */

class Reward
{
    public $rewardMoney;        #紅包金額、單位元
    public $rewardNum;          #紅包數(shù)量

    #執(zhí)行紅包生成算法
    public function splitReward($rewardMoney, $rewardNum, $max, $min)
    {
        #傳入紅包金額和數(shù)量恕汇,因?yàn)樾?shù)在計(jì)算過程中會(huì)出現(xiàn)很大誤差腕唧,所以我們直接把金額放大100倍,后面的計(jì)算全部用整數(shù)進(jìn)行
        $min = $min * 100;
        $max = $max * 100;
        #預(yù)留出一部分錢作為誤差補(bǔ)償瘾英,保證每個(gè)紅包至少有一個(gè)最小值
        $this->rewardMoney = $rewardMoney * 100 - $rewardNum * $min;
        $this->rewardNum = $rewardNum;
        #計(jì)算出發(fā)出紅包的平均概率值四苇、精確到小數(shù)4位。
        $avgRand = 1 / $this->rewardNum;
        $randArr = array();
        #定義生成的數(shù)據(jù)總合sum
        $sum = 0;
        $t_count = 0;
        while ($t_count < $rewardNum) {
            #隨機(jī)產(chǎn)出四個(gè)區(qū)間的額度
            $c = rand(1, 100);
            if ($c < 15) {
                $t = round(sqrt(mt_rand(1, 1500)));
            } else if ($c < 65) {
                $t = round(sqrt(mt_rand(1500, 6500)));
            } else if ($c < 95) {
                $t = round(sqrt(mt_rand(6500, 9500)));
            } else {
                $t = round(sqrt(mt_rand(9500, 10000)));
            }
            ++$t_count;
            $sum += $t;
            $randArr[] = $t;
        }

        #計(jì)算當(dāng)前生成的隨機(jī)數(shù)的平均值方咆,保留4位小數(shù)
        $randAll = round($sum / $rewardNum, 4);

        #為將生成的隨機(jī)數(shù)的平均值變成我們要的1/N,計(jì)算一下每個(gè)隨機(jī)數(shù)要除以的總基數(shù)mixrand蟀架。此處可以約等處理瓣赂,產(chǎn)生的誤差后邊會(huì)找齊
        #總基數(shù) = 均值/平均概率
        $mixrand = round($randAll / $avgRand, 4);

        #對(duì)每一個(gè)隨機(jī)數(shù)進(jìn)行處理,并乘以總金額數(shù)來得出這個(gè)紅包的金額片拍。
        $rewardArr = array();
        foreach ($randArr as $key => $randVal) {
            #單個(gè)紅包所占比例randVal
            $randVal = round($randVal / $mixrand, 4);
            #算出單個(gè)紅包金額
            $single = floor($this->rewardMoney * $randVal);
            #小于最小值直接給最小值
            if ($single < $min) {
                $single += $min;
            }
            #大于最大值直接給最大值
            if ($single > $max) {
                $single = $max;
            }
            #將紅包放入結(jié)果數(shù)組
            $rewardArr[] = $single;
        }

        #對(duì)比紅包總數(shù)的差異煌集、將差值放在第一個(gè)紅包上
        $rewardAll = array_sum($rewardArr);
        $rewardArr[0] = $rewardMoney * 100 - ($rewardAll - $rewardArr[0]);#此處應(yīng)使用真正的總金額rewardMoney,$rewardArr[0]可能小于0

        #第一個(gè)紅包小于0時(shí),做修正
        if ($rewardArr[0] < 0) {
            rsort($rewardArr);
            $this->add($rewardArr, $min);
        }

        rsort($rewardArr);
        #隨機(jī)生成的最大值大于指定最大值
        if ($rewardArr[0] > $max) {
            #差額
            $diff = 0;
            foreach ($rewardArr as $k => &$v) {
                if ($v > $max) {
                    $diff += $v - $max;
                    $v = $max;
                } else {
                    break;
                }
            }
            $transfer = round($diff / ($this->rewardNum - $k + 1));
            $this->diff($diff, $rewardArr, $max, $min, $transfer, $k);
        }
        return $rewardArr;
    }

    #處理所有超過最大值的紅包
    public function diff($diff, &$rewardArr, $max, $min, $transfer, $k)
    {
        #將多余的錢均攤給小于最大值的紅包
        for ($i = $k; $i < $this->rewardNum; $i++) {
            #造隨機(jī)值
            if ($transfer > $min * 20) {
                $aa = rand($min, $min * 20);
                if ($i % 2) {
                    $transfer += $aa;
                } else {
                    $transfer -= $aa;
                }
            }
            if ($rewardArr[$i] + $transfer > $max) continue;
            if ($diff - $transfer < 0) {
                $rewardArr[$i] += $diff;
                $diff = 0;
                break;
            }
            $rewardArr[$i] += $transfer;
            $diff -= $transfer;
        }
        if ($diff > 0) {
            $i++;
            $this->diff($diff, $rewardArr, $max, $min, $transfer, $k);
        }
    }

    #第一個(gè)紅包小于0,從大紅包上往下減
    public function add(&$rewardArr, $min)
    {
        foreach ($rewardArr as &$re) {
            $dev = floor($re / $min);
            if ($dev > 2) {
                $transfer = $min * floor($dev / 2);
                $re -= $transfer;
                $rewardArr[$this->rewardNum - 1] += $transfer;
            } elseif ($dev == 2) {
                $re -= $min;
                $rewardArr[$this->rewardNum - 1] += $min;
            } else {
                break;
            }
        }
        if ($rewardArr[$this->rewardNum - 1] > $min || $rewardArr[$this->rewardNum - 1] == $min) {
            return;
        } else {
            $this->add($rewardArr, $min);
        }
    }
}
細(xì)節(jié)考慮:

下邊這段代碼用來控制具體的業(yè)務(wù)邏輯捌省,按照具體的需求苫纤,留出固定的最大值、最小值紅包的金額等;
在代碼中調(diào)用生成紅包的方法時(shí)splitReward($total, $num,$max - 0.01, $min);卷拘,我傳入的最大值減了0.01喊废,這樣就保證了里面生成的紅包最大值絕對(duì)不會(huì)超過我們設(shè)置的最大值。

<?php 
class CreateReward{
    /*
     * 生成紅包
     * author    xx     2016年9月23日13:53:38
     * @param   int          $total               紅包總金額
     * @param   int          $num                 紅包總數(shù)量
     * @param   int          $max                 紅包最大值
     * 
     */
    public function random_red($total, $num, $max, $min)
    {
        #總共要發(fā)的紅包金額栗弟,留出一個(gè)最大值;
        $total = $total - $max;
        $reward = new Reward();
        $result_merge = $reward->splitReward($total, $num, $max - 0.01, $min);
        sort($result_merge);
        $result_merge[1] = $result_merge[1] + $result_merge[0];
        $result_merge[0] = $max * 100;
        foreach ($result_merge as &$v) {
            $v = floor($v) / 100;
        }
        return $result_merge;
    }
}

四污筷、拉出來遛遛

基礎(chǔ)代碼:

設(shè)置好各種初始值

<?php
/**
 * Created by PhpStorm.
 * User: lufei
 * Date: 2017/1/4
 * Time: 22:49
 */
header('content-type:text/html;charset=utf-8');
ini_set('memory_limit', '128M');

require_once('CreateReward.php');
require_once('Reward.php');

$total = 50000;
$num = 300000;
$max = 50;
$min = 0.01;

$create_reward = new CreateReward();
性能測試:

因?yàn)閙emory_limit的限制,所以只測了5次的均值乍赫,結(jié)果都在1.6s左右瓣蛀。

for($i=0; $i<5; $i++) {
    $time_start = microtime_float();
    $reward_arr = $create_reward->random_red($total, $num, $max, $min);
    $time_end = microtime_float();
    $time[] = $time_end - $time_start;
}
echo array_sum($time)/5;
function microtime_float()
{
    list($usec, $sec) = explode(" ", microtime());
    return ((float)$usec + (float)$sec);
}

運(yùn)行結(jié)果:

<img src="http://upload-images.jianshu.io/upload_images/3389468-1080352a85cd72bc.png">

數(shù)據(jù)檢查:

檢測有沒有負(fù)值,有沒有最大值雷厂,最大值有多少個(gè)惋增,有沒有小于最小值的值;

$reward_arr = $create_reward->random_red($total, $num, $max, $min);
sort($reward_arr);//正序改鲫,最小的在前面
$sum = 0;
$min_count = 0;
$max_count = 0;
foreach($reward_arr as $i => $val) {
    if ($i<3) {
        echo "<br />第".($i+1)."個(gè)紅包诈皿,金額為:".$val."<br />";  
    } 
    if ($val == $max) {
        $max_count++;
    }
    if ($val < $min) {
        $min_count++;
    }
    $val = $val*100;
    $sum += $val;
}
//檢測錢是否全部發(fā)完
echo '<hr>已生成紅包總金額為:'.($sum/100).';總個(gè)數(shù)為:'.count($reward_arr).'<hr>';
//檢測有沒有小于0的值
echo "<br />最大值:".($val/100).',共有'.$max_count.'個(gè)最大值,共有'.$min_count.'個(gè)值比最小值小';

運(yùn)行結(jié)果:

<img src="http://upload-images.jianshu.io/upload_images/3389468-6c2b0755ee8c3896.png">

正態(tài)分布圖:

注意钩杰,出圖的時(shí)候纫塌,紅包的數(shù)量不要給的太大,不然頁面渲染不出來讲弄,會(huì)崩
<img src="http://upload-images.jianshu.io/upload_images/3389468-065b6ec3958c8f90.png">

$reward_arr = $create_reward->random_red($total, $num, $max, $min);
$show = array();
rsort($reward_arr);
//為了更直觀的顯示正態(tài)分布效果,需要將數(shù)組重新排序
foreach($reward_arr as $k=>$value)
{
    $t=$k%2;
    if(!$t) $show[]=$value;;
    else array_unshift($show,$value);
}
echo "設(shè)定最大值為:".$max.',最小值為:'.$min.'<hr />';
echo "<table style='font-size:12px;width:600px;border:1px solid #ccc;text-align:left;'><tr><td>紅包金額</td><td>圖示</td></tr>";
foreach($show as $val)
{
    #線條長度計(jì)算
    $width=intval($num*$val*300/$total);
    echo "<tr><td> {$val} </td><td width='500px;text-align:left;'><hr style='width:{$width}px;height:3px;border:none;border-top:3px double red;margin:0 auto 0 0px;'></td></tr>";
}
echo "</table>";

運(yùn)行結(jié)果:

<img src="http://upload-images.jianshu.io/upload_images/3389468-9db8573297785b29.png">

PS:有朋友問我生成的數(shù)據(jù)有沒有通過數(shù)學(xué)方法來驗(yàn)證其是否符合標(biāo)準(zhǔn)正態(tài)分布措左,因?yàn)槲业臄?shù)學(xué)不好,這個(gè)還真沒算過避除,只是看著覺得像怎披,就當(dāng)他是了。
  既然遇到了這個(gè)問題瓶摆,就一定要解決嘛凉逛,所以我就用php內(nèi)置函數(shù)算了一下,算出來的結(jié)果在數(shù)據(jù)量小的時(shí)候還是比較接近正態(tài)分布的群井,但是數(shù)據(jù)量大起來的時(shí)候就不能看了状飞,我整不太明白這個(gè),大家感興趣的可以找一下原因喲书斜。
  php的四個(gè)函數(shù):stats_standard_deviation(標(biāo)準(zhǔn)差)诬辈,stats_variance(方差), stats_kurtosis(峰度)荐吉,stats_skew(偏度)
  使用上面的函數(shù)需要安裝stats擴(kuò)展@下載地址

五焙糟、In the end

到這里,紅包就算是寫完啦样屠,不知道能不能漲50塊工資穿撮,但應(yīng)該能解決燃眉之急了缺脉。
<img src="http://upload-images.jianshu.io/upload_images/3389468-9021345e34a053fd.jpg">
哦對(duì),還落下了這個(gè)代碼打包下載

歡迎來我的技術(shù)博客 KEEP GOING

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悦穿,一起剝皮案震驚了整個(gè)濱河市攻礼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咧党,老刑警劉巖秘蛔,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異傍衡,居然都是意外死亡深员,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門蛙埂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倦畅,“玉大人,你說我怎么就攤上這事绣的〉停” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵屡江,是天一觀的道長芭概。 經(jīng)常有香客問我,道長惩嘉,這世上最難降的妖魔是什么罢洲? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮文黎,結(jié)果婚禮上惹苗,老公的妹妹穿的比我還像新娘。我一直安慰自己耸峭,他們只是感情好桩蓉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著劳闹,像睡著了一般院究。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上本涕,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天儡首,我揣著相機(jī)與錄音,去河邊找鬼偏友。 笑死,一個(gè)胖子當(dāng)著我的面吹牛对供,可吹牛的內(nèi)容都是我干的位他。 我是一名探鬼主播氛濒,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鹅髓!你這毒婦竟也來了舞竿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤窿冯,失蹤者是張志新(化名)和其女友劉穎番刊,沒想到半個(gè)月后芙粱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年桅狠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椎工。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冒冬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缠沈,到底是詐尸還是另有隱情膘壶,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布洲愤,位于F島的核電站颓芭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏柬赐。R本人自食惡果不足惜亡问,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躺率。 院中可真熱鬧玛界,春花似錦、人聲如沸悼吱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽后添。三九已至笨枯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間遇西,已是汗流浹背馅精。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粱檀,地道東北人洲敢。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像茄蚯,于是被迫代替她去往敵國和親压彭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子睦优,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • (轉(zhuǎn)自http://www.douban.com/group/topic/14820131/,轉(zhuǎn)自人大論壇) 調(diào)整...
    f382b3d9bdb3閱讀 10,438評(píng)論 0 8
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理壮不,服務(wù)發(fā)現(xiàn)汗盘,斷路器,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • 來源: http://www.douban.com/group/topic/14820131/ 調(diào)整變量格式: f...
    MC1229閱讀 6,917評(píng)論 0 5
  • 此快捷鍵適合于Mac電腦: 代碼提示:Alt + /; 打友弧:syso main函數(shù):main 格式化:comma...
    frankisbaby閱讀 305評(píng)論 0 0
  • 進(jìn)度:整本書終結(jié)隐孽。 今天主要是第二十章 智人末日和后記。 印象最深的是健蕊,作者在最后的反思: 問題不是“我們究竟想要...
    大林_Rbenefit閱讀 172評(píng)論 0 0