【问题标题】:Is there a standard Python data structure that keeps things in sorted order?是否有标准的 Python 数据结构可以使事物保持有序?
【发布时间】:2011-07-28 12:15:21
【问题描述】:

我有一组可能看起来像这样的范围:

[(0, 100), (150, 220), (500, 1000)]

然后我会添加一个范围,比如(250, 400),列表将如下所示:

[(0, 100), (150, 220), (250, 400), (500, 1000)]

然后我会尝试添加范围 (399, 450),但由于与 (250, 400) 重叠,它会出错。

当我添加一个新范围时,我需要搜索以确保新范围不与现有范围重叠。并且列表中的任何范围都不会与列表中的另一个范围重叠。

为此,我想要一种数据结构,它可以廉价地按排序顺序维护其元素,并允许我快速找到给定元素之前或之后的元素。

有没有更好的方法来解决这个问题? Python中是否有类似的数据结构?我知道bisect 模块存在,这很可能是我将要使用的。但我希望有更好的东西。

编辑:我使用bisect 模块解决了这个问题。这是代码的链接。有点长,这里就不贴了:

Implementation of byte range list

【问题讨论】:

  • 考虑具有修改的节点下降条件的标准树(具有最小值/最大值与奇异值)。也许是一个简单的Red-Black、AVL 或类似的。 Python 示例/实现应该比比皆是。我认为这太专业了,不能成为“标准”结构。
  • 在您的示例中,范围 (250,400) 是否与 (150,300) 重叠?
  • @ire_and_curses: sheepish grin 是的,是的。 sigh 我现在正在修复它。
  • @pst:在 Python 中实现红黑树似乎很愚蠢。
  • 没有直接回答你的问题,但看起来你真正想要的是一个区间树。 pypi.python.org/pypi/intervaltree

标签: python data-structures


【解决方案1】:

使用SortedCollection 中的SortedDict

SortedDict 提供与 dict 相同的方法。此外,SortedDict 有效地按排序顺序维护其键。因此,keys 方法将按排序顺序返回键,popitem 方法将删除具有最高键的项目,等等。

我用过它——它有效。不幸的是,我现在没有时间进行适当的性能比较,但主观上它似乎比 bisect 模块更快。

【讨论】:

  • 知道为什么这不在标准 Python 中吗?有趣的是,Python 从一开始就有基于哈希的字典,而 C++ 首先得到了基于树的字典。
【解决方案2】:

您似乎想要bisect's insort_right/insort_left 之类的东西。 bisect 模块适用于列表和元组。

import bisect

l = [(0, 100), (150, 300), (500, 1000)]
bisect.insort_right(l, (250, 400))
print l # [(0, 100), (150, 300), (250, 400), (500, 1000)]
bisect.insort_right(l, (399, 450))
print l # [(0, 100), (150, 300), (250, 400), (399, 450), (500, 1000)]

您可以编写自己的overlaps 函数,在使用insort 之前可以使用该函数进行检查。

我认为您的数字有误,因为 (250, 400)(150, 300) 重叠。 overlaps() 可以这样写:

def overlaps(inlist, inrange):
    for min, max in inlist:
        if min < inrange[0] < max and max < inrange[1]:
            return True
    return False

【讨论】:

  • 是的,这行得通。不幸的是,在列表中间插入是 O(n)。但是,它会起作用的。是的,我确实弄错了我的数字。哎呀。
【解决方案3】:

廉价搜索和廉价插入往往是矛盾的。您可以使用linked list 作为数据结构。然后搜索找到一个新元素的插入点是O(n),后续在正确位置插入新元素是O(1)。

但您最好只使用简单的 Python 列表。随机访问(即找到你的位置)需要固定的时间。在正确的位置插入以维持排序理论上更昂贵,但这取决于dynamic array 的实现方式。在底层数组重新分配之前,您不会真正为插入付出高昂的代价。

关于检查日期范围重叠,我过去碰巧遇到过同样的问题。这是我使用的代码。我最初在一篇博客文章中找到它,链接自 SO 答案,但该网站似乎不再存在。我实际上在我的范围内使用日期时间,但它同样适用于您的数值。

def dt_windows_intersect(dt1start, dt1end, dt2start, dt2end):
    '''Returns true if two ranges intersect. Note that if two
    ranges are adjacent, they do not intersect.

    Code based on:
    http://beautifulisbetterthanugly.com/posts/2009/oct/7/datetime-intersection-python/
    http://stackoverflow.com/questions/143552/comparing-date-ranges  
    '''

    if dt2end <= dt1start or dt2start >= dt1end:
        return False

    return  dt1start <= dt2end and dt1end >= dt2start

以下是证明它有效的单元测试:

from nose.tools import eq_, assert_equal, raises

