面试场景题:如何实现带权重值的抽奖程序

背景:

在之前有遇到一个面试题,让实现一个带有权重值的抽奖系统,具体如下:

假设一个类似抽盲盒的场景,配置了三个物品,1,2,3,分别的权重是20,30,50,表明物品1中奖概率20%,物品2中奖概率30%,物品3中奖概率50%,用户买了这个盲盒,实现根据中奖概率返回哪件物品?

infos := []RewardInfo{
		{RewardId: 1, RewardName: "物品A", Weight: 20},// 20%
		{RewardId: 2, RewardName: "物品B", Weight: 30},// 30%
		{RewardId: 3, RewardName: "物品C", Weight: 50},// 50%
	}

大家可以先想一下具体要怎么实现?

以穿越火线官方活动为例,活动链接:cf道聚城活动
在这里插入图片描述

思路:

前提:以下代码示例都是参考上图的权重值。

  • 首先,求出能使得所有奖品权重值都变为正整数的最大倍数 maxMultiple
    • 找出所有奖品对应的权重值中,小数点后 [数字位数最多的权重值],并根据此权重值小数点后的位数,求出使其权重值变为正整数的最大倍数 maxMultiple。遍历过程中,我们只关注小数类型的权重值,整数类型的权重值由于没有小数点,可以直接跳过。
    • 例如:“奖品A权重值 0.25,奖品B权重值 0.75, 奖品C权重值 1,奖品D权重值 98,加起来一共是 100 ”。我们可以找出 小数点后 [数字位数最多的权重值],即 奖品A的 0.25(或者 奖品B的权重值 0.75)。并且其小数点之后的位数有2位,它需要乘以100(10*10)才能变为正整数 25,因此得到 maxMultiple = 100
  • 然后,根据 [不同奖品的权重值] 乘以 [之前求得的最大倍数 maxMultiple],分配对应个数的奖品到奖池中:
  • 最后,根据 rand 随机种子函数,从构建好的奖池中抽取奖品即可。
    如果需要的话,可以将每次抽取的奖品记录单独输出到一个文件中。

注意:

  • rewardList := make([]int32, 0, maxMultiple*100)
    为了避免奖池切片反复扩容影响性能,最好提前预估cap容量:倍率*100
    例如奖品A的权重为 0.25,乘以100倍率后为 25;奖品D的权重为 98,乘以100倍率后为 9800;那么对于总权重为100,乘以100倍率后就为1w。因此可以预估切片容量为1w。
  • time.Sleep(time.Nanosecond)
    每次抽奖都有一定时间间隔,避免程序运行过快导致一瞬间的随机数种子都一致,我这里每次休眠时长为100纳秒。

更多优化点:

为了避免切片占据过大的内存空间,可以考虑使用 BitMap位图 来代替常规切片数组,来减少内存开销,大家可以自己尝试下如何实现。

代码:

// RewardInfo 奖励信息
type RewardInfo struct {
	RewardId   int32   `json:"-"`          // 奖品id
	RewardName string  `json:"RewardName"` // 奖品名称
	Weight     float32 `json:"Weight"`     // 抽中的权重(省略末尾百分号的形式:例如某奖品权重为 0.1%,那么这里值也为 0.1)
	GetCnt     int32   `json:"GetCnt"`     // 中奖数
}

