排序在这里是多余的。 这只是一个具有恒定内存要求的单次线性时间算法:
from __future__ import print_function
import numpy as np
p = np.array([3, 2, 0, 1])
s = np.empty(p.size, dtype=np.int32)
for i in np.arange(p.size):
s[p[i]] = i
print('s =', s)
上面的代码打印
s = [2 3 1 0]
按要求。
答案的其余部分与上述for
循环的有效矢量化有关。如果您只是想知道解决方案,请跳到此答案的末尾。
(2014 年 8 月 27 日的原始答案;时间对 NumPy 1.8 有效。稍后会更新 NumPy 1.11。)
单程线性时间算法预计比np.argsort
; 有趣的是,上述循环的微不足道的矢量化(s[p] = xrange(p.size)
见索引数组for
)实际上比np.argsort
只要p.size < 700 000
(好吧,在我的机器上,你的里程会有所不同)稍慢:
import numpy as np
def np_argsort(p):
return np.argsort(p)
def np_fancy(p):
s = np.zeros(p.size, p.dtype) # np.zeros is better than np.empty here, at least on Linux
s[p] = xrange(p.size)
return s
def create_input(n):
np.random.seed(31)
indices = np.arange(n, dtype = np.int32)
return np.random.permutation(indices)
从我的 IPython 笔记本中:
p = create_input(700000)
%timeit np_argsort(p)
10 loops, best of 3: 72.7 ms per loop
%timeit np_fancy(p)
10 loops, best of 3: 70.2 ms per loop
最终,渐近复杂性开始出现(O(n log n)
对于argsort
单通道算法),并且单通道算法在足够大(我的机器上的阈值约为 700k)O(n)
之后将始终更快。n = p.size
但是,有一种不太直接的方法可以使用以下方法对上述for
循环进行矢量化np.put
:
def np_put(p):
n = p.size
s = np.zeros(n, dtype = np.int32)
i = np.arange(n, dtype = np.int32)
np.put(s, p, i) # s[p[i]] = i
return s
这给出了n = 700 000
(与上面相同的大小):
p = create_input(700000)
%timeit np_put(p)
100 loops, best of 3: 12.8 ms per loop
这是一个不错的 5.6 倍加速,几乎没有!
公平地说,np.argsort
仍然优于np.put
较小的方法n
(引爆点n = 1210
在我的机器上):
p = create_input(1210)
%timeit np_argsort(p)
10000 loops, best of 3: 25.1 µs per loop
%timeit np_fancy(p)
10000 loops, best of 3: 118 µs per loop
%timeit np_put(p)
10000 loops, best of 3: 25 µs per loop
这很可能是因为我们np.arange()
使用该方法分配并填充了一个额外的数组(在调用时)np_put
。
尽管您没有要求 Cython 解决方案,但出于好奇,我还使用键入的 memoryviews为以下 Cython 解决方案计时:
import numpy as np
cimport numpy as np
def in_cython(np.ndarray[np.int32_t] p):
cdef int i
cdef int[:] pmv
cdef int[:] smv
pmv = p
s = np.empty(p.size, dtype=np.int32)
smv = s
for i in xrange(p.size):
smv[pmv[i]] = i
return s
时间:
p = create_input(700000)
%timeit in_cython(p)
100 loops, best of 3: 2.59 ms per loop
所以,np.put
解决方案仍然没有尽可能快(这个输入大小运行 12.8 毫秒;argsort 花了 72.7 毫秒)。
NumPy 1.11 于 2017 年 2 月 3 日更新
Jamie、Andris 和 Paul 在下面的评论中指出,花式索引的性能问题已经解决。Jamie 说它已经在 NumPy 1.9 中解决了。我在 2014 年使用的机器上使用 Python 3.5 和 NumPy 1.11 对其进行了测试。
def invert_permutation(p):
s = np.empty(p.size, p.dtype)
s[p] = np.arange(p.size)
return s
时间:
p = create_input(880)
%timeit np_argsort(p)
100000 loops, best of 3: 11.6 µs per loop
%timeit invert_permutation(p)
100000 loops, best of 3: 11.5 µs per loop
确实是一个显着的进步!
结论
总而言之,我会选择
def invert_permutation(p):
'''The argument p is assumed to be some permutation of 0, 1, ..., len(p)-1.
Returns an array s, where s[i] gives the index of i in p.
'''
s = np.empty_like(p)
s[p] = np.arange(p.size)
return s
代码清晰的方法。在我看来,它比 不那么晦涩argsort
,并且对于大输入大小也更快。如果速度成为问题,我会选择 Cython 解决方案。