文章目录
- 一、为什么需要前缀和?
- 二、前缀和核心原理
- 1. 空间换时间的艺术
- 2. 数学之美
- 三、Vue3 中的实现
- 1. Composition API 封装
- 2. 在组件中使用
- 四、应用场景
- 1. 动态数据仪表盘
- 2. 大数据量滚动加载
- 3. 动态规划优化
- 五、性能对比实测
- 六、注意事项
- 1. 索引偏移陷阱
- 2. 动态更新处理
- 3. 内存优化技巧
- 七、总结
一、为什么需要前缀和?
在开发可视化仪表盘时会遇到这样的请求:
// 每秒更新 100,000 条交易数据的区间统计
const transactions = ref(new Array(100000).fill(0))
需要实时计算 [2000, 5000] 区间的交易总额,之间遍历会导致:
// 时间复杂度 O(n) 的暴力解法
function sumRange(start, end) {let sum = 0for (let i = start; i <= end; i++) {sum += transactions.value[i]}return sum // 每调用一次就遍历一次,性能灾难!
}
二、前缀和核心原理
前缀和(Prefix Sum)是一种预处理数组的技术,通过构建辅助数组快速计算原数组任意区间的和。
数学表达:
原数组:nums[0], nums[1], nums[2],…,nums[n - 1]
前缀和数组:preSum[0],preSum[1], preSum[2],…,preSum[n]
其中 preSum[i] = nums[0] + nums[1] + … + nums[i - 1]
构建过程:
初始化:preSum[0] = 0 (空数组的和)
递推公式:preSum[i] = preSum[i - 1] + nums[i - 1]
区间和计算:
计算区间[left, right]的和:
sum(nums[left…right]) = preSum[right + 1] - preSum[left]
1. 空间换时间的艺术
// 预处理阶段(O(n))
const preSum = reactive([])
preSum[0] = 0
for (let i = 1; i <= transactions.value.length; i++) {preSum[i] = preSum[i - 1] + transactions.value[i - 1]
}// 查询阶段 (O(1)的复杂度)
function optimizedSum(start, end) {return preSum[end + 1] - preSum[start]
}
2. 数学之美
三、Vue3 中的实现
1. Composition API 封装
// usePrefixSum.ts
import { ref, reactive, watchEffect } from 'vue'export function usePrefixSum(sourceArray: Ref<number[]>) {const preSum = reactive<number[]>([])watchEffect(() => {preSum.length = 0preSum.push(0)sourceArray.value.forEach((num, index) => {preSum[index + 1] = preSum[index] + num})})const getRangeSum = (start: number, end: number) => {return preSum[end + 1] - preSum[start]}return { preSum, getRangeSum }
}
2. 在组件中使用
<script setup lang="ts">import { ref, computed } from 'vue'import { usePrefixSum } form './usePrefixSum'const data = ref([10, 20, 30, 40, 50])const { getRangeSum } = usePrefixSum(data)// 实时响应式计算const currentSum = computed(() => getRangeSum(1, 3)) // 20 + 30 + 40 = 90
</script>
四、应用场景
1. 动态数据仪表盘
// 实时更新股票波动区间统计
watch(stockData, (newVal) => {data.value = newVal.map((item) => item.price)
})
2. 大数据量滚动加载
<template><!-- 显示当前可视区域的数据总和 --><div v-for="(chunk, i) in visibleChunks" :key="i">区块{{i}}总和:{{getRangeSum(...getChunkRange(i))}}</div>
</template>
3. 动态规划优化
// 最长子数组和问题优化
function maxSubArray(nums: number[]) {const preSum = [0]let minSum = 0let max = -Infinitynums.forEach((num, index) => {preSum[i + 1] = preSum[i] + nummax = Math.max(max, preSum[i + 1] - minSum)minSum = Math.min(minSum, preSum[i + 1])})return max
}
五、性能对比实测
测试数据:100,000 个元素的数组
方法 | 初始化耗时 | 10 | 万次查询总耗时 |
---|---|---|---|
暴力求和 | 0ms | 1250ms | |
前缀和 | 15ms | 3ms |
结论 :在频繁查询场景下,前缀和的性能提升可达 400 倍!
六、注意事项
1. 索引偏移陷阱
记住 preSum[n]表示前 n 项和:
// 正确写法
sumRange(a,b) => preSum[b + 1] - preSum[a]
2. 动态更新处理
当原始数组变化时:
// 使用 watchEffect 自动重建前缀和
watchEffect(updatePreSum)
3. 内存优化技巧
对大数组使用 Int32Array:
const preSum = new Int32Array(data.length + 1)
七、总结
除了上述的应用场景外,我们还可以进行其他的扩展思考,比如说,差分数组:前缀和的逆运算,适合区间更新场景;树状数组:动态前缀和的更优解,支持单点修改等等。