Python中对列表中的坐标对(2元组)的高效重新排序

时间:2010-01-28 11:05:01

标签: python sorting tuples

我想用新实体压缩实体列表以生成坐标列表(2元组),但我想确保(i,j)i< j总是如此。

但是,我对目前的解决方案并不十分满意:

from itertools import repeat

mems = range(1, 10, 2) 
mem = 8

def ij(i, j):
  if i < j:
    return (i, j)
  else:
    return (j, i)

def zipij(m=mem, ms=mems, f=ij):
  return map(lambda i: f(i, m), ms)

def zipij2(m=mem, ms=mems):
  return map(lambda i: tuple(sorted([i, m])), ms)

def zipij3(m=mem, ms=mems):
  return [tuple(sorted([i, m])) for i in ms]

def zipij4(m=mem, ms=mems):
  mems = zip(ms, repeat(m))
  half1 = [(i, j) for i, j in mems if i < j]
  half2 = [(j, i) for i, j in mems[len(half1):]]

  return half1 + half2

def zipij5(m=mem, ms=mems):
  mems = zip(ms, repeat(m))
  return [(i, j) for i, j in mems if i < j] + [(j, i) for i, j in mems if i > j]

以上输出:

>>> print zipij() # or zipij{2-5}  
[(1, 8), (3, 8), (5, 8), (7, 8), (8, 9)]

而不是正常:

>>> print zip(mems, repeat(mem))
[(1, 8), (3, 8), (5, 8), (7, 8), (9, 8)]

计时: 剪断(不再相关,请在下面的答案中看到更快的结果)

对于len(mems) == 5,任何解决方案都没有真正的问题,但是对于zipij5(),例如,当i > j已经评估为True时,第二个列表理解会不必要地返回前四个值。对于第一次理解的人来说,len(mems)

就我的目的而言,我很肯定(i, j)永远不会超过~10000,如果这有助于形成任何最佳解决方案的答案。为了解释我的用例(我发现它很有趣),我将存储一个稀疏的,上三角形的各种相似性矩阵,因此我需要在{{1}处不重复坐标(j, i) }。我说 of of sort 因为我将利用2.7中的新Counter()对象来执行准矩阵矩阵和矩阵向量加法。然后,我只需向counter_obj.update()提供一个2元组列表,然后将这些坐标增加多少次。对于我的用例,SciPy稀疏矩阵运行速度慢了大约50倍,让我感到沮丧...所以我很快放弃了它们。