class test_dt_windows_intersect():
    """
    test_dt_windows_intersect
    Code based on: 
    http://beautifulisbetterthanugly.com/posts/2009/oct/7/datetime-intersection-python/
    http://stackoverflow.com/questions/143552/comparing-date-ranges  

               |-------------------|         compare to this one
    1               |---------|              contained within
    2          |----------|                  contained within, equal start
    3                  |-----------|         contained within, equal end
    4          |-------------------|         contained within, equal start+end
    5     |------------|                     overlaps start but not end
    6                      |-----------|     overlaps end but not start
    7     |------------------------|         overlaps start, but equal end
    8          |-----------------------|     overlaps end, but equal start
    9     |------------------------------|   overlaps entire range

    10 |---|                                 not overlap, less than
    11 |-------|                             not overlap, end equal
    12                              |---|    not overlap, bigger than
    13                             |---|     not overlap, start equal
    """


    def test_contained_within(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,30),   datetime(2009,10,1,6,40),
        )

    def test_contained_within_equal_start(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,0),    datetime(2009,10,1,6,30),
        )

    def test_contained_within_equal_end(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,30),   datetime(2009,10,1,7,0),
        )

    def test_contained_within_equal_start_and_end(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
        )

    def test_overlaps_start_but_not_end(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,5,30),   datetime(2009,10,1,6,30),
        )

    def test_overlaps_end_but_not_start(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,30),   datetime(2009,10,1,7,30),
        )

    def test_overlaps_start_equal_end(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,5,30),   datetime(2009,10,1,7,0),
        )

    def test_equal_start_overlaps_end(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,30),
        )

    def test_overlaps_entire_range(self):
        assert dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,5,0),    datetime(2009,10,1,8,0),
        )

    def test_not_overlap_less_than(self):
        assert not dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,5,0),    datetime(2009,10,1,5,30),
        )

    def test_not_overlap_end_equal(self):
        assert not dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,5,0),    datetime(2009,10,1,6,0),
        )

    def test_not_overlap_greater_than(self):
        assert not dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,7,30),    datetime(2009,10,1,8,0),
        )

    def test_not_overlap_start_equal(self):
        assert not dt_windows_intersect(
            datetime(2009,10,1,6,0),    datetime(2009,10,1,7,0),
            datetime(2009,10,1,7,0),    datetime(2009,10,1,8,0),
        )

【讨论】:

    【解决方案4】:

    也许模块 bisect 可能比简单的以下功能更好? :

    li = [(0, 100), (150, 220), (250, 400), (500, 1000)]
    
    
    def verified_insertion(x,L):
        u,v = x
        if v<L[0][0]:
            return [x] + L
        elif u>L[-1][0]:
            return L + [x]
        else:
            for i,(a,b) in enumerate(L[0:-1]):
                if a<u and v<L[i+1][0]:
                    return L[0:i+1] + [x] + L[i+1:]
        return L 
    
    
    lo = verified_insertion((-10,-2),li)
    
    lu = verified_insertion((102,140),li)
    
    le = verified_insertion((222,230),li)
    
    lee = verified_insertion((234,236),le) # <== le
    
    la = verified_insertion((408,450),li)
    
    ly = verified_insertion((2000,3000),li)
    
    for w in (lo,lu,le,lee,la,ly):
        print li,'\n',w,'\n'
    

    该函数返回一个列表而不修改作为参数传递的列表。

    结果

    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(-10, -2), (0, 100), (150, 220), (250, 400), (500, 1000)] 
    
    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(0, 100), (102, 140), (150, 220), (250, 400), (500, 1000)] 
    
    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(0, 100), (150, 220), (222, 230), (250, 400), (500, 1000)] 
    
    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(0, 100), (150, 220), (222, 230), (234, 236), (250, 400), (500, 1000)] 
    
    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(0, 100), (150, 220), (250, 400), (408, 450), (500, 1000)] 
    
    [(0, 100), (150, 220), (250, 400), (500, 1000)] 
    [(0, 100), (150, 220), (250, 400), (500, 1000), (2000, 3000)] 
    

    【讨论】:

      【解决方案5】:

      回答你的问题:

      Is there a data structure like that available in Python?
      

      不,没有。但是您可以使用 bisect 模块中的列表作为基本结构和代码轻松地自己构建一个,以保持列表有序并检查重叠。

      class RangeList(list):
      """Maintain ordered list of non-overlapping ranges"""
          def add(self, range):
          """Add a range if no overlap else reject it"""
              lo = 0; hi = len(self)
              while lo < hi:
                  mid = (lo + hi)//2
                  if range < self[mid]: hi = mid
                  else: lo = mid + 1
              if overlaps(range, self[lo]):
                  print("range overlap, not added")
              else:
                  self.insert(lo, range)
      

      我将overlaps 函数留作练习。 (此代码未经测试,可能需要一些调整)

      【讨论】:

      • 这可行,但您刚刚重新实现了 bisect 模块的一部分。 :-)
      • @Omnifarious:你是对的。我最初的意图是复制和扩展 bisect 函数,认为它需要一个不同的比较运算符来处理范围比较。事实证明它没有,所以我应该调用 bisect 函数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-23
      • 2018-08-11
      相关资源
      最近更新 更多