Python最有效的方式来保留排序的数据

时间:2020-09-01 00:58:04

标签: python algorithm performance optimization

最有效的方式来跟踪数据的升序/降序。假设我有一个数据流,假设这是非常大的。样本流:

key,mod,value
5,add,1
2,add,3
4,add,2
2,add,2
2,rem,5

在阅读流时,我将其放在字典中以跟踪内容。例如,在上述迷你流的结尾,我将有一个带有{5:1, 4:2}的字典。其中add表示该值增加了该值,而rem表示您从该键中删除了很多。如果该值变为0,则从字典中删除该键。但是我还希望能够按顺序打印数据(但不必一直打印)。我确实想跟踪最高/最低键,以便知道最高/最低值何时更改。更改密钥或更改其值。

我现在的操作方式是相应地从字典中填充/删除键。这应该是常数O(1)。跟踪sorted_keys列表,其中每个流都会检查新数字是否在词典中,如果不在列表中,则将执行bisect.insort_right(sorted_keys, key)。因此,sorted_keys始终都在排序。假设在排序列表中添加1个值很快,尽管它确实需要扩展大小,因此这可能会使O(n)保持不变。并且我跟踪prev_highestprev_lowest,并分别针对sorted_keys [0]或sorted_keys [-1]进行检查。

我尝试将双端队列与bisect.insort_right,sortedcontainers中的SortedDict,链接列表,OrderedDict一起使用,但似乎上述方法似乎效果最好。还有另一种可能的实现方式可以进一步优化吗?还是我应该按顺序跟踪某个级别,例如说10个项目。并相应地进行更新。但是,这样做的问题是,如果有新密钥,我如何知道它是否是新密钥之一?似乎拥有一个heapq会有所帮助,但是直到弹出它们,我才能获得排序后的值。而且,如果我需要按顺序打印整个内容,则只需对整个字典的键进行排序。

编辑: 使用下面的bisect和SortedDict添加我的测试:

import timeit
import bisect
import random
from sortedcontainers import SortedDict

NUM_ITERATION_TEST = 10
TOTAL_NUM_DATA = 1000000
MODS = ['add', 'rem']
QUANTITY = [1, 5, 10, 20, 100, 200, 300, 500, 1000]

DATA = [{'mod': random.choice(MODS),
         'key': random.randint(0, 1000),
         'val': random.choice(QUANTITY)} for x in range(TOTAL_NUM_DATA)]


def method1(DATA):
    d = {}
    sorted_keys = []

    for data in DATA:
        if data['mod'] == 'add':
            key = data['key']
            if key in d.keys():
                d[key] += data['val']
            else:
                d[key] = data['val']
                bisect.insort_right(sorted_keys, key)
        elif data['mod'] == 'rem':
            key = data['key']
            if key in d.keys():
                if d[key] <= data['val']:
                    del d[key]
                    sorted_keys.remove(key)           
                else:
                    d[key] -= data['val']
            else:
                pass # Deleting something not there yet

def method2(DATA):
    d = SortedDict()

    for data in DATA:
        if data['mod'] == 'add':
            key = data['key']
            if key in d.keys():
                d[key] += data['val']
            else:
                d[key] = data['val']
        elif data['mod'] == 'rem':
            key = data['key']
            if key in d.keys():
                if d[key] <= data['val']:
                    del d[key]
                else:
                    d[key] -= data['val']
            else:
                pass  # Deleting something not there yet


if __name__ == "__main__":
    # METHOD 1
    print("Method 1 Execution Time:")
    print(timeit.timeit("test_timeit.method1(test_timeit.DATA)",
                        number=NUM_ITERATION_TEST,
                        setup="import test_timeit"))

    # METHOD 2
    print("Method 2 Execution Time:")
    print(timeit.timeit("test_timeit.method2(test_timeit.DATA)",
                        number=NUM_ITERATION_TEST,
                        setup="import test_timeit"))

以上结果为:

Method 1 Execution Time:
4.427699800000001
Method 2 Execution Time:
12.7445671

2 个答案:

答案 0 :(得分:3)

对于适合内存的数据,“ SortedDict from sortedcontainers”(您已经尝试过)通常和将此类dict保持排序顺序一样好。但是查找时间大约是O(log N)(请参阅最后的编辑-看来是错误的!)。

假定在排序列表中添加1个值很快速,尽管它确实需要扩展大小,所以这可能需要O(n)。

在Python列表L中,在索引i处插入元素必须(至少)物理移动len(L) - i指针,这意味着64位指针的字节数是其的8倍。位盒。这就是当数据“大”时sortedcontainer获得巨大成功的地方:它需要物理移动的最坏情况的指针数量受一个独立于len(L)的常数的限制。在len(L)进入成千上万之前,很难注意到其中的区别。但是,当len(L)达到数百万美元时,两者之间的差异就很大。

