【问题标题】:Use range as a key value in a dictionary, most efficient way?使用范围作为字典中的键值,最有效的方式?
【发布时间】:2018-11-04 06:47:07
【问题描述】:

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

所以,例如给定一个字典,

d = {[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"
d[random.random()] # this should also work

有没有办法实现这样的目标?感谢您对此的任何回复或回答。

【问题讨论】:

  • 是的,因为您的范围是任意的,您可以编写一个将范围映射到整数的函数。然后只需在列表(如果范围数是固定的)或字典中查找该整数。如果范围更系统,该函数甚至可以是一个简单的 lambda 函数。
  • 我假设这个解决方案需要某种搜索来检查值是否在范围内,那么在这种情况下至少是 O(log(n)) 吗?这很好,我只是想知道是否有一些传统的方法或数据结构可以在 O(1) 时间内完成
  • 顺便说一句,这实际上是一个非常有趣的问题。我从未读过将连续值范围映射到同一索引的哈希函数,这可能是一个非常好的研究问题。

标签: python dictionary data-structures range


【解决方案1】:

如果您接受较低的范围边界分辨率并牺牲内存以提高查找速度,则可以有 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)区间建模的元组)(支持范围使重用默认键、值和项字典变得更容易查看实现,它们都来自__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)

【讨论】:

    【解决方案2】:

    首先,将数据拆分为两个数组:

    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]
    

    【讨论】:

    • 虽然此应答器解决了 OP 所述的问题,但它不以任何方式使用 dict。因此,作为答案,它仅部分涵盖了所提出的问题。
    • @JohanL:如果您有更好的解决方案,请发布。问题标题要求“最有效的方法”。
    • 我没有。不过,我有兴趣看看是否有。
    • @JohanL 有时答案是:不要使用字典。我在 python 中不常说的话......
    • @juanpa.arrivillaga 好吧,是的,我认为这是一个有效的答案。像这样的问题每隔一段时间就会出现一次,在或多或少类似于此的情况下,然后很高兴看到它说:不要那样做 :-)
    猜你喜欢
    • 2011-01-09
    • 2017-03-23
    • 1970-01-01
    • 2014-03-02
    • 1970-01-01
    • 2017-05-20
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多