1、需求分析
我们最近做了个小程序叫「Soda壁纸」,这个小程序的首页会随机全屏显示一张壁纸,点一下刷新按钮就换一张,再点再换,很适合打发时间。
有一天,产品发话了:“这个随机刷新应该多展示受欢迎的壁纸,少展示较冷门的壁纸。”
这句话要这么理解:
现状:随机
比方说我们有两张壁纸
现在要从中抽一张图片,就跟扔硬币一样,正面就壁纸A,反面就壁纸B,概率各50%。
int random = (int)(Math.random()+1);
if(random == 1){
//壁纸A
} else {
//壁纸B
}
目标:加权随机
我们还是有两张壁纸,但是壁纸A的妹子更萌一些,产品希望刷新时更大概率刷出壁纸A。怎么个大概率法呢?就让壁纸A的概率是壁纸B的5倍吧。
虽然还是两张图片,但这个时候就不能扔硬币了。两张图片的权重加起来是6,可以扔骰子。扔到1~5是壁纸A,扔到6是壁纸B。概率分别为83.3%和16.7%。
int random = (int)(Math.random()*5+1);
if(random >= 1 && random <= 5){
//壁纸A
} else {
//壁纸B
}
加权就这么简单。
2、代码实现
代码实现大概分为几个步骤:
- 计算每张壁纸的权重
- 取得权重计算所需要的全局因子
- 取得每张壁纸需参与权重计算的因子
- 计算权重
- 根据权重重新分配每张壁纸对应的值域
- 获取一个随机数,随机数落在哪张壁纸的值域中,就取哪一张壁纸
首先,设计两个DTO用于封装全局计算因子和壁纸计算因子
/**
* 图片权重计算全局因子
*/
public class PictureRankCommonFactor {
// 例如壁纸平均收藏次数
// 例如活跃用户数
}
/**
* 单张图片参与权重计算的因子
*/
public class PictureRankFactor {
private String id; //壁纸Id
// 例如壁纸的收藏次数
// 例如壁纸的发布时间
}
然后,设计权重计算器
/**
* 定义计算器的接口
*/
public interface RankCalculater {
Double calculater(PictureRankFactor prFactor);
}
/**
* 实现计算器的接口
*/
public class PictureRankCalculater implements RankCalculater {
private PictureRankCommonFactor prcFactor; // 全局计算因子
public void set PictureRankCommonFactor(PictureRankCommonFactor prcFactor) {
this.prcFactor = prcFactor;
}
public Double calculater(PictureRankFactor prFactor){
// 具体实现
}
}
再设计一个单例,用来存放排好序的TreeMap
public enum PictureRankTree {
INSTANCE;
private TreeMap<Double,String> rankTree;
public boolean isEmpty() {
if (null == rankTree || rankTree.isEmpty()) {
return true;
} else {
return false;
}
}
public void setRankTree(TreeMap<Double, String> rankTree) {
this.rankTree = rankTree;
}
private Double maxRank;
public void setMaxRank(Double maxRank) {
this.maxRank = maxRank;
}
/**
* 加权随机获得一个Id
* @return
*/
public String getRandomId() {
// 具体实现
}
}
接下来设计一个定时任务,用来执行各项任务
@Service
@Lazy(value = false)
public class PictureRankScheduler {
@Scheduled(cron = "0 0 2 * * ?") //每天凌晨2点执行
public void updatePictureRankMap() {
// 具体实现
}
}
好了准备工作做完了,现在开始实现定时任务中的具体代码
public void updatePictureRankMap() {
/**
* 初始化计算器
* 1.取出全局因子
* 2.创建计算机并传入全局因子
*/
PictureRankCommonFactor prCommonFactor = pictureService.getPictureRankCommonFactor();
RankCalculater calculater = new PictureRankCalculater();
calculater.setPrCommonFactor(prCommonFactor);
/**
* 计算权重
* 1.取出壁纸的计算因子集合
* 2.遍历壁纸,进行权重计算,并把结果存入HashMap
*/
Map<String,Double> rankMap = Maps.newHashMap(); //这里用HashMap,读写速度快
List<PictureRankFactor> prFactorList = pictureService.findPRFactorList(picture);
for (PictureRankFactor prFactor : prFactorList) {
Double rank = calculater.prCalculater(prFactor);
rankMap.put(prFactor.getId(),rank);
}
/**
* 重新分配值域并存入TreeMap
* 1.无序取出HashMap中的壁纸Id和权重值
* 2.逐个对HashMap中的权重值进行累加,并存入TreeMap进行排序
* 以文初的图示为例,壁纸A的权重为5,壁纸B的权重为1,重新分配值域:
* 壁纸A:[0,5),壁纸B:[5,6) 或 壁纸B:[0,1),壁纸A:[1,6)
* 此时取[0,6)的随机数,根据随机数落入的值域来确定图片,概率分布为壁纸A:83.3%,壁纸B:16.7%
*/
TreeMap<Double,String> rankTree = Maps.newTreeMap(); //这里用treeMap,排序,比较大小快
Double lastRank = 0.0d,currentRank;
Map.Entry<String,Double> current;
Iterator<Map.Entry<String,Double>> iterator = rankMap.entrySet().iterator();
while (iterator.hasNext()){
current = iterator.next();
currentRank = current.getValue() + lastRank;
rankTree.put(currentRank, current.getKey());
lastRank = currentRank;
}
PictureRankTree.INSTANCE.setRankTree(rankTree);
PictureRankTree.INSTANCE.setMaxRank(rankTree.lastKey());
}
最后,实现PictureRankTree中的getRandomId逻辑
public String getRandomId() {
// 获得一个[0,maxRank)范围内的随机数
Double randomDouble = Math.random() * maxRank;
// 取出TreeMap中所有key值大于(不等于)随机数的子集
SortedMap<Double, String> sm = this.rankTree.tailMap(randomDouble,false);
if (!sm.isEmpty()) {
return this.rankTree.get(sm.firstKey()); //子集中key最小的那个对象就是我们要的结果啦
} else {
return null;
}
}
再用一张图片来说明下
- 举个栗子,先取得一个随机数6,在图中坐标轴的箭头1所在位置
- 在TreeMap中,取得一个key值大于6的子集,即图中箭头2所示的红色区域
- 在子集中取得最左边一个元素,即图中箭头3所示的壁纸B(随机数6落在壁纸B的值域中)
全文完。
感谢悠然大神电话远程指导。
大家有兴趣的话可以试用一下我们的小程序: