不断增长的数组的内存高效下采样(图表)
Memory-efficient downsampling (charting) of a growing array
我的一个节点进程每半秒接收一个样本点,我想更新我接收到的所有样本点的历史图表。
该图表应该是一个数组,其中包含从 0 到当前点的所有点的下采样历史记录。
也就是说,数组的最大长度应该是l
。如果我收到的样本点多于 l
,我希望图表数组是整个历史的下采样到 l
版本。
用代码表示:
const CHART_LENGTH = 2048
createChart(CHART_LENGTH)
onReceivePoint = function(p) {
// p can be considered a number
const chart = addPointToChart(p)
// chart is an array representing all the samples received, from 0 to now
console.assert(chart.length <= CHART_LENGTH)
}
我已经有了一个数字数组的下采样函数:
function downsample (arr, density) {
let i, j, p, _i, _len
const downsampled = []
for (i = _i = 0, _len = arr.length; _i < _len; i = ++_i) {
p = arr[i]
j = ~~(i / arr.length * density)
if (downsampled[j] == null) downsampled[j] = 0
downsampled[j] += Math.abs(arr[i] * density / arr.length)
}
return downsampled
}
这样做的一个简单方法显然是将我收到的所有点保存到一个数组中,并在数组增长时应用 downsample
函数。这可行,但是,由于这段代码会 运行 在服务器中,可能连续几个月,它最终会使支持数组增长得如此之多,以至于进程内存不足。
问题是:有没有办法重新使用图表本身的先前内容来构造图表数组,以避免维护不断增长的数据结构?换句话说,这个问题有内存复杂度不变的解决方案吗?
请注意图表必须随时包含自样本点 #0 以来的整个历史,因此绘制最后 n 个点的图表是不可接受的。
唯一不会扭曲数据并且可以多次使用的操作是聚合整数个相邻样本。你可能想要 2.
更具体地说:如果您发现添加新样本会超出数组边界,请执行以下操作:从数组的开头开始并对两个后续样本进行平均。这会将数组大小减少 2,并且您有 space 添加新样本。这样做,您应该跟踪当前的簇大小 c
(构成数组中一个条目的样本数量)。你从一个开始。每次减少都会将簇大小乘以二。
现在的问题是您不能再将新样本直接添加到数组中,因为它们具有完全不同的比例。相反,您应该将接下来的 c
个样本平均到一个新条目。事实证明,在当前集群中存储样本数n
就足够了。因此,如果您添加一个新示例 s
,您将执行以下操作。
n++
if n = 1
append s to array
else
//update the average
last array element += (s - last array element) / n
if n = c
n = 0 //start a new cluster
所以你实际需要的内存如下:
- 具有预定义长度的历史数组
- 历史数组中的元素个数
- 当前簇大小
c
- 当前簇中的元素个数
n
附加内存的大小不依赖于样本总数,因此O(1)
。
我的一个节点进程每半秒接收一个样本点,我想更新我接收到的所有样本点的历史图表。
该图表应该是一个数组,其中包含从 0 到当前点的所有点的下采样历史记录。
也就是说,数组的最大长度应该是l
。如果我收到的样本点多于 l
,我希望图表数组是整个历史的下采样到 l
版本。
用代码表示:
const CHART_LENGTH = 2048
createChart(CHART_LENGTH)
onReceivePoint = function(p) {
// p can be considered a number
const chart = addPointToChart(p)
// chart is an array representing all the samples received, from 0 to now
console.assert(chart.length <= CHART_LENGTH)
}
我已经有了一个数字数组的下采样函数:
function downsample (arr, density) {
let i, j, p, _i, _len
const downsampled = []
for (i = _i = 0, _len = arr.length; _i < _len; i = ++_i) {
p = arr[i]
j = ~~(i / arr.length * density)
if (downsampled[j] == null) downsampled[j] = 0
downsampled[j] += Math.abs(arr[i] * density / arr.length)
}
return downsampled
}
这样做的一个简单方法显然是将我收到的所有点保存到一个数组中,并在数组增长时应用 downsample
函数。这可行,但是,由于这段代码会 运行 在服务器中,可能连续几个月,它最终会使支持数组增长得如此之多,以至于进程内存不足。
问题是:有没有办法重新使用图表本身的先前内容来构造图表数组,以避免维护不断增长的数据结构?换句话说,这个问题有内存复杂度不变的解决方案吗?
请注意图表必须随时包含自样本点 #0 以来的整个历史,因此绘制最后 n 个点的图表是不可接受的。
唯一不会扭曲数据并且可以多次使用的操作是聚合整数个相邻样本。你可能想要 2.
更具体地说:如果您发现添加新样本会超出数组边界,请执行以下操作:从数组的开头开始并对两个后续样本进行平均。这会将数组大小减少 2,并且您有 space 添加新样本。这样做,您应该跟踪当前的簇大小 c
(构成数组中一个条目的样本数量)。你从一个开始。每次减少都会将簇大小乘以二。
现在的问题是您不能再将新样本直接添加到数组中,因为它们具有完全不同的比例。相反,您应该将接下来的 c
个样本平均到一个新条目。事实证明,在当前集群中存储样本数n
就足够了。因此,如果您添加一个新示例 s
,您将执行以下操作。
n++
if n = 1
append s to array
else
//update the average
last array element += (s - last array element) / n
if n = c
n = 0 //start a new cluster
所以你实际需要的内存如下:
- 具有预定义长度的历史数组
- 历史数组中的元素个数
- 当前簇大小
c
- 当前簇中的元素个数
n
附加内存的大小不依赖于样本总数,因此O(1)
。