所以无论如何,我对我的结果感到惊讶......我提出的第一个方法是zipij4zipij5,但它们仍然是最快的,尽管构建正常{{1然后在更改值后生成一个新zip。相对而言,我对Python仍然很陌生(Alex Martelli,你能听见我吗?),所以这是我天真的结论:

  • zip()非常昂贵(为什么?)
  • tuple(sorted([i, j]))似乎总是比列表组件更差(我想我已经读过这个并且它有意义了)
  • 不管怎样map(lambda ...)并没有太慢,尽管两次检查列表以检查i-j不等式。 (这是为什么?)

最后,我想知道哪个被认为是最有效的......或者是否还有其他快速且内存便宜的方式,我还没有想到。谢谢。


当前最佳解决方案

zipij5()

计时

## Most BRIEF, Quickest with UNSORTED input list:
## truppo's
def zipij9(m=mem, ms=mems):
  return [(i, m) if i < m else (m, i) for i in ms]

## Quickest with pre-SORTED input list:
## Michal's
def zipij10(m=mem, ms=mems):
  i = binsearch(m, ms)  ## See Michal's answer for binsearch()
  return zip(ms[:i], repeat(m)) + zip(repeat(m), ms[i:])

计时正在使用# Michal's Presorted - 410µs per loop Unsorted - 2.09ms per loop ## Due solely to the expensive sorted() # truppo's Presorted - 880µs per loop Unsorted - 896µs per loop ## No sorted() needed ,其长度仅为约5000。 mems = range(1, 10000, 2)可能会在较高的值时变得更糟,并且列表会更加混乱。 sorted()用于“未排序”时间。

3 个答案:

答案 0 :(得分:2)

当前版本:

(在我的机器上使用Python 2.6.4发布时最快。)

更新3:由于我们全力以赴,让我们进行二分搜索 - 不需要将m注入mems

def binsearch(x, lst):
    low, high = -1, len(lst)
    while low < high:                                                           
        i = (high - low) // 2
        if i > 0:
            i += low
            if lst[i] < x:
                low = i
            else:
                high = i
        else:
            i = high
            high = low
    return i

def zipij(m=mem, ms=mems):
    i = binsearch(m, ms)
    return zip(ms[:i], repeat(m)) + zip(repeat(m), ms[i:])

在我的机器上运行828μs = 0.828 ms与OP的当前解决方案的1.14 ms相比。假设输入列表已排序(当然,测试用例是常用的)。

此二进制搜索实现返回给定列表中第一个元素的索引,该索引不小于要搜索的对象。因此,不需要将m注入mems并对整个事物进行排序(例如在OP的当前解决方案中使用.index(m)),或者逐步遍历列表的开头(就像我一样)以前做过)找到应该划分的偏移量。


早期尝试:

这个怎么样? (下面In [25]旁边提出的解决方案,到zipij5的3.13毫秒2.42毫秒。)

In [24]: timeit zipij5(m = mem, ms = mems)
100 loops, best of 3: 3.13 ms per loop

In [25]: timeit [(i, j) if i < j else (j, i) for (i, j) in zip(mems, repeat(mem))]
100 loops, best of 3: 2.42 ms per loop

In [27]: [(i, j) if i < j else (j, i) for (i, j) in zip(mems, repeat(mem))] == zipij5(m=mem, ms=mems)
Out[27]: True

更新:这似乎与OP的自我答案完全一样快。但是,似乎更直接。

更新2:OP提议的简化解决方案的实施:

def zipij(m=mem, ms=mems):
    split_at = 0
    for item in ms:
        if item < m:
            split_at += 1
        else:
            break
    return [(item, m) for item in mems[:split_at]] + [(m, item) for item in mems[split_at:]]

In [54]: timeit zipij()
1000 loops, best of 3: 1.15 ms per loop

此外,truppo的解决方案在我的机器上运行1.36毫秒。我猜以上是目前为止最快的。注意在将mems传递给此函数之前需要对其进行排序!如果您使用range生成它,它当然已经排序了。

答案 1 :(得分:1)

为什么不直接内联你的ij() - 函数?

def zipij(m=mem, ms=mems):
  return [(i, m) if i < m else (m, i) for i in ms]

(这在我的计算机上以0.64毫秒而不是2.12毫秒运行)

一些基准:

zipit.py:

from itertools import repeat

mems = range(1, 50000, 2)
mem = 8

def zipij7(m=mem, ms=mems):
  cpy = sorted(ms + [m])
  loc = cpy.index(m)

  return zip(ms[:(loc)], repeat(m)) + zip(repeat(m), ms[(loc):])

def zipinline(m=mem, ms=mems):
  return [(i, m) if i < m else (m, i) for i in ms]

排序:

>python -m timeit -s "import zipit" "zipit.zipinline()"
100 loops, best of 3: 4.44 msec per loop

>python -m timeit -s "import zipit" "zipit.zipij7()"
100 loops, best of 3: 4.8 msec per loop

未排序:

>python -m timeit -s "import zipit, random; random.shuffle(zipit.mems)" "zipit.zipinline()"
100 loops, best of 3: 4.65 msec per loop

p>python -m timeit -s "import zipit, random; random.shuffle(zipit.mems)" "zipit.zipij7()"
100 loops, best of 3: 17.1 msec per loop

答案 2 :(得分:0)

最新版本:

def zipij7(m=mem, ms=mems):
  cpy = sorted(ms + [m])
  loc = cpy.index(m)

  return zip(ms[:(loc)], repeat(m)) + zip(repeat(m), ms[(loc):])

对我来说,长凳比truppo的速度快一点,比Michal的速度慢了30%。 (现在调查)


我可能已找到答案(暂时)。我似乎忘了为`zipij()``:

创建一个列表comp版本
def zipij1(m=mem, ms=mems, f=ij):
  return [f(i, m) for i in ms]

它仍然依赖于我愚蠢的ij()辅助功能,所以它当然不会因为简洁而获奖,但时间有所改善:

# 10000
1.27s
# 50000
6.74s

所以它现在是我当前的“赢家”,也不需要生成多个列表,或者使用很多函数调用,而不是ij()帮助器,所以我相信它也是效率最高。

但是,我认为这仍然可以改进......我认为不需要进行N ij()个函数调用(其中N是结果列表的长度):

  • 查找订购时mem适合mems的索引
  • 将该指数分为两部分
  • zip(part1, repeat(mem))
  • 向其添加zip(repeat(mem), part2)

它基本上是对zipij4()的改进,这避免了N个额外的函数调用,但我不确定速度/内存的好处而不是简洁成本。如果我弄明白的话,我可能会将这个版本添加到这个答案中。