一、原题
编写自定义函数void hongbao(int amount,int renshu),实现红包分配算法,在函数中输出每个人分配到的红包金额。amount,表示红包总金额,renshu表示分配总人数。编写main函数测试,并且在main函数中,输入红包总金额和分配总人数。(不限制算法,尽可能合理)
二、算法及思路分析
初次看到这道题,笔者认为这只是一道简单的生成随机数的问题,考察的重点是rand函数,srand函数,同时确保随机数总和一定。但随着笔者的逐步编写,发现这是一个并不那么简单的问题,下面就跟着笔者一起来看看这个程序的编写思路。
1、rand函数,srand函数,time函数
首先我们介绍一下rand函数。rand
函数是 C 标准库中的一个函数,用于生成伪随机数,定义在 <stdlib.h>
头文件中。rand
函数会返回一个介于 0 和 RAND_MAX
之间的整数。RAND_MAX
是一个常量,表示 rand
函数可以返回的最大值。根据 C 标准,RAND_MAX
至少为 32767,但实际值取决于具体的实现。
之所以说rand函数生成的是伪随机数,因为如果只用rand函数,每一次运行程序的时候生成的随机数是一样的。rand函数生成的数字在统计上具有随机数的特性,但实际上是通过一个初始值(称为随机数种子)和一个固定的算法生成的。大多数c标准库里的rand
函数都基于线性同余法 (LCG)。
srand函数的作用是设置随机数种子。如果没有调用 srand
,则默认随机数种子值为 1。所以只用一个rand函数生成的随机数,在每一次的运行中都是一样的。
因此,我们需要用srand函数来设置随机数,我们需要找一个不断地变化的数,来设置随机数种子。因此,我们想到了时间。每一次运行程序的的时间都是不一样的,因此,我们把时间设为随机数种子,就可以实现每一次都不同的随机数生成。
代码如下:
srand((unsigned int)time(NULL))
2、调整随机数范围与格式
rand函数生成的随机数是0~RAND_MAX(至少为
32767),根据不同实际情况,我们要调整范围。本题的抢红包随机算法,需要生成0.01到输入的总金额之间的两位小数浮点型随机数。
尤其是总和一定,这说明每一次生成随机数的范围是在变化的。我们可以设计如下代码:
float max = amount2 - (renshu2 - 1) * min;//amount2代表实时剩余总金额,renshu2代表实时剩余人数
float temp_random = (float)rand() / RAND_MAX;//生成临时随机数,强制转换实型,再除以最大值(生成了一个0~0.99999999.....之间的随机数)
float random = min + (max - min) * temp_random;//0~0.9999...缩放至0.01~实时最大金额的范围
random = round(random * 100) / 100.0;//保留两位小数,作为随机生成的红包
3、 达成题目的其他要求完成代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
void hongbao(int amount, int renshu);//声明函数原型
int main()//主函数部分
{
int amount, reshu;
srand((unsigned int)time(NULL));// 设置当前时间作为随机数生成器的种子
scanf("%d %d", &amount, &reshu); // 输入总金额和人数
hongbao(amount, reshu);// 调用红包分配函数
return 0;
}
void hongbao(int amount, int renshu)
{
int i;
float min = 0.01f; // 最小红包金额
float all_amount = amount; // 总金额
float amount2 = all_amount; // 当前剩余金额
float renshu2 = renshu; // 当前剩余人数
for (i = 1; i < renshu; i++)// 循环分配红包给每个人(除了最后一个人)
{
float max = amount2 - (renshu2 - 1) * min;// 计算当前最大可能的红包金额(即随机数范围)
float temp_random = (float)rand() / RAND_MAX; // 生成一个 [0, 1) 区间内的随机浮点数
float random = min + (max - min) * temp_random; // 将随机数缩放到 [min, max] 区间内
random = round(random * 100) / 100.0;// 将随机数四舍五入到小数点后两位
printf("%d %.2f\n", i, random);// 打印当前人的编号和分配的红包金额
amount2 -= random; // 更新剩余金额和剩余人数
renshu2--;
}
printf("%d %.2f", renshu, amount2);// 此处考虑保留小数是否会造成所有红包加起来不等于总金额,所以分配最后一个红包时用减法,确保所有金额都被分配完
}
4、调试与运行
然而笔者测试数据的时候却发现了问题:
随着红包个数的增加,笔者发现出现了大量的0.01元红包,几乎全部的金额都集中在个别几个红包中,而且还扎堆在前几个红包里。说明算法存在重大的问题。
三、算法优化
经过仔细分析,笔者发现笔者的随机数算法生成的随机数存在明显的下降趋势。笔者认为这是动态的随机范围造成的,在笔者原来的代码里,每一个红包的随机数范围是在不断变化的,尤其是最大值在不断的减小,这就是造成这个下降趋势的随机数列的罪魁祸首。因此,我们需要优化我们的算法,在固定的范围内生成随机数
1、隔板法思想
我们不妨换一个思路,假设我们要发100个红包,总金额1000元。以0.01为最小单位,在0~1000内随机生成99个两位小数的数字(即隔板),每一个红包的金额是相邻两块隔板间的距离。这样做的好处是可以不改变随机数生成的范围,也就不会造成这样趋势下降的随机数列。
生成的隔板数字我们需要存起来,因此,我们创建一个隔板数组来存放这些隔板数字。
代码实现:
for (i = 1; i < renshu; i++)
{
float temp_random = (float)rand() / RAND_MAX;
float random = min + (max - min) * temp_random;
random = round(random * 1000) / 1000.0;
arr[i - 1] = random;
}
2、隔板排序
需要注意,我们生成的隔板是随机的数据,为了后续方便处理这些数据,我们需要把这个隔板数组里的数,按从小到大的顺序排列一下。
我们可以采用经典的冒泡排序算法
代码如下:
void paixu(float arr[], int n)
{
int i, j;
float temp;
for (i = 0; i < n - 1; i++) // 外层循环控制遍历次数
{
for (j = 0; j < n - 1 - i; j++) // 内层循环进行相邻元素的比较和交换
{
if (arr[j] > arr[j + 1]) // 如果当前元素大于下一个元素,则交换它们
{
temp = arr[j]; // 临时保存当前元素
arr[j] = arr[j + 1]; // 将下一个元素赋值给当前元素
arr[j + 1] = temp; // 将临时保存的当前元素赋值给下一个元素
}
}
}
}
该算法实现过程是从数组第一个元素开始,遍历每一个元素,为每一个元素找到它应该在的位置。
外层循环由数组元素数量决定,控制遍历次数与次序。内层循环不断比较相邻两个元素,一旦当前的元素大于下一个元素就交换。如此循环往复。
实际上就是内层循环每执行完一次回到外层循环时,最大的元素被移到了数组的末尾。
示例
假设我们有一个浮点数数组 arr = {3.2, 1.5, 4.8, 2.1, 5.0}
,我们将使用 paixu
函数对其进行排序。
arr = [3.2, 1.5, 4.8, 2.1, 5.0]
第一轮遍历
比较 3.2
和 1.5
,交换:
arr = [1.5, 3.2, 4.8, 2.1, 5.0]
比较 3.2
和 4.8
,不交换:
arr = [1.5, 3.2, 4.8, 2.1, 5.0]
比较 4.8
和 2.1
,交换:
arr = [1.5, 3.2, 2.1, 4.8, 5.0]
比较 4.8
和 5.0
,不交换:
arr = [1.5, 3.2, 2.1, 4.8, 5.0]
第二轮遍历
比较 1.5
和 3.2
,不交换
arr = [1.5, 3.2, 2.1, 4.8, 5.0]
比较 3.2
和 2.1
,交换:
arr = [1.5, 2.1, 3.2, 4.8, 5.0]
比较 3.2
和 4.8
,不交换:
arr = [1.5, 2.1, 3.2, 4.8, 5.0]
第三轮遍历
比较 1.5
和 2.1
,不交换:
arr = [1.5, 2.1, 3.2, 4.8, 5.0]
比较 2.1
和 3.2
,不交换:
arr = [1.5, 2.1, 3.2, 4.8, 5.0]
第四轮遍历
比较 1.5
和 2.1
,不交换:
arr = [1.5, 2.1, 3.2, 4.8, 5.0]
完成排序
运用到本题中,可直接将上方的排序算法复制进程序里调用即可。
3、完善算法,调整输入输出等
四、项目完成
本题的最终代码如下:
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库,用于 rand 和 srand
#include <time.h> // 时间库,用于 time
#include <math.h> // 数学库,用于 round
void hongbao(int amount, int renshu); // 声明红包分配函数
void paixu(float arr[], int n); // 声明排序函数
int main()
{
int amount, renshu; // 定义总金额和人数变量
srand((unsigned int)time(NULL)); // 使用当前时间作为随机数生成器的种子
scanf("%d%d", &amount, &renshu); // 输入总金额和人数
hongbao(amount, renshu); // 调用红包分配函数
return 0; // 主函数结束
}
void paixu(float arr[], int n)
{
int i, j; // 定义循环控制变量
float temp; // 定义临时变量用于交换
for (i = 0; i < n - 1; i++) // 外层循环控制遍历次数
{
for (j = 0; j < n - 1 - i; j++) // 内层循环进行相邻元素的比较和交换
{
if (arr[j] > arr[j + 1]) // 如果当前元素大于下一个元素,则交换它们
{
temp = arr[j]; // 临时保存当前元素
arr[j] = arr[j + 1]; // 将下一个元素赋值给当前元素
arr[j + 1] = temp; // 将临时保存的当前元素赋值给下一个元素
}
}
}
}
void hongbao(int amount, int renshu)
{
int i; // 定义循环控制变量
float min = 0.01f; // 最小红包金额
float max = amount; // 最大红包金额
float renshu2 = renshu; // 当前剩余人数
double sum = 0; // 用于累计已分配的红包金额(用于算最后一人的红包金额)
float arr[10000] = { 0 }; // 定义一个数组用于存储每个红包的金额
int j; // 定义循环控制变量
for (i = 1; i < renshu; i++) // 循环分配红包给每个人(除了最后一个人)
{
float temp_random = (float)rand() / RAND_MAX; // 生成一个 [0, 1) 区间内的随机浮点数
float random = min + (max - min) * temp_random; // 将随机数缩放到 [min, max] 区间内
random = round(random * 1000) / 1000.0; // 将随机数四舍五入到小数点后三位
arr[i - 1] = random; // 将生成的随机数存入数组
}
paixu(arr, renshu - 1); // 对生成的红包金额进行排序
for (j = 0; j < renshu - 1; j++) // 循环打印每个红包的金额
{
float amount2 = arr[j] - sum; // 计算当前人的红包金额
if (amount2 < min) // 确保红包金额不低于最小值
amount2 = min;
printf("第%d人的红包为%.2f元\n", j + 1, amount2); // 打印当前人的红包金额
sum += amount2; // 累计已分配的红包金额
}
float reat_amount = amount - sum; // 计算最后一人的红包金额
if (reat_amount < min) // 确保最后一人的红包金额不低于最小值
reat_amount = min;
printf("第%d人的红包为%.2f元", renshu, amount - sum); // 打印最后一人的红包金额
}
测试数据
至此,完整实现的微信红包随机算法。
笔者只是一位初学者,如果任何大佬有更好的算法和思路,欢迎多多指导~