使用范围作为字典中的键值,最有效的方法是?

时间:2018-11-04 06:47:07

标签: python dictionary data-structures range

我一直想知道是否存在某种数据结构或巧妙的方法来使用字典(O(1)查找)来返回值,如果给定范围的给定值不重叠。到目前为止,我一直认为如果范围具有一定的常数差(0-2、2-4、4-6等)可以执行此操作,或者可以在O(log(n ))时间。

例如,给定一本字典

d[0.05] 
>>> "a"
d[0.8]
>>> "e"
d[0.9]
>>> "e"
d[random.random()] # this should also work

它应该返回,

{{1}}

反正有实现这样的目标吗?感谢您对此的任何答复或回答。

2 个答案:

答案 0 :(得分:3)

如果您接受较低的范围边界分辨率并牺牲内存以提高查找速度,则可以使用O(1)查找时间。

字典可以在O(1)平均时间内进行查找,因为在固定大小的数据结构(对于平均情况为hash(key) % tablesize中,键和位置之间存在简单的算术关系。您的范围实际上是具有浮点边界的可变大小,因此没有固定的表大小可将搜索值映射到。

除非,就是限制范围的绝对上下边界,并让范围边界落在固定的步长上。您的示例使用从0.0到1.0的值,并且范围可以量化为0.05级。可以将其转换为固定表:

import math
from collections import MutableMapping

# empty slot marker
_EMPTY = object()

class RangeMap(MutableMapping):
    """Map points to values, and find values for points in O(1) constant time

    The map requires a fixed minimum lower and maximum upper bound for
    the ranges. Range boundaries are quantized to a fixed step size. Gaps
    are permitted, when setting overlapping ranges last range set wins.

    """
    def __init__(self, map=None, lower=0.0, upper=1.0, step=0.05):
        self._mag = 10 ** -round(math.log10(step) - 1)  # shift to integers
        self._lower, self._upper = round(lower * self._mag), round(upper * self._mag)
        self._step = round(step * self._mag)
        self._steps = (self._upper - self._lower) // self._step
        self._table = [_EMPTY] * self._steps
        self._len = 0
        if map is not None:
            self.update(map)

    def __len__(self):
        return self._len

    def _map_range(self, r):
        low, high = r
        start = round(low * self._mag) // self._step
        stop = round(high * self._mag) // self._step
        if not self._lower <= start < stop <= self._upper:
            raise IndexError('Range outside of map boundaries')
        return range(start - self._lower, stop - self._lower)

    def __setitem__(self, r, value):
        for i in self._map_range(r):
            self._len += int(self._table[i] is _EMPTY)
            self._table[i] = value

    def __delitem__(self, r):
        for i in self._map_range(r):
            self._len -= int(self._table[i] is not _EMPTY)
            self._table[i] = _EMPTY

    def _point_to_index(self, point):
        point = round(point * self._mag)
        if not self._lower <= point <= self._upper:
            raise IndexError('Point outside of map boundaries')
        return (point - self._lower) // self._step

    def __getitem__(self, point_or_range):
        if isinstance(point_or_range, tuple):
            low, high = point_or_range
            r = self._map_range(point_or_range)
            # all points in the range must point to the same value
            value = self._table[r[0]]
            if value is _EMPTY or any(self._table[i] != value for i in r):
                raise IndexError('Not a range for a single value')
        else:
            value = self._table[self._point_to_index(point_or_range)]
            if value is _EMPTY:
                raise IndexError('Point not in map')
        return value

    def __iter__(self):
        low = None
        value = _EMPTY
        for i, v in enumerate(self._table):
            pos = (self._lower + (i * self._step)) / self._mag
            if v is _EMPTY:
                if low is not None:
                    yield (low, pos)
                    low = None
            elif v != value:
                if low is not None:
                    yield (low, pos)
                low = pos
                value = v
        if low is not None:
            yield (low, self._upper / self._mag)

上面的代码实现了完整的映射接口,并且在建立索引或测试包含性时(接受范围使它更易于重用默认键,值和值),并且接受点和范围(作为[start, stop)间隔的元组建模)。 items字典视图实现,所有这些操作都可以从__iter__实现开始。

演示:

>>> d = RangeMap({
...     (0.0, 0.1): "a",
...     (0.1, 0.3): "b",
...     (0.3, 0.55): "c",
...     (0.55, 0.7): "d",
...     (0.7, 1.0): "e",
... })
>>> print(*d.items(), sep='\n')
((0.0, 0.1), 'a')
((0.1, 0.3), 'b')
((0.3, 0.55), 'c')
((0.55, 0.7), 'd')
((0.7, 1.0), 'e')
>>> d[0.05]
'a'
>>> d[0.8]
'e'
>>> d[0.9]
'e'
>>> import random
>>> d[random.random()]
'c'
>>> d[random.random()]
'a'

如果您不能如此轻易地限制步长和边界,那么您的下一个最佳选择是使用某种binary search algorithm;您将范围保持在已排序的顺序,并在数据结构的中间选择一个点;根据您的搜索关键字高于或低于中点,您将继续在数据结构的一半进行搜索,直到找到匹配项。

如果您的范围涵盖了从最低边界到最高边界的整个间隔,则可以使用bisect module;只需将每个范围的上下边界存储在一个列表中,将相应的值存储在另一个列表中,然后使用二分法将第一个列表中的位置映射到第二个列表中的结果。

如果您的范围有空白,则您需要保留带有其他边界的第三个列表,并首先验证该点是否在范围内,或使用interval tree。对于非重叠范围,可以使用简单的二叉树,但是也有一些更专业的实现也支持重叠范围。 PyPI上有一个intervaltree project,它支持全间隔树操作。

基于bisect的映射将行为与固定表实现进行匹配,如下所示:

from bisect import bisect_left
from collections.abc import MutableMapping


class RangeBisection(MutableMapping):
    """Map ranges to values

    Lookups are done in O(logN) time. There are no limits set on the upper or
    lower bounds of the ranges, but ranges must not overlap.

    """
    def __init__(self, map=None):
        self._upper = []
        self._lower = []
        self._values = []
        if map is not None:
            self.update(map)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, point_or_range):
        if isinstance(point_or_range, tuple):
            low, high = point_or_range
            i = bisect_left(self._upper, high)
            point = low
        else:
            point = point_or_range
            i = bisect_left(self._upper, point)
        if i >= len(self._values) or self._lower[i] > point:
            raise IndexError(point_or_range)
        return self._values[i]

    def __setitem__(self, r, value):
        lower, upper = r
        i = bisect_left(self._upper, upper)
        if i < len(self._values) and self._lower[i] < upper:
            raise IndexError('No overlaps permitted')
        self._upper.insert(i, upper)
        self._lower.insert(i, lower)
        self._values.insert(i, value)

    def __delitem__(self, r):
        lower, upper = r
        i = bisect_left(self._upper, upper)
        if self._upper[i] != upper or self._lower[i] != lower:
            raise IndexError('Range not in map')
        del self._upper[i]
        del self._lower[i]
        del self._values[i]

    def __iter__(self):
        yield from zip(self._lower, self._upper)

答案 1 :(得分:1)

首先,将数据分成两个数组:

limits = [0.1, 0.3, 0.55, 0.7, 1.0]
values = ["a", "b", "c", "d", "e"]

limits已排序,因此您可以在其中进行binary search

import bisect

def value_at(n):
    index = bisect.bisect_left(limits, n)
    return values[index]