// 抽奖模拟器
// 入参:chouJiangCnt-用户的抽取次数
func LotterySimulator(chouJiangCnt int) {
	// 以下权重都是百分制(省略了末尾百分号%),例如 0.1 → 0.1%,79 → 79%,一共是100%
	infos := []RewardInfo{
		{RewardId: 1, RewardName: "幻神-CFS2022(皮肤,不可交易)", Weight: 0.1},
		{RewardId: 2, RewardName: "火麒麟-巅峰荣耀(皮肤,不可交易)", Weight: 0.15},
		{RewardId: 3, RewardName: "黑骑士-CFS2021(皮肤,不可交易)", Weight: 0.25},
		{RewardId: 4, RewardName: "毁灭-CFS2020(皮肤,不可交易)", Weight: 0.25},
		{RewardId: 5, RewardName: "M4A1-雷神-CFS2019(皮肤,不可交易)", Weight: 0.3},
		{RewardId: 6, RewardName: "毁灭-CFS2018(皮肤,不可交易)", Weight: 0.3},
		{RewardId: 7, RewardName: "火麒麟-CFS2018(皮肤,不可交易)", Weight: 0.3},
		{RewardId: 8, RewardName: "屠龙-荣耀之锋(皮肤,不可交易)", Weight: 0.4},
		{RewardId: 9, RewardName: "修罗-CFS2018(皮肤,不可交易)", Weight: 0.85},
		{RewardId: 10, RewardName: "王者之石x1", Weight: 7},
		{RewardId: 11, RewardName: "积分x100", Weight: 0.1},
		{RewardId: 12, RewardName: "积分x10", Weight: 1},
		{RewardId: 13, RewardName: "积分x5", Weight: 10},
		{RewardId: 14, RewardName: "积分x1", Weight: 79},
	}

	// 初始化奖品属性map
	rewardMap := make(map[int32]RewardInfo)
	for _, rewardInfo := range infos {
		rewardMap[rewardInfo.RewardId] = RewardInfo{
			RewardId:   rewardInfo.RewardId,
			RewardName: rewardInfo.RewardName,
			Weight:     rewardInfo.Weight,
			GetCnt:     0,
		}
	}

	// 此处按需添加:将抽奖结果记录文件于当前路径下
	// 注意:我下面是判断抽到幻神才输出结果记录到文件
	testJsonFile, _ := os.OpenFile("./cf_reward.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
	defer testJsonFile.Close()

	// 当前抽奖已花费总金额,初始为0;单次抽奖花费金额10元
	costMoneyOfTotal, costMoneyOfEveryTime := 0, 10
	for {
		time.Sleep(time.Second) // 每秒抽一次,控制下速率,方便控制台打印观察
		fmt.Printf("抽奖中...,当前已消费%v元\n", costMoneyOfTotal)

		for i := 0; i < chouJiangCnt; i++ {
			rewardId := SelectByWeight(infos)
			// 组装抽取奖品的数据
			rewardMap[rewardId] = RewardInfo{
				RewardId:   rewardMap[rewardId].RewardId,
				RewardName: rewardMap[rewardId].RewardName,
				Weight:     rewardMap[rewardId].Weight,
				GetCnt:     rewardMap[rewardId].GetCnt + 1,
			}
			costMoneyOfTotal += costMoneyOfEveryTime

			// 每次抽奖都有一定间隔,避免运行过快导致一瞬间的随机数种子都一致
			time.Sleep(time.Nanosecond)
		}

		// 以下逻辑按需添加,只是输出指定抽奖结果到文件
		// 如果 幻神数 > 0,则把抽奖结果记录到文件中,并停止抽奖
		if rewardMap[1].GetCnt > 0 {
			jsonRewardMap, _ := json.Marshal(rewardMap)
			// fmt.Printf("rewardMap: \n%v\n", string(jsonRewardMap))

			writer := bufio.NewWriter(testJsonFile)
			writer.WriteString(string(jsonRewardMap) + "\n" + fmt.Sprintf("一共花费RMB:%v", costMoneyOfTotal))
			writer.Flush()
			break
		}
	}

	return
}

// SelectByWeight 根据不同奖品权重随机抽取奖品并返回
func SelectByWeight(infos []RewardInfo) (RewardId int32) {
	// 先找出所有奖品数要乘的最大公共倍数 maxMultiple
	var maxMultiple int = 1

	for i := 0; i < len(infos); i++ {
		strWeight := fmt.Sprintf("%v", infos[i].Weight)
		// maxMultiple 只关注小数类型权重值,整数类型权重值直接跳过。例如"0.25",我们通过其中的"25"来得到 maxMultiple=100
		if i := strings.Index(strWeight, "."); i == -1 {
			continue
		}

		var tmpMultiple int = 1
		for i := 0; i < len(strWeight[i+1:]); i++ {
			tmpMultiple *= 10
		}

		if maxMultiple < tmpMultiple {
			maxMultiple = tmpMultiple
		}
	}
	// fmt.Println("最大倍数 maxMultiple: ", maxMultiple)

	// 避免反复扩容,提前预估cap容量:倍率*100
	// 例如权重为0.85,乘以100倍率后为85;那么对于总权重为100,乘以100倍率后就为1w
	rewardList := make([]int32, 0, maxMultiple*100)

	// 根据不同奖品的权重值,分配对应个数的奖品到奖池中
	for _, info := range infos {
		for i := 0; i < int(info.Weight*float32(maxMultiple)); i++ { // 填充每个奖品的整数样本个数到容量为1w的奖池中
			rewardList = append(rewardList, info.RewardId)
		}
	}
	// fmt.Printf("奖池奖品总数 = %v, 奖池奖品明细 = %v \n", len(rewardList), rewardList)
	// // os.Exit(0)

	rand.Seed(time.Now().UnixNano())              // UnixNano将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位纳秒)
	return rewardList[rand.Intn(len(rewardList))] // rand.Intn(100)类似于rand.Int()%100
}

func main() {
	// 模拟cf抽奖
	LotterySimulator(10)
}

运行结果

比如我这次试水了1800,就抽中了概率最低的 幻神 − C F S 2022 \color{red}{幻神-CFS2022} 幻神CFS2022,运气中规中矩。
在这里插入图片描述
抽奖结果单独记录到文件:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 什么是Eureka? Eureka是Netflix开源的一款服务发现框架,用于实现微服务的注册与发现。 2. Eureka的主要作用是什么? Eureka的主要作用是帮助微服务实现自动化的服务注册和发现。 3. Eureka的核心组件有哪些? Eureka的核心组件有Eureka Server、Eureka Client和Eureka注册中心。 4. Eureka Server和Eureka Client的区别是什么? Eureka Server是服务注册中心,负责管理所有的服务实例;Eureka Client是服务提供者和服务消费者,负责注册服务和发现服务。 5. Eureka的服务注册过程是什么? 服务提供者向Eureka Server发送注册请求,Eureka Server将服务实例信息注册到自己的注册表中。 6. Eureka的服务发现过程是什么? 服务消费者向Eureka Server发送发现请求,Eureka Server返回注册表中的服务实例列表,服务消费者从中选择一个服务实例进行调用。 7. Eureka的自我保护机制是什么? Eureka的自我保护机制是在一定时间内,如果Eureka Server没有收到服务实例的心跳,则认为该服务实例不可用,但不会立即从注册表中删除,以避免误判。 8. Eureka的心跳机制是什么? Eureka的心跳机制是服务实例每隔一段时间向Eureka Server发送一次心跳包,以表明自己的状态。 9. Eureka的服务注册表如何保证高可用性? Eureka的服务注册表可以使用多个Eureka Server实例组成集群,以提高可用性。 10. Eureka的服务注册表如何保证一致性? Eureka的服务注册表使用了CAP理论中的AP模型,保证了高可用性和分区容错性,但可能会存在数据不一致的情况。 11. Eureka的服务注册表如何进行负载均衡? Eureka的服务注册表中每个服务实例都有一个权重,Eureka Client在进行服务发现时会根据权重进行负载均衡。 12. Eureka的服务注册表如何进行动态扩容? 当Eureka Server的负载达到一定阈时,可以通过增加Eureka Server实例的方式进行动态扩容。 13. Eureka的服务注册表如何进行动态缩容? 当Eureka Server的负载下降到一定阈时,可以通过减少Eureka Server实例的方式进行动态缩容。 14. Eureka的服务注册表如何进行数据备份? 可以通过将Eureka Server的注册表数据备份到其他存储介质中,如MySQL数据库等。 15. Eureka的服务注册表如何进行数据恢复? 可以通过将备份的数据导入到Eureka Server的注册表中进行数据恢复。 16. Eureka的服务注册表如何进行故障转移? 当Eureka Server实例发生故障时,可以通过其他Eureka Server实例接管其服务注册表来进行故障转移。 17. Eureka的服务注册表如何进行数据同步? Eureka的服务注册表使用了Netflix的Delta算法进行数据同步,该算法能够有效地减少网络宽的使用。 18. Eureka的服务注册表如何进行数据压缩? Eureka的服务注册表使用了GZIP算法进行数据压缩,以减少网络宽的使用。 19. Eureka的服务注册表如何进行安全认证? 可以通过HTTPS协议和基于用户名密码的认证方式来保证Eureka Server和Eureka Client之间的通信安全。 20. Eureka的服务注册表如何进行权限控制? 可以通过ACL(Access Control List)机制来对Eureka Server和Eureka Client的访问进行权限控制。 21. Eureka的服务注册表如何进行监控和统计? 可以通过Eureka的REST API和Netflix的Hystrix Dashboard来监控和统计Eureka Server和Eureka Client的运行情况。 22. Eureka的服务注册表如何进行自定义配置? 可以通过在配置文件中添加spring.cloud.netflix.eureka.*前缀的配置项来进行自定义配置。 23. Eureka的服务注册表如何进行多环境配置? 可以通过在配置文件中添加spring.profiles.active=dev/prod等标识来进行多环境配置。 24. Eureka的服务注册表如何进行集成测试? 可以通过使用Spring Cloud的@SpringBootTest注解和EurekaTestRestTemplateCustomizer类来进行集成测试。 25. Eureka的服务注册表如何进行容器化部署? 可以通过使用Docker等容器化技术来进行Eureka Server和Eureka Client的容器化部署。 26. Eureka的服务注册表如何进行跨平台部署? 可以通过使用Kubernetes等容器编排技术来进行Eureka Server和Eureka Client的跨平台部署。 27. Eureka的服务注册表如何进行版本控制? 可以通过使用Git等版本控制工具来进行Eureka Server和Eureka Client的版本控制。 28. Eureka的服务注册表如何进行自动化部署? 可以通过使用Jenkins等自动化部署工具来实现Eureka Server和Eureka Client的自动化部署。 29. Eureka的服务注册表如何进行监控告警? 可以通过使用Prometheus等监控告警工具来实现Eureka Server和Eureka Client的监控告警。 30. Eureka的服务注册表如何进行性能优化? 可以通过优化Eureka Server和Eureka Client的配置、调整心跳间隔和超时时间、增加节点数等方式来进行性能优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值