我会尝试一个折衷方案:使用sortedcontainers SortedList跟踪当前键,并使用Python普通字典作为实际字典。然后:

对于“键添加值”:查看键是否在字典中。非常快。如果是,则无需触摸SortedList。只是改变字典。如果键不在字典中,则需要将其添加到SortedList和字典中。

对于“密钥rem值”:查看密钥是否在字典中。如果不是,我不知道您想做什么,但是您会弄清楚;-)但是,如果它在字典中,请减去该值。如果结果为非零,则操作完成。否则(结果为0),从dict和SortedList中删除键。

注意:不是出于语义原因,我建议使用SortedList而不是SortedSet,而是因为SortedSet需要更多的内存,因此要与有序列表并行维护集合。您没有必要使用它。

除了字典外,您可能真的还需要double-ended ("min max") heap。从您所说的内容中无法猜测-这取决于,例如,您仅想知道“最小和/或最大”的频率,而不是经常要实现整个排序顺序的频率。但是我不知道为速度而构建的Python的最小-最大堆实现-它们是凌乱的代码野兽,很少使用。

编辑

再三考虑,似乎sortedcontainer的SortedDict已经结合了SortedList和普通Python dict(的子类)。例如,在SortedDict中设置值的实现方式如下:

def __setitem__(self, key, value):
    if key not in self:
        self._list_add(key)
    dict.__setitem__(self, key, value)

因此,只有在字典中没有键时,它才会触摸SortedList。如果您维护自己的对,则没有太多改善机会。

自己动手

这是另一种尝试:

def method3(DATA):
    sorted_keys = SortedList()
    d = {}

    for data in DATA:
        if data['mod'] == 'add':
            key = data['key']
            if key in d:
                d[key] += data['val']
            else:
                d[key] = data['val']
                sorted_keys.add(key)
        elif data['mod'] == 'rem':
            key = data['key']
            if key in d:
                if d[key] <= data['val']:
                    del d[key]
                    sorted_keys.remove(key)
                else:
                    d[key] -= data['val']
            else:
                pass  # Deleting something not there yet

这实现了我最初的建议:使用纯Python字典维护您自己的SortedList对。它具有与使用SortedDict相同的O()行为,但以恒定因子显示则明显更快。看起来部分原因是因为dict操作现在全部用C编码(SortedDict用Python编码),其余的原因是我们仅对每个data项目的dict成员资格进行一次测试。例如,在

if key in d:
    d[key] += data['val']

d是SortedDict时,key in d会对其进行一次显式测试,但是d.__setitem__()的实现必须再次对其进行测试,以便可以向其添加key如果键未知,则为隐藏的SortedList。从更高级别的角度来看,我们已经知道密钥位于if主体中的dict中,因此可以完全忽略此处的显式SortedList。

答案 1 :(得分:2)

您在Arc<Mutex<Params>>中犯了两个错误:

  • 您选择method1而不是if key in d.keys():。没有必要在此处创建按键视图。

  • 您可以使用if key in d:从列表中删除,而不是使用sorted_keys.remove(key)来查找索引,然后删除该索引。

修复这些问题,将一些方法存储在局部变量中以进行更短/更快的访问,并使用bisect可能找到一个键并获取其值(而不是d.get检查然后查找值) ),我得到这些时间(方法1/2是您的,方法3是Tim的,方法4是我的):

in

Tim要求我将Round 1 method1 7.590627200000004 method2 19.851634099999984 method3 6.093115100000006 method4 5.069753999999989 Round 2 method1 7.857367500000009 method2 19.59779759999998 method3 6.057990299999972 method4 5.0046839999999975 Round 3 method1 7.843560700000012 method2 19.8673627 method3 6.079332300000033 method4 5.073929300000032 更改为randint(0, 1000)

randint(0, 40_000)

method1 607.2835661000001 method2 26.667593300000135 method3 12.84969140000021 method4 16.68231250000008 (仅较快速的解决方案):

randint(0, 400_000)

我的版本:

method3 20.179627500000002
method4 115.39424580000002

完整的基准代码,包括正确性检查:

def method4(DATA):
    d = {}
    sorted_keys = []
    insort = bisect.insort_right
    index = bisect.bisect_left
    get = d.get
    
    for data in DATA:
        if data['mod'] == 'add':
            key = data['key']
            val = get(key)
            if val:
                d[key] = val + data['val']
            else:
                d[key] = data['val']
                insort(sorted_keys, key)
        elif data['mod'] == 'rem':
            key = data['key']
            val = get(key)
            if val:
                if val <= data['val']:
                    del d[key]
                    del sorted_keys[index(sorted_keys, key)]
                else:
                    d[key] = val - data['val']
            else:
                pass # Deleting something not there yet
相关问题