还在用Math.random()简单排序做洗牌?小心掉进"伪随机"的陷阱!一套完整的数组处理工具让你的随机化真正公平!
在开发中,我们经常需要对数组进行随机化处理:游戏中的洗牌、抽奖系统的奖品分配、A/B测试的用户分组、内容推荐的随机展示等。看似简单的需求,但很多开发者使用的方法其实是有问题的。今天我们就来深入了解真正公平的随机化算法,并打造一套功能完备的数组处理工具。
想象你在开发一个在线扑克游戏:
一副54张牌需要完全随机洗牌
每个玩家必须有完全相等的获得任何牌的概率
如果洗牌算法有偏差,可能导致某些牌出现频率过高
这会影响游戏公平性,甚至可能涉及法律问题
在开发抽奖活动时:
一等奖:1%概率
二等奖:5%概率
三等奖:20%概率
谢谢参与:74%概率
需要确保概率完全准确,不能有任何偏差
在新闻或视频推荐系统中:
热门内容:60%权重
新发布内容:25%权重
用户关注内容:15%权重
既要保证随机性,又要考虑权重分配
在开始正确的解决方案之前,让我们看看很多人犯的错误:
// 这种做法是错误的!
function badShuffle(array) {
return array.sort(() => Math.random() - 0.5);
}
const cards = ['A', 'K', 'Q', 'J'];
console.log(badShuffle(cards));
为什么这样不行?
这种方法看起来随机,但实际上有严重的偏差问题:
// 让我们测试一下偏差
function testBadShuffle() {
const results = {};
const testArray = ['A', 'B', 'C'];
// 测试10000次
for (let i = 0; i < 10000; i++) {
const shuffled = testArray.sort(() =>Math.random() - 0.5);
const key = shuffled.join('');
results[key] = (results[key] || 0) + 1;
}
console.log('错误方法的结果分布:');
Object.entries(results).forEach(([key, count]) => {
console.log(`${key}: ${count}次 (${(count/10000*100).toFixed(1)}%)`);
});
}
testBadShuffle();
// 结果可能是:
// ABC: 3750次 (37.5%) - 应该是16.7%
// ACB: 1250次 (12.5%) - 应该是16.7%
// BAC: 1250次 (12.5%) - 应该是16.7%
// 明显不均匀!
// 这样也不够好
function anotherBadShuffle(array) {
const result = [...array];
for (let i = 0; i < 100; i++) { // 随机交换100次
const a = Math.floor(Math.random() * result.length);
const b = Math.floor(Math.random() * result.length);
[result[a], result[b]] = [result[b], result[a]];
}
return result;
}
虽然比第一种好,但仍然不是最优解,而且效率低。
现在让我们来实现真正科学的解决方案:
class ArrayUtils {
// Fisher-Yates洗牌算法 - 数学上证明的无偏算法
static shuffle(array) {
// 创建副本,避免修改原数组
const shuffled = [...array];
// 从后往前遍历
for (let i = shuffled.length - 1; i > 0; i--) {
// 在0到i之间(包含i)随机选择一个索引
const j = Math.floor(Math.random() * (i + 1));
// 交换元素
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// 加权洗牌 - 根据权重进行随机选择
static weightedShuffle(items, weights) {
if (items.length !== weights.length) {
thrownewError('物品数组和权重数组长度必须相同');
}
const result = [];
const workingItems = [...items];
const workingWeights = [...weights];
while (workingItems.length > 0) {
// 计算当前总权重
const totalWeight = workingWeights.reduce((sum, w) => sum + w, 0);
// 生成0到总权重之间的随机数
const random = Math.random() * totalWeight;
let currentWeight = 0;
// 找到对应的项
for (let i = 0; i < workingItems.length; i++) {
currentWeight += workingWeights[i];
if (random <= currentWeight) {
result.push(workingItems[i]);
workingItems.splice(i, 1);
workingWeights.splice(i, 1);
break;
}
}
}
return result;
}
// 随机采样 - 从数组中随机选择n个元素
static sample(array, n) {
if (n >= array.length) returnthis.shuffle(array);
const shuffled = this.shuffle(array);
return shuffled.slice(0, n);
}
// 分块处理 - 将数组分割成指定大小的小数组
static chunk(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// 分组处理 - 根据条件将数组元素分组
static groupBy(array, keyFn) {
return array.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) groups[key] = [];
groups[key].push(item);
return groups;
}, {});
}
// 随机选择单个元素
static randomPick(array) {
if (array.length === 0) returnundefined;
const randomIndex = Math.floor(Math.random() * array.length);
return array[randomIndex];
}
// 去重
static unique(array) {
return [...new Set(array)];
}
// 数组差集
static difference(array1, array2) {
const set2 = newSet(array2);
return array1.filter(item => !set2.has(item));
}
// 数组交集
static intersection(array1, array2) {
const set2 = newSet(array2);
return array1.filter(item => set2.has(item));
}
// 数组并集
static union(array1, array2) {
return [...new Set([...array1, ...array2])];
}
}
让我们详细理解这个"洗牌之王"的工作原理:
// 让我们用一个具体例子来理解算法
function explainFisherYates() {
const array = ['A', 'B', 'C', 'D'];
console.log('初始数组:', array);
const shuffled = [...array];
// 第一轮:i = 3
console.log('\n第一轮 (i=3):');
console.log('在索引 0-3 中随机选择...');
let j = Math.floor(Math.random() * 4); // 0到3
console.log(`选中索引 ${j}, 交换 shuffled[3] 和 shuffled[${j}]`);
[shuffled[3], shuffled[j]] = [shuffled[j], shuffled[3]];
console.log('结果:', shuffled);
// 第二轮:i = 2
console.log('\n第二轮 (i=2):');
console.log('在索引 0-2 中随机选择...');
j = Math.floor(Math.random() * 3); // 0到2
console.log(`选中索引 ${j}, 交换 shuffled[2] 和 shuffled[${j}]`);
[shuffled[2], shuffled[j]] = [shuffled[j], shuffled[2]];
console.log('结果:', shuffled);
// 第三轮:i = 1
console.log('\n第三轮 (i=1):');
console.log('在索引 0-1 中随机选择...');
j = Math.floor(Math.random() * 2); // 0到1
console.log(`选中索引 ${j}, 交换 shuffled[1] 和 shuffled[${j}]`);
[shuffled[1], shuffled[j]] = [shuffled[j], shuffled[1]];
console.log('最终结果:', shuffled);
}
explainFisherYates();
// 数学证明的简化版本
function whyItWorks() {
console.log('为什么Fisher-Yates算法是公平的:');
console.log('');
console.log('对于4个元素的数组:');
console.log('- 每个元素在第1位的概率: 1/4');
console.log('- 每个元素在第2位的概率: (3/4) * (1/3) = 1/4');
console.log('- 每个元素在第3位的概率: (3/4) * (2/3) * (1/2) = 1/4');
console.log('- 每个元素在第4位的概率: (3/4) * (2/3) * (1/2) * 1 = 1/4');
console.log('');
console.log('每个位置的概率都是1/4,完全公平!');
}
whyItWorks();
class PokerGame {
constructor() {
this.suits = ['♠', '♥', '♦', '♣'];
this.ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
this.deck = [];
this.players = [];
this.initializeDeck();
}
// 初始化牌组
initializeDeck() {
this.deck = [];
for (const suit ofthis.suits) {
for (const rank ofthis.ranks) {
this.deck.push({
rank,
suit,
value: this.getCardValue(rank),
display: `${rank}${suit}`
});
}
}
}
// 获取牌的数值(用于比较大小)
getCardValue(rank) {
if (rank === 'A') return1;
if (['J', 'Q', 'K'].includes(rank)) return10;
returnparseInt(rank);
}
// 洗牌
shuffleDeck() {
this.deck = ArrayUtils.shuffle(this.deck);
console.log('洗牌完成,牌组已随机化');
}
// 发牌
dealCards(playerCount, cardsPerPlayer) {
if (playerCount * cardsPerPlayer > this.deck.length) {
thrownewError('牌不够分配');
}
this.players = [];
for (let i = 0; i < playerCount; i++) {
const playerCards = [];
for (let j = 0; j < cardsPerPlayer; j++) {
const cardIndex = i * cardsPerPlayer + j;
playerCards.push(this.deck[cardIndex]);
}
this.players.push({
id: i + 1,
cards: playerCards,
handValue: this.calculateHandValue(playerCards)
});
}
returnthis.players;
}
// 计算手牌总值
calculateHandValue(cards) {
return cards.reduce((sum, card) => sum + card.value, 0);
}
// 开始新游戏
startNewGame(playerCount, cardsPerPlayer = 5) {
console.log(`开始新游戏: ${playerCount}个玩家,每人${cardsPerPlayer}张牌`);
this.initializeDeck();
this.shuffleDeck();
const players = this.dealCards(playerCount, cardsPerPlayer);
console.log('发牌结果:');
players.forEach(player => {
console.log(`玩家${player.id}: ${player.cards.map(c => c.display).join(', ')} (总值: ${player.handValue})`);
});
return players;
}
// 统计洗牌随机性(用于测试)
testShuffleRandomness(iterations = 1000) {
const positionCounts = {};
// 初始化计数器
this.deck.forEach((card, index) => {
positionCounts[card.display] = newArray(this.deck.length).fill(0);
});
for (let i = 0; i < iterations; i++) {
this.initializeDeck();
this.shuffleDeck();
this.deck.forEach((card, position) => {
positionCounts[card.display][position]++;
});
}
// 分析结果
console.log('洗牌随机性测试结果:');
const expectedFrequency = iterations / this.deck.length;
let totalDeviation = 0;
Object.entries(positionCounts).forEach(([card, positions]) => {
const deviations = positions.map(count =>Math.abs(count - expectedFrequency));
const avgDeviation = deviations.reduce((sum, dev) => sum + dev, 0) / deviations.length;
totalDeviation += avgDeviation;
if (avgDeviation > expectedFrequency * 0.1) { // 如果偏差超过10%
console.warn(`${card} 的分布可能不够随机,平均偏差: ${avgDeviation.toFixed(2)}`);
}
});
const overallDeviation = totalDeviation / this.deck.length;
console.log(`总体随机性评分: ${overallDeviation < expectedFrequency * 0.05 ? '优秀' : '一般'}`);
console.log(`平均偏差: ${overallDeviation.toFixed(2)} (期望: ${expectedFrequency.toFixed(2)})`);
}
}
// 使用示例
const game = new PokerGame();
// 开始一局游戏
game.startNewGame(4, 5);
// 测试洗牌随机性
game.testShuffleRandomness(1000);
class LotterySystem {
constructor() {
this.prizes = [];
this.participants = [];
this.history = [];
}
// 设置奖品和概率
setPrizes(prizeConfig) {
this.prizes = prizeConfig.map(config => ({
...config,
remainingCount: config.totalCount || Infinity
}));
// 验证概率总和
const totalProbability = this.prizes.reduce((sum, prize) => sum + prize.probability, 0);
if (Math.abs(totalProbability - 100) > 0.01) {
console.warn(`概率总和为 ${totalProbability}%,建议调整为100%`);
}
console.log('奖品设置完成:');
this.prizes.forEach(prize => {
console.log(`${prize.name}: ${prize.probability}% (剩余${prize.remainingCount}个)`);
});
}
// 添加参与者
addParticipants(users) {
this.participants = users.map(user => ({
...user,
participationCount: 0,
winHistory: []
}));
console.log(`已添加 ${this.participants.length} 个参与者`);
}
// 进行抽奖
drawLottery(userId) {
const participant = this.participants.find(p => p.id === userId);
if (!participant) {
thrownewError('参与者不存在');
}
// 获取有效奖品(还有库存的)
const availablePrizes = this.prizes.filter(prize => prize.remainingCount > 0);
if (availablePrizes.length === 0) {
return { result: 'no_prizes', message: '抱歉,奖品已全部发完' };
}
// 重新计算概率(基于剩余奖品)
const totalAvailableProbability = availablePrizes.reduce((sum, prize) => sum + prize.probability, 0);
const normalizedPrizes = availablePrizes.map(prize => ({
...prize,
normalizedProbability: (prize.probability / totalAvailableProbability) * 100
}));
// 使用加权随机选择
const prizeNames = normalizedPrizes.map(p => p.name);
const weights = normalizedPrizes.map(p => p.normalizedProbability);
const selectedPrizeName = ArrayUtils.weightedShuffle(prizeNames, weights)[0];
const wonPrize = this.prizes.find(p => p.name === selectedPrizeName);
// 更新数据
wonPrize.remainingCount--;
participant.participationCount++;
participant.winHistory.push({
prize: wonPrize.name,
timestamp: newDate(),
drawId: this.history.length + 1
});
// 记录抽奖历史
const drawRecord = {
drawId: this.history.length + 1,
userId: userId,
userName: participant.name,
prize: wonPrize.name,
timestamp: newDate()
};
this.history.push(drawRecord);
console.log(`🎉 ${participant.name} 抽中了 ${wonPrize.name}!`);
return {
result: 'success',
prize: wonPrize,
drawRecord,
remainingPrizes: this.getRemainingPrizes()
};
}
// 批量抽奖(适用于活动结束时的随机分配)
batchDraw(count) {
const shuffledParticipants = ArrayUtils.shuffle(this.participants);
const winners = [];
for (let i = 0; i < Math.min(count, shuffledParticipants.length); i++) {
const participant = shuffledParticipants[i];
try {
const result = this.drawLottery(participant.id);
if (result.result === 'success') {
winners.push(result);
}
} catch (error) {
console.error(`用户 ${participant.name} 抽奖失败:`, error.message);
}
}
console.log(`批量抽奖完成,共产生 ${winners.length} 个获奖者`);
return winners;
}
// 获取剩余奖品信息
getRemainingPrizes() {
returnthis.prizes.map(prize => ({
name: prize.name,
remaining: prize.remainingCount,
total: prize.totalCount || '无限',
probability: prize.probability + '%'
}));
}
// 生成抽奖统计报告
generateReport() {
const report = {
totalDraws: this.history.length,
prizeDistribution: {},
participantStats: {},
timeAnalysis: {}
};
// 奖品分布统计
this.history.forEach(record => {
report.prizeDistribution[record.prize] = (report.prizeDistribution[record.prize] || 0) + 1;
});
// 参与者统计
this.participants.forEach(participant => {
report.participantStats[participant.name] = {
participationCount: participant.participationCount,
winCount: participant.winHistory.length,
winRate: participant.participationCount > 0 ?
(participant.winHistory.length / participant.participationCount * 100).toFixed(2) + '%' : '0%',
prizes: participant.winHistory.map(w => w.prize)
};
});
// 时间分析(按小时统计)
this.history.forEach(record => {
const hour = record.timestamp.getHours();
report.timeAnalysis[hour] = (report.timeAnalysis[hour] || 0) + 1;
});
console.log('📊 抽奖统计报告:');
console.log('奖品分布:', report.prizeDistribution);
console.log('活跃时段:', report.timeAnalysis);
return report;
}
// 验证抽奖公平性
validateFairness(simulationCount = 10000) {
console.log(`开始公平性验证,模拟 ${simulationCount} 次抽奖...`);
const originalPrizes = JSON.parse(JSON.stringify(this.prizes));
const simulationResults = {};
// 重置奖品库存为无限
this.prizes.forEach(prize => {
prize.remainingCount = Infinity;
simulationResults[prize.name] = 0;
});
// 模拟抽奖
for (let i = 0; i < simulationCount; i++) {
const prizeNames = this.prizes.map(p => p.name);
const weights = this.prizes.map(p => p.probability);
const wonPrize = ArrayUtils.weightedShuffle(prizeNames, weights)[0];
simulationResults[wonPrize]++;
}
// 分析结果
console.log('公平性验证结果:');
this.prizes.forEach(prize => {
const actualRate = (simulationResults[prize.name] / simulationCount * 100).toFixed(2);
const expectedRate = prize.probability.toFixed(2);
const deviation = Math.abs(actualRate - expectedRate).toFixed(2);
console.log(`${prize.name}: 实际${actualRate}% vs 期望${expectedRate}% (偏差${deviation}%)`);
if (parseFloat(deviation) > 1) {
console.warn(`${prize.name} 的偏差较大,可能需要检查权重设置`);
}
});
// 恢复原始设置
this.prizes = originalPrizes;
}
}
// 使用示例
const lottery = new LotterySystem();
// 设置奖品
lottery.setPrizes([
{ name: '一等奖 - iPhone', probability: 1, totalCount: 1 },
{ name: '二等奖 - iPad', probability: 5, totalCount: 3 },
{ name: '三等奖 - 耳机', probability: 20, totalCount: 10 },
{ name: '参与奖 - 优惠券', probability: 74, totalCount: 100 }
]);
// 添加参与者
lottery.addParticipants([
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' },
{ id: 3, name: '王五', email: 'wang@example.com' },
{ id: 4, name: '赵六', email: 'zhao@example.com' }
]);
// 进行几次抽奖
lottery.drawLottery(1);
lottery.drawLottery(2);
lottery.drawLottery(3);
// 验证公平性
lottery.validateFairness(10000);
// 生成报告
lottery.generateReport();
class ABTestManager {
constructor() {
this.tests = newMap();
this.userAssignments = newMap();
}
// 创建A/B测试
createTest(testConfig) {
const test = {
id: testConfig.id,
name: testConfig.name,
description: testConfig.description,
variants: testConfig.variants.map(variant => ({
...variant,
assignedUsers: []
})),
trafficAllocation: testConfig.trafficAllocation || 100, // 参与测试的用户百分比
isActive: true,
createdAt: newDate(),
metrics: {}
};
// 验证变体权重总和
const totalWeight = test.variants.reduce((sum, variant) => sum + variant.weight, 0);
if (totalWeight !== 100) {
thrownewError(`变体权重总和必须为100%,当前为${totalWeight}%`);
}
this.tests.set(test.id, test);
console.log(`A/B测试 "${test.name}" 创建成功`);
return test;
}
// 为用户分配测试变体
assignUser(testId, userId, userInfo = {}) {
const test = this.tests.get(testId);
if (!test) {
thrownewError(`测试 ${testId} 不存在`);
}
if (!test.isActive) {
return { variant: null, reason: 'test_inactive' };
}
// 检查用户是否已经分配过
const existingAssignment = this.getUserAssignment(testId, userId);
if (existingAssignment) {
return existingAssignment;
}
// 确定用户是否参与测试(基于流量分配)
const participationRandom = Math.random() * 100;
if (participationRandom >= test.trafficAllocation) {
return { variant: null, reason: 'traffic_allocation' };
}
// 为了确保分配的一致性,使用用户ID作为随机种子
const userSeed = this.hashUserId(userId);
// 使用加权随机分配变体
const variantNames = test.variants.map(v => v.name);
const weights = test.variants.map(v => v.weight);
// 使用确定性的随机选择(基于用户ID)
const selectedVariant = this.deterministicWeightedChoice(variantNames, weights, userSeed);
const variant = test.variants.find(v => v.name === selectedVariant);
// 记录分配
const assignment = {
testId,
userId,
variant: variant.name,
assignedAt: newDate(),
userInfo,
events: []
};
variant.assignedUsers.push(userId);
if (!this.userAssignments.has(userId)) {
this.userAssignments.set(userId, newMap());
}
this.userAssignments.get(userId).set(testId, assignment);
console.log(`用户 ${userId} 被分配到测试 "${test.name}" 的变体 "${variant.name}"`);
return { variant: variant.name, assignment };
}
// 使用用户ID生成确定性的"随机"数
hashUserId(userId) {
let hash = 0;
const str = userId.toString();
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
returnMath.abs(hash) / 2147483648; // 归一化到0-1
}
// 确定性的加权选择
deterministicWeightedChoice(items, weights, seed) {
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
const target = seed * totalWeight;
let currentWeight = 0;
for (let i = 0; i < items.length; i++) {
currentWeight += weights[i];
if (target <= currentWeight) {
return items[i];
}
}
return items[items.length - 1]; // 兜底返回最后一个
}
// 获取用户的测试分配
getUserAssignment(testId, userId) {
const userTests = this.userAssignments.get(userId);
return userTests ? userTests.get(testId) : null;
}
// 记录用户事件
trackEvent(testId, userId, eventName, eventData = {}) {
const assignment = this.getUserAssignment(testId, userId);
if (!assignment) {
console.warn(`用户 ${userId} 未参与测试 ${testId}`);
returnfalse;
}
const event = {
name: eventName,
data: eventData,
timestamp: newDate()
};
assignment.events.push(event);
// 更新测试指标
const test = this.tests.get(testId);
if (!test.metrics[assignment.variant]) {
test.metrics[assignment.variant] = {};
}
if (!test.metrics[assignment.variant][eventName]) {
test.metrics[assignment.variant][eventName] = 0;
}
test.metrics[assignment.variant][eventName]++;
console.log(`记录事件: 用户${userId} 在变体"${assignment.variant}"中触发"${eventName}"`);
returntrue;
}
// 分析测试结果
analyzeTest(testId) {
const test = this.tests.get(testId);
if (!test) {
thrownewError(`测试 ${testId} 不存在`);
}
const analysis = {
testId,
testName: test.name,
totalUsers: 0,
variants: {},
summary: {}
};
// 收集各变体的数据
test.variants.forEach(variant => {
const userCount = variant.assignedUsers.length;
analysis.totalUsers += userCount;
const variantMetrics = test.metrics[variant.name] || {};
analysis.variants[variant.name] = {
userCount,
weightPercentage: variant.weight,
actualPercentage: 0, // 稍后计算
events: variantMetrics,
conversionRate: {}
};
});
// 计算实际分配百分比和转化率
Object.keys(analysis.variants).forEach(variantName => {
const variant = analysis.variants[variantName];
variant.actualPercentage = analysis.totalUsers > 0 ?
(variant.userCount / analysis.totalUsers * 100).toFixed(2) : 0;
// 计算各种转化率
Object.keys(variant.events).forEach(eventName => {
variant.conversionRate[eventName] = variant.userCount > 0 ?
(variant.events[eventName] / variant.userCount * 100).toFixed(2) : 0;
});
});
// 生成汇总报告
analysis.summary = this.generateTestSummary(analysis);
console.log('📈 A/B测试分析结果:');
console.log(`测试: ${analysis.testName}`);
console.log(`总用户数: ${analysis.totalUsers}`);
Object.entries(analysis.variants).forEach(([variantName, data]) => {
console.log(`\n变体 "${variantName}":`);
console.log(` 用户数: ${data.userCount} (${data.actualPercentage}%)`);
console.log(` 转化率:`, data.conversionRate);
});
if (analysis.summary.recommendation) {
console.log(`\n📋 建议: ${analysis.summary.recommendation}`);
}
return analysis;
}
// 生成测试汇总
generateTestSummary(analysis) {
const variants = Object.keys(analysis.variants);
if (variants.length < 2) {
return { recommendation: '需要至少2个变体才能进行比较' };
}
// 找出转化率最高的变体(以第一个事件为准)
const firstEventName = Object.keys(analysis.variants[variants[0]].events)[0];
if (!firstEventName) {
return { recommendation: '暂无足够数据进行分析' };
}
let bestVariant = variants[0];
let bestConversionRate = parseFloat(analysis.variants[variants[0]].conversionRate[firstEventName] || 0);
variants.forEach(variantName => {
const conversionRate = parseFloat(analysis.variants[variantName].conversionRate[firstEventName] || 0);
if (conversionRate > bestConversionRate) {
bestConversionRate = conversionRate;
bestVariant = variantName;
}
});
const summary = {
bestVariant,
bestConversionRate: bestConversionRate + '%',
recommendation: `变体 "${bestVariant}" 表现最佳,转化率为 ${bestConversionRate}%`
};
// 计算提升幅度
const baselineVariant = variants.find(v => v.toLowerCase().includes('control') || v.toLowerCase().includes('a')) || variants[0];
if (bestVariant !== baselineVariant) {
const baselineRate = parseFloat(analysis.variants[baselineVariant].conversionRate[firstEventName] || 0);
const improvement = ((bestConversionRate - baselineRate) / baselineRate * 100).toFixed(1);
summary.improvement = `相比基准变体提升 ${improvement}%`;
summary.recommendation += `,${summary.improvement}`;
}
return summary;
}
// 均匀分配用户到各个变体(用于初始化或重新平衡)
rebalanceTest(testId) {
const test = this.tests.get(testId);
if (!test) {
thrownewError(`测试 ${testId} 不存在`);
}
// 收集所有已分配的用户
const allAssignedUsers = [];
test.variants.forEach(variant => {
variant.assignedUsers.forEach(userId => {
allAssignedUsers.push(userId);
});
variant.assignedUsers = []; // 清空现有分配
});
// 重新随机分配
const shuffledUsers = ArrayUtils.shuffle(allAssignedUsers);
const variantNames = test.variants.map(v => v.name);
const weights = test.variants.map(v => v.weight);
shuffledUsers.forEach(userId => {
const selectedVariant = ArrayUtils.weightedShuffle(variantNames, weights)[0];
const variant = test.variants.find(v => v.name === selectedVariant);
variant.assignedUsers.push(userId);
// 更新用户分配记录
const assignment = this.getUserAssignment(testId, userId);
if (assignment) {
assignment.variant = selectedVariant;
assignment.rebalancedAt = newDate();
}
});
console.log(`测试 "${test.name}" 重新平衡完成`);
return test;
}
}
// 使用示例
const abTest = new ABTestManager();
// 创建一个A/B测试
abTest.createTest({
id: 'homepage_button_test',
name: '首页按钮颜色测试',
description: '测试不同颜色的CTA按钮对转化率的影响',
variants: [
{ name: 'control', weight: 50, description: '蓝色按钮(对照组)' },
{ name: 'red_button', weight: 25, description: '红色按钮' },
{ name: 'green_button', weight: 25, description: '绿色按钮' }
],
trafficAllocation: 80// 80%的用户参与测试
});
// 模拟用户访问和分配
const users = [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010];
users.forEach(userId => {
const assignment = abTest.assignUser('homepage_button_test', userId, {
source: 'organic',
device: 'desktop'
});
if (assignment.variant) {
// 模拟用户行为
const clickProbability = {
'control': 0.12,
'red_button': 0.15,
'green_button': 0.14
};
// 模拟点击事件
if (Math.random() < clickProbability[assignment.variant]) {
abTest.trackEvent('homepage_button_test', userId, 'button_click');
// 模拟转化事件
if (Math.random() < 0.3) { // 30%的点击会转化
abTest.trackEvent('homepage_button_test', userId, 'conversion');
}
}
}
});
// 分析测试结果
abTest.analyzeTest('homepage_button_test');
function compareShuffleMethods() {
const testArray = Array.from({ length: 1000 }, (_, i) => i);
const iterations = 10000;
// 错误的排序方法
console.time('错误的sort方法');
for (let i = 0; i < iterations; i++) {
[...testArray].sort(() =>Math.random() - 0.5);
}
console.timeEnd('错误的sort方法');
// Fisher-Yates方法
console.time('Fisher-Yates洗牌');
for (let i = 0; i < iterations; i++) {
ArrayUtils.shuffle(testArray);
}
console.timeEnd('Fisher-Yates洗牌');
// 多次随机交换方法
console.time('多次随机交换');
for (let i = 0; i < iterations; i++) {
const arr = [...testArray];
for (let j = 0; j < 100; j++) {
const a = Math.floor(Math.random() * arr.length);
const b = Math.floor(Math.random() * arr.length);
[arr[a], arr[b]] = [arr[b], arr[a]];
}
}
console.timeEnd('多次随机交换');
// 结果分析:
// 错误的sort方法: ~2000ms (最慢,且结果有偏差)
// Fisher-Yates洗牌: ~200ms (最快,且结果无偏差)
// 多次随机交换: ~800ms (中等速度,结果接近无偏差)
}
compareShuffleMethods();
// 优化版本的ArrayUtils,减少内存分配
class OptimizedArrayUtils extends ArrayUtils {
// 原地洗牌(修改原数组,节省内存)
static shuffleInPlace(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// 大数组优化的分块处理
static chunkLarge(array, size) {
if (array.length < 10000) {
returnsuper.chunk(array, size);
}
// 对于大数组,使用生成器节省内存
returnfunction* () {
for (let i = 0; i < array.length; i += size) {
yield array.slice(i, i + size);
}
}();
}
// 流式分组(适合大数据集)
static groupByStream(array, keyFn) {
const groups = newMap();
for (const item of array) {
const key = keyFn(item);
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(item);
}
returnObject.fromEntries(groups);
}
}
class RandomnessTest {
// 测试洗牌算法的随机性质量
static testShuffleQuality(shuffleFunction, arraySize = 10, iterations = 10000) {
const array = Array.from({ length: arraySize }, (_, i) => i);
const positionCounts = Array(arraySize).fill().map(() =>Array(arraySize).fill(0));
for (let i = 0; i < iterations; i++) {
const shuffled = shuffleFunction([...array]);
shuffled.forEach((value, position) => {
positionCounts[value][position]++;
});
}
// 计算期望频率
const expectedFrequency = iterations / arraySize;
// 计算卡方统计量
let chiSquare = 0;
positionCounts.forEach(counts => {
counts.forEach(count => {
chiSquare += Math.pow(count - expectedFrequency, 2) / expectedFrequency;
});
});
// 自由度 = (数组长度 - 1) * (位置数 - 1)
const degreesOfFreedom = (arraySize - 1) * (arraySize - 1);
console.log(`随机性测试结果:`);
console.log(`卡方统计量: ${chiSquare.toFixed(2)}`);
console.log(`自由度: ${degreesOfFreedom}`);
console.log(`期望频率: ${expectedFrequency}`);
// 简单的质量评估
const criticalValue = degreesOfFreedom * 1.5; // 简化的临界值
const quality = chiSquare < criticalValue ? '优秀' : '一般';
console.log(`随机性质量: ${quality}`);
return {
chiSquare,
degreesOfFreedom,
quality,
positionCounts
};
}
// 可视化随机性分布
static visualizeDistribution(testResult) {
console.log('\n随机性分布可视化:');
console.log('位置: 0 1 2 3 4 5 6 7 8 9');
testResult.positionCounts.forEach((counts, value) => {
const normalized = counts.map(count =>
Math.round(count / Math.max(...counts) * 10)
);
const bar = normalized.map(n =>'█'.repeat(n).padEnd(3)).join(' ');
console.log(`值${value}: ${bar}`);
});
}
}
// 测试我们的洗牌算法
console.log('测试Fisher-Yates洗牌算法:');
const result1 = RandomnessTest.testShuffleQuality(ArrayUtils.shuffle);
RandomnessTest.visualizeDistribution(result1);
console.log('\n测试错误的sort洗牌:');
const badShuffle = (arr) => arr.sort(() =>Math.random() - 0.5);
const result2 = RandomnessTest.testShuffleQuality(badShuffle);
RandomnessTest.visualizeDistribution(result2);
通过我们自制的数组处理神器,我们实现了:
核心算法优势:
功能完备性:
实际应用场景:
性能和质量保证:
这套数组处理工具不仅解决了日常开发中的随机化需求,更重要的是确保了算法的科学性和公平性。无论是游戏开发、抽奖活动还是A/B测试,都能提供可靠且高效的解决方案。
掌握了这些技术,你就能在需要随机化处理的场景中,自信地实现真正公平、高效的算法,再也不用担心"伪随机"带来的问题了!