基于时间分片的累加求和
问题描述
请实现一个函数 calculateSum(n)
,用于计算从 1 到 n
的累加和。n
可能是一个非常大的数字(例如小于 1,000,000,000)。
为了避免因长时间运行的计算任务而阻塞浏览器主线程,导致页面卡顿,该函数必须采用时间分片 (Time Slicing) 机制。
核心要求:
- 分批处理: 函数应将整个累加任务分割成多个小批次。
- 时间限制: 每个批次的处理时间不应超过 15 毫秒。在一个批次中,应尽可能多地执行累加操作。
- 异步执行: 当一个批次执行超时后,应立即停止当前批次的计算,并通过
setTimeout
将剩余的任务推迟到下一个宏任务中执行,从而让出主线程,保障页面响应性。 - 返回结果: 由于计算是异步的,函数必须返回一个
Promise
。当所有累加操作完成后,该Promise
应resolve
并返回最终的累加和。 - 禁用数学公式: 禁止直接通过数学公式(如
(n * (n + 1)) / 2
)一次性计算出结果,必须通过模拟逐步累加的过程来完成。 - 处理大数:
n
和最终的和可能会超过 JavaScriptNumber
类型的安全整数范围,需要使用BigInt
进行计算。
函数签名
/**
* 基于时间分片机制,计算从 1 到 n 的累加和。
* @param {number} n - 一个正整数,上限可以很大。
* @returns {Promise<bigint>} - 一个 Promise,在计算完成后解析为 BigInt 类型的总和。
*/
function calculateSum(n) {
// TODO: 请实现代码
}
示例用法
calculateSum(1000000).then((sum) => {
console.log('Sum:', sum);
// 预期输出: Sum: 500000500000n
});
calculateSum(2000000000).then((sum) => {
console.log('Large Sum:', sum);
// 预期输出: Large Sum: 2000000001000000000n
});
解题思路
要实现带时间分片的异步计算,核心思想是将一个大的同步循环改造成多个小的、异步执行的单元。
异步返回: 由于计算过程被拆分到多个事件循环周期中,函数无法立即返回最终结果。因此,它必须返回一个
Promise
,并在所有计算完成后resolve
这个Promise
。状态管理: 我们需要在多个计算批次之间维护状态:
sum
: 当前的累加和。由于结果会非常大,必须使用BigInt
(例如0n
)。i
: 当前累加到的数字。同样,使用BigInt
(例如1n
)。n_big
: 目标数字n
的BigInt
版本,用于比较。
创建工作单元函数: 定义一个内部函数,例如
performChunk()
,它将负责执行一个批次的计算。时间分片逻辑: 在
performChunk()
函数内部:- 记录开始时间:使用
performance.now()
或Date.now()
获取当前时间戳作为批次的开始时间。 - 执行循环:在一个
while
循环中持续进行累加操作 (sum += i; i++;
)。 - 设置跳出条件:
while
循环的条件应该有两个:i <= n_big
:确保计算没有完成。performance.now() - startTime < 15
:检查从批次开始到现在所经过的时间是否在 15 毫秒的限制内。
- 当循环因为超时而跳出时,说明这个批次的工作已经完成。
- 记录开始时间:使用
调度下一个任务:
- 在
performChunk()
的末尾,检查计算是否已全部完成(即i > n_big
)。 - 如果已完成,调用
Promise
的resolve
方法,并传入最终的sum
。 - 如果未完成,使用
setTimeout(performChunk, 0)
将下一次的performChunk
调用安排到宏任务队列中。这会将控制权交还给主线程,允许浏览器处理渲染、用户交互等其他任务,然后在下一个事件循环中继续我们的计算。
- 在
启动计算: 在
calculateSum
函数的主体中,调用一次performChunk()
来启动整个计算流程。
这种“工作-检查-调度”的模式是实现时间分片、避免阻塞主线程的关键。
代码实现
/**
* 基于时间分片机制,计算从 1 到 n 的累加和。
* @param {number} n - 一个正整数,上限可以很大。
* @returns {Promise<bigint>} - 一个 Promise,在计算完成后解析为 BigInt 类型的总和。
*/
function calculateSum(n) {
// 函数立即返回一个 Promise
return new Promise((resolve) => {
// 使用 BigInt 来处理可能超出 Number 安全范围的大数
let sum = 0n;
let i = 1n;
const n_big = BigInt(n);
// 每个计算批次的时间限制(毫秒)
const timeLimit = 15;
/**
* 执行一个计算批次的函数
*/
function performChunk() {
const startTime = performance.now();
// 在时间限制内,并且未计算完成时,持续执行累加
while (i <= n_big && performance.now() - startTime < timeLimit) {
sum += i;
i++;
}
// 检查是否所有累加都已完成
if (i > n_big) {
// 如果完成,则 resolve Promise 并返回最终结果
resolve(sum);
} else {
// 如果未完成,将下一个计算批次推迟到宏任务队列中
// 这会将控制权交还给主线程,避免页面卡顿
setTimeout(performChunk, 0);
}
}
// 启动第一个计算批次
performChunk();
});
}
// --- 测试用例 ---
console.log('开始计算 1 到 1,000,000 的和...');
calculateSum(1000000).then((sum) => {
console.log('Sum (1 to 1,000,000):', sum); // 预期: 500000500000n
console.log('预期结果与实际结果是否一致:', sum === 500000500000n);
});
console.log('开始计算 1 到 2,000,000,000 的和... (这可能需要一些时间)');
calculateSum(2000000000).then((sum) => {
console.log('Sum (1 to 2,000,000,000):', sum); // 预期: 2000000001000000000n
});
// 在计算大数时,页面仍然可以响应
let count = 0;
const intervalId = setInterval(() => {
console.log(`主线程没有被阻塞,这是第 ${++count} 次心跳检查。`);
}, 500);
// 当最大的计算任务完成时,停止心跳检查
calculateSum(2000000000).then(() => {
clearInterval(intervalId);
console.log('所有计算任务完成,心跳检查已停止。');
});