用Python优化列表操作代码

用Python优化列表操作代码,python,list,optimization,Python,List,Optimization,我有一个简单的代码来分组和计算列表的内容。我担心完成这项工作所需的时间。只有不到100个项目,速度足够快,但数百个项目使我的Mac尖叫。我从真实世界的数据中得到了多达10000个项目。因此,我需要知道如何优化此代码: v = [1,2,3,4,5,6,7,8,9,10] v1 = v[:5] l = len(v1) a = [sum(v1[x:y]) for y in range(l+1) for x in range(y)] d = {x:a.count(x) for x in a} 所以在

我有一个简单的代码来分组和计算列表的内容。我担心完成这项工作所需的时间。只有不到100个项目,速度足够快,但数百个项目使我的Mac尖叫。我从真实世界的数据中得到了多达10000个项目。因此,我需要知道如何优化此代码:

v = [1,2,3,4,5,6,7,8,9,10]
v1 = v[:5]
l = len(v1)
a = [sum(v1[x:y]) for y in range(l+1) for x in range(y)]
d = {x:a.count(x) for x in a}
所以在v上有一个整数列表。数字可以是1到4000。示例中的列表长度是10,但如上所述,它将变为10000。v1只是一个拆分列表,用于处理较小的测试数据

a将每个项作为单个实例获取,并将前面的所有项作为实例获取:

[1, 3, 2, 6, 5, 3, 10, 9, 7, 4, 15, 14, 12, 9, 5]
d将所有项目分组,并将其作为键值对字典进行计数:

{1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 9: 2, 10: 1, 12: 1, 14: 1, 15: 1}
100+项之后,计算a似乎已经变得非常繁重。a上有500个项目,我得到了d的276813个实例。所以当列表上有10000个项目时,我预计d上最多会有5559333个项目,a上可能会多出100倍

更新

根据下面的评论和回答,通过以下实施进行了一些改进:

def t5(v1):
    l = len(v1)
    d = {}
    for x in range(0, l):
       s = sum(v1[x:])
       if s in d:
          d[s] += 1
       else:
          d[s] = 1
       for y in range(1, l-x):
           s -= v1[-y]
           if s in d:
               d[s] += 1
           else:
               d[s] = 1
    return d

我不知道如何使用numpy和/或numba进行更多优化。可能适合不同的单独问题…

您不断地重新计算之前已经计算过的切片的总和(每个
sum()
调用一个冗余循环),此外,您还通过一直切片来创建新列表对象的负载

您可以使用动态规划来计算总和,从输入列表的末尾开始;您只需将“当前”值添加到列表中下一个值的总和中:

# v1 is the input list, l is len(v1)
sums = [0] * (l * (l + 1) // 2)
for x in range(l - 1, -1, -1):
    for y in range(x, l):
         i = (y * (y + 1) // 2) + x
         sums[i] += v1[x] + (sums[i + 1] if x < y else 0)
(空格是反向切片,重复,不用于输出)。对于每一行和每一列,可以通过计算该列前面三角形中适合的值的数量加上行号来计算输出索引,或者
((col*col+1)//2)+行)

动态规划方法将后续值(索引下方的行)的已生成切片的总和添加到当前行的
v1
值:

v1     0  1  2  3  4

1 > 0  1  3  6 10 15
          ^  ^  ^  ^  
2 > 1     2  5  9 14
             ^  ^  ^
3 > 2        3  7 12
                ^  ^
4 > 3           4  9
                   ^ 
5 > 4              5
因此,第2行第4列的值是
v1[2]
加上第3行第4列已经计算的总和,或者在本例中是
2+12
。对于行索引小于列索引的元素,只有一个已经计算的和;在位置(3,3)处,在(4,3)处没有计算出的总和

下一个问题是,
list.count()
遍历整个总和列表,以计算一个数字出现的频率。因此,表达式中x的
{x:a.count(x)创建了一个O(N^2)双循环;处理所有计数需要二次时间

改用a生成该字典(a
计数器是dict
的子类):


一个
计数器()
只对所有元素进行一次计数;它只是在迭代过程中为它看到的每个值保留计数器。

您不断地重新计算之前已经计算过的切片的总和(每个
sum()
调用一个冗余循环),此外,您还通过一直切片来创建新列表对象的负载

您可以使用动态规划来计算总和,从输入列表的末尾开始;您只需将“当前”值添加到列表中下一个值的总和中:

# v1 is the input list, l is len(v1)
sums = [0] * (l * (l + 1) // 2)
for x in range(l - 1, -1, -1):
    for y in range(x, l):
         i = (y * (y + 1) // 2) + x
         sums[i] += v1[x] + (sums[i + 1] if x < y else 0)
(空格是反向切片,重复,不用于输出)。对于每一行和每一列,可以通过计算该列前面三角形中适合的值的数量加上行号来计算输出索引,或者
((col*col+1)//2)+行)

动态规划方法将后续值(索引下方的行)的已生成切片的总和添加到当前行的
v1
值:

v1     0  1  2  3  4

1 > 0  1  3  6 10 15
          ^  ^  ^  ^  
2 > 1     2  5  9 14
             ^  ^  ^
3 > 2        3  7 12
                ^  ^
4 > 3           4  9
                   ^ 
5 > 4              5
因此,第2行第4列的值是
v1[2]
加上第3行第4列已经计算的总和,或者在本例中是
2+12
。对于行索引小于列索引的元素,只有一个已经计算的和;在位置(3,3)处,在(4,3)处没有计算出的总和

下一个问题是,
list.count()
遍历整个总和列表,以计算一个数字出现的频率。因此,表达式中x的
{x:a.count(x)创建了一个O(N^2)双循环;处理所有计数需要二次时间

改用a生成该字典(a
计数器是dict
的子类):

一个
计数器()
只对所有元素进行一次计数;它只是为迭代时看到的每个值保留计数器。

您也可以尝试以下方法:

In [75]: v = [1,2,3,4,5,6,7,8,9,10, 9, 2, 1]

In [76]: r = {}

In [77]: for x in v: r[x] = r.setdefault(x,0) + 1

In [78]: r
Out[78]: {1: 2, 2: 2, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 2, 10: 1}

In [79]: def func():
   ....:     r = {}
   ....:     for x in v: r[x] = r.setdefault(x,0) + 1
   ....:     return r
   ....: 
In [80]: def func1():
   ....:     return Counter(v)
   ....: 
In [81]: %timeit -q -o func()
Out [81]: <TimeitResult : 100000 loops, best of 3: 2.25 µs per loop>

In [82]: %timeit -q -o func1()
Out [82]: <TimeitResult : 100000 loops, best of 3: 4.17 µs per loop>
结果:

{1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 9: 2, 10: 1, 12: 1, 14: 1, 15: 1}
您也可以尝试以下方法:

In [75]: v = [1,2,3,4,5,6,7,8,9,10, 9, 2, 1]

In [76]: r = {}

In [77]: for x in v: r[x] = r.setdefault(x,0) + 1

In [78]: r
Out[78]: {1: 2, 2: 2, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 2, 10: 1}

In [79]: def func():
   ....:     r = {}
   ....:     for x in v: r[x] = r.setdefault(x,0) + 1
   ....:     return r
   ....: 
In [80]: def func1():
   ....:     return Counter(v)
   ....: 
In [81]: %timeit -q -o func()
Out [81]: <TimeitResult : 100000 loops, best of 3: 2.25 µs per loop>

In [82]: %timeit -q -o func1()
Out [82]: <TimeitResult : 100000 loops, best of 3: 4.17 µs per loop>
结果:

{1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 9: 2, 10: 1, 12: 1, 14: 1, 15: 1}

您可以使用以下类似的方法降低求和的成本:

cache = defaultdict(defaultdict(dict))

def my_sum(v, x, y):
    if y - x == 1:
        return v[x]
    if not y in cache[v][x]:
        mid = (x + y) / 2
        cache[v][x][y] = my_sum(v, x, mid) + my_sum(v, mid, y)
    return cache[v][x][y]
然后:


可能更快…

您可以使用类似以下方法降低求和的成本:

cache = defaultdict(defaultdict(dict))

def my_sum(v, x, y):
    if y - x == 1:
        return v[x]
    if not y in cache[v][x]:
        mid = (x + y) / 2
        cache[v][x][y] = my_sum(v, x, mid) + my_sum(v, mid, y)
    return cache[v][x][y]
然后:

可能更快…

类似

[sum(v1[x:y]) for y in range(l+1) for x in range(y)]
对我来说似乎有问题,原因如下:

  • 它考虑
    v1
    的每个子串,其中长度
    n
    列表中有
    1+2+3+…+n
    ,即子串的数量与长度的平方成比例增长
  • 它为每个子字符串创建一个切片,每个切片都是一个副本
  • 它对每个数据片求和,即使数据片之间显然有很多重叠(例如,如果你知道前四个数字的和,你应该只需要一个额外的加法就可以得到前五个数字的和,而不是从头开始对前五个数字求和)
  • 我想关于1你能做的不多,但是你可以更聪明地处理另外两项:利用切片的总和
    [I:j]
    正好是切片的总和
    [I:j-1]
    (即从
    sums = {(i,i+1): x for i, x in enumerate(v1)}
    for intervalLen in range(2, l + 1):
        for i in range(l - intervalLen + 1):
            j = i + intervalLen
            sums[(i,j)] = sums[(i,j-1)] + sums[(j-1,j)]
    a = sums.values()