是否存在将线性索引转换为支持负步幅的下标列表的算法?
背景
环境(如MATLAB,Julia等)和库(如NumPy)为跨步数组(也称为ndarrays)提供支持。跨步阵列由线性存储器(例如,单个底层缓冲器)支持,其与嵌套阵列形成对比,其中每个嵌套阵列对应于维度。例如,考虑以下2x2矩阵
[ 1 2
3 4 ]
实现为数组数组
A = [ [ 1, 2 ], [ 3, 4 ] ]
其中(使用从零开始的索引)
a01 = A[0][1] = 2
我们可以表示与步进数组相同的2x2矩阵,如下所示(假设为行主)
A = [ 1, 2,
3, 4 ]
,其中
a01 = A[ 2*0 + 1*1 ] = 2
通常,对于跨步的NxM矩阵,可以通过
访问元素(i,j)
function get( i, j ) {
return buffer[ si*i + sj*j ];
}
其中buffer
是基础数据缓冲区,si
和sj
分别对应i
和j
维度的步幅。假设行主要跨步数组,对于上面的2x2矩阵,si = 2
和sj = 1
(省略元素字节长度)。
通常,步幅可以从阵列形状计算如下:
function shape2strides( shape, order ) {
var out = new Array( shape.length );
var s = 1;
var i;
if ( order === 'column-major' ) {
for ( i = 0; i < shape.length; i++ ) {
out[ i ] = shape[ i ];
s *= shape[ i ];
}
return out;
} else { // row-major
for ( i = shape.length-1; i >= 0; i-- ) {
out[ i ] = shape[ i ];
s *= shape[ i ];
}
}
}
为了便于使用跨步数组,环境/库通常提供便利功能,允许在线性索引和下标之间轻松转换。例如,在MATLAB中,要从下标转换为线性索引
idx = sub2ind( size( A ), i, j )
同样,要从MATLAB
中的线性索引转换为下标s = ind2sub( size( A ), idx )
朱莉娅还有sub2ind和ind2sub。在NumPy中,您可以使用ravel_multi_index和unravel_index。
除了数据局部性之外,跨步数组也很方便,因为它们允许创建数组&#34;视图&#34;通过操纵步幅是负面还是正面。当步幅为负时,我们不是从左向右迭代,而是沿着该维度从右向左迭代。为了支持这种迭代行为,我们需要确定底层数据缓冲区中第一个索引元素的位置。按照惯例,我们将此索引称为&#34; offset&#34;,可以按如下方式计算
function strides2offset( shape, strides ) {
var offset = 0;
var i;
for ( i = 0; i < shape.length; i++ ) {
if ( strides[ i ] < 0 ) {
offset -= strides[i] * ( shape[i]-1 ); // increments the offset
}
}
return offset;
}
一旦我们得到了偏移量,我们需要修改我们的get( i, j )
函数,如下所示
function get( i, j ) {
return buffer[ offset + si*i + sj*j ];
}
对于步幅为2,1
的2x2矩阵 A ,偏移量为0
,从而返回上面的原始get
函数。当步幅为2,-1
时,偏移量为1
;对于-2,1
,偏移量为2
;对于-2,-1
,偏移量为3
。因此,我们可以生成以下矩阵视图(假设为行主要)
Dims: 2x2
Strides: 2,1
Offset: 0
A = [ 1, 2,
3, 4 ]
Strides: 2,-1
Offset: 1
A = [ 2, 1,
4, 3 ]
Strides: -2,1
Offset: 2
A = [ 3, 4,
1, 2 ]
Strides: -2,-1
Offset: 3
A = [ 4, 3,
2, 1 ]
以上观点强调了跨步阵列的一个优点:O(1)操作。例如,要从左到右翻转矩阵,我们只需要翻转第二维的步幅(假设为row-major)。为了向上翻转,我们翻转第一维的步幅(假设为行主)。为了从左到右,从上到下翻转,我们翻转了两个步幅的标志。所有上述操作都不涉及触及底层数据缓冲区;我们只需更改跨步数组元数据。
sub2ind
即使在考虑负步幅(即跨步数组视图)时,从下标转换为线性索引也很简单。例如,对于任意维度的跨步数组,
function sub2ind( ...subscripts ) {
var sub;
var idx;
var s;
var n;
idx = offset;
for ( n = 0; n < shape.length; n++ ) {
sub = subscripts[ n ];
s = strides[ n ];
if ( s < 0 && offset === 0 ) { // assume want "view" index
idx -= sub * s; // always increments `idx`
} else { // assume want underlying data buffer index
idx += sub * s; // may increment or decrement `idx`
}
}
return idx;
}
这里,我们允许从视图的角度或从底层数据缓冲区的角度返回线性索引。当&#34;偏移&#34;是0
,我们假设我们总是将一个线性索引返回到视图中(可能不对应于底层数据缓冲区中的线性索引)。换句话说,对于2x2矩阵视图,(0,0) => 0, (0,1) => 1, (1,0) => 2, (1,1) => 3
,始终。从使用视图的角度来看,这种映射与直觉一致是有道理的。当我想要A(0,0)
时,我希望元素位于&#34;第一个&#34;线性索引,即使这不是该元素实际存储在底层数据缓冲区中的地方。
您可以向自己证明,sub2ind
在将元素查找扩展到负步幅时,会为上述任何偏移返回相同的索引。
ind2sub
这里要问的问题是如何实现
sub2ind
的反向,支持消极的步伐。
对于正步幅(因此,0
的偏移量),我们可以使用模运算来恢复下标。例如,考虑用于解析NxMxL跨越阵列的线性索引的等式。
idx = offset + si*i + sj*j + sk*k
其中,假设行主要,si = nj*nk, sj = nk, sk = 1
和ni, nj, nk
分别是维度大小N, M, L
。替换值,
idx = 0 + (nj*nk)*i + nk*j + k
可以重新排列
idx = nk*(nj*i + j) + k
如果我们使用nk
,
idx % nk = k
了解k
,让我们重新排列初始等式
(idx - k) = nk*(nj*i + j)
(idx - k)/nk = nj*i + j
如果我们使用nj
,
((idx - k)/nk) % nj = j
了解j
,让我们重新排列初始等式以求解i
(((idx - k)/nk) - j)/nj = i
上述算法可以推广到任意数量的维度,并且可以直接实现(另请参阅Julia和NumPy)。
function ind2sub( idx, order ) {
var out = new Array( shape.length );
var s;
var i;
if ( order === 'column-major' ) {
for ( i = 0; i < shape.length; i++ ) {
s = idx % shape[ i ];
idx -= s;
idx /= shape[ i ];
out[ i ] = s;
}
} else { // row-major
for ( i = shape.length-1; i >= 0; i-- ) {
s = idx % shape[ i ];
idx -= s;
idx /= shape[ i ];
out[ i ] = s;
}
}
return out;
}
然而,使用模运算的上述算法不支持负步幅。如果我们使用上面相同的程序来解决下标i,j,k
,我们将从等式开始
idx = offset + nk*(nj*i + j) + k
可以简化为
idx-offset = nk*(nj*i + j) + k
这里的问题当然是idx-offset
可能是负面的并且有效地改变了可能i,j,k
值的范围(i
应该在半开区间[0, N);区间[0,M]上的j
;和[{1}}在区间[0,L)上。
然后,这提示了是否存在用于将线性索引转换为支持负步幅的下标的算法的问题。或者换句话说,是否存在一种算法,给定基础数据缓冲区的线性索引,可以返回相应的视图下标?
其他语言/库(例如Julia和NumPy)中的实现似乎只为k
案例提供支持。我正在寻找更通用的东西,它也适用于跨步数组视图。
非常感谢任何指向现有实现/算法的指针。
答案 0 :(得分:0)
(编辑 - 我可能正在处理更容易的nd-index到平面索引的情况,而你则专注于反向。现在探索这个任务为时已晚 - 我将重新审视这个问题。早上。)
如果偏移是正确的,我认为将n-d指数转换为平面索引的相同公式适用于消极和正向步幅:
将(3,4)数组的索引与其双重翻转进行比较:
In [32]: x = np.arange(12).reshape(3,4)
In [33]: y = x[::-1, ::-1]
In [34]: x.strides
Out[34]: (16, 4)
In [35]: y.strides
Out[35]: (-16, -4)
数据缓冲区&#39;开始&#39;可以在__array_interface__
找到:
In [36]: x.__array_interface__['data']
Out[36]: (166934688, False)
In [37]: y.__array_interface__['data']
Out[37]: (166934732, False)
In [38]: 732-688
Out[38]: 44
缓冲区中有48个字节,但y
的偏移量为44,即x [2,3]元素的开头(本例中为11)。
现在测试x
元素的平面索引:
In [39]: x[1,2]
Out[39]: 6 # value
In [40]: x[1:2, 2:3].__array_interface__['data'] # a view
Out[40]: (166934712, False)
In [41]: 688+(1*16)+(2*4) # offset + si*i + sj*j
Out[41]: 712
现在对y
执行相同操作:
In [42]: y[1:2, 2:3].__array_interface__['data']
Out[42]: (166934708, False)
In [43]: 732+(1*-16)+(2*-4)
Out[43]: 708
答案 1 :(得分:0)
我已经编写了一段代码,我认为这些代码可以解决您的问题。您要求的功能不止一个,因为其他功能与您的功能略有不同,所以我将所有这些功能放在这里以避免潜在的不一致。
以下代码为不是用任何特定语言写的。但是,您可以找到C语法的一些元素。
function calcStrides(shape[]) {
strides[]; # Result array
currentStride = 1;
for(i = shape.size; 0 < i;) {
--i;
if(0 < shape[i]) {
strides[i] = currentStride;
currentStride *= shape[i];
} else {
strides[i] = -currentStride;
currentStride *= -shape[i];
}
}
return strides;
}
function calcOffset(shape[], strides[]) {
offset = 0;
for(i = 0; i < shape.size; ++i) {
if(shape[i] < 0) {
offset += strides[i] * (shape[i] + 1);
}
}
return offset;
}
function sub2ind(strides[], offset, subs[]) {
ind = offset;
for(i = 0; i < strides.size; ++i) {
ind += strides[i] * subs[i];
}
return ind;
};
function ind2sub(shape[], strides[], ind) {
subs[]; # Result array
for(i = 0; i < shape.size; ++i) {
if(0 < strides[i]) {
subs[i] = ind / strides[i];
ind -= subs[i] * strides[i];
} else {
absSub = ind / -strides[i];
subs[i] = -shape[i] - 1 - absSub;
ind -= absSub * -strides[i];
}
}
return subs;
}