【问题标题】:What is the time complexity of checking membership in dict.items()?检查 dict.items() 中的成员资格的时间复杂度是多少?
【发布时间】:2020-10-16 02:18:37
【问题描述】:

在dict.items()中检查成员的时间复杂度是多少?

根据documentation

键视图是类似集合的,因为它们的条目是唯一且可散列的。 如果所有值都是可散列的,那么 (key, value) 对是唯一的并且 可散列,然后项目视图也是类似集合的。(值视图不是 被视为类似集合,因为条目通常不是唯一的。)对于 类似集合的视图,为抽象基础定义的所有操作 类 collections.abc.Set 可用(例如,==、

所以我用下面的代码做了一些测试:

from timeit import timeit

def membership(val, container):
    val in container

r = range(100000)
s = set(r)
d = dict.fromkeys(r, 1)
d2 = {k: [1] for k in r}
items_list = list(d2.items())

print('set'.ljust(12), end='')
print(timeit(lambda: membership(-1, s), number=1000))
print('dict'.ljust(12), end='')
print(timeit(lambda: membership(-1, d), number=1000))
print('d_keys'.ljust(12), end='')
print(timeit(lambda: membership(-1, d.keys()), number=1000))
print('d_values'.ljust(12), end='')
print(timeit(lambda: membership(-1, d.values()), number=1000))
print('\n*With hashable dict.values')
print('d_items'.ljust(12), end='')
print(timeit(lambda: membership((-1, 1), d.items()), number=1000))
print('*With unhashable dict.values')
print('d_items'.ljust(12), end='')
print(timeit(lambda: membership((-1, 1), d2.items()), number=1000))
print('d_items'.ljust(12), end='')
print(timeit(lambda: membership((-1, [1]), d2.items()), number=1000))
print('\nitems_list'.ljust(12), end='')
print(timeit(lambda: membership((-1, [1]), items_list), number=1000))

输出:

set         0.00034419999999998896
dict        0.0003307000000000171
d_keys      0.0004200000000000037
d_values    2.4773092

*With hashable dict.values
d_items     0.0004413000000003109
*With unhashable dict.values
d_items     0.00042879999999989593
d_items     0.0005549000000000248

items_list  3.5529328

如您所见,当dict.values 都是可散列的(int)时,
成员资格的执行时间类似于setd_keys
因为items 视图类似于集合
最后两个示例在 dict.values 上,带有不可散列的对象 (list)。
所以我假设执行时间与list 的执行时间相似。
但是,它们仍然类似于set

这是否意味着即使dict.values 是不可散列的对象,
items view的实现还是很高效的,
结果 O(1) 检查成员的时间复杂度?

我错过了什么吗?

已编辑 根据@chepner 的评论:dict.fromkeys(r, [1]) -> {k: [1] for k in r}
已编辑 根据@MarkRansom 的评论:另一个测试用例list(d2.items())

【问题讨论】:

  • 我认为in 不会检查成员资格的值,因此这些值是否可散列并不重要。
  • @MarkRansom 你是说python在内部只​​检查每个元组的第一项吗? items 返回一个包含 (key, value) 对的视图对象作为元组。
  • 请记住,dict.fromkeys(r, [1]) 创建一个只有一个 unique 值的 dict;对于任意两个键 xyd2[x] is d2[y] 将为真。这可能是相关的。
  • @chepner 也许,我会尝试使用不同的对象。
  • @chepner 这不是问题所在。查看修改后的代码。

标签: python python-3.x dictionary time-complexity


【解决方案1】:

dict_items 的实例中查找是一个 O(1) 操作(尽管它具有任意大的常数,与比较值的复杂性有关。)


dictitems_contains 不会简单地尝试对元组进行哈希处理并在类似集合的键/值对集合中查找它。

(注意:以下所有链接都指向dictitems_contain的不同行,如果您不想单独单击它们。)

评估

(-1, [1]) in d2.items()

首先extracts the key from the tuple,然后尝试find that key in the underlying dict。如果查找fails,它立即returns false。只有找到密钥时才会找到compare the value from the tuple to the value mapped to the key in the dict

dictitems_contains 在任何时候都不需要散列元组的第二个元素。

如文档中所述,当值不可散列时,dict_items 的实例在哪些方面类似于集合,目前尚不清楚。


dict_items.__contains__ 的简化的纯 Python 实现可能类似于

class DictItems:
    def __init__(self, d):
        self.d = d

    def __contains__(self, t):
        key = t[0]
        value = t[1]
        try:
            dict_value = self.d[key]  # O(1) lookup
        except KeyError:
            return False
    
        return value == dict_value  # Arbitrarily expensive comparison

    ...

d.items() 返回DictItems(d)

【讨论】:

  • 所以要回答标题中的问题,那就是 O(1) + 比较时间(即found.__eq__(value)),对吧?
  • 这是推测,还是你去源头了?
  • 关于非集合类dict_items{0} | {1: []}.items() -> TypeError: unhashable type: 'list'。虽然类似集合的工作:{0} | {1: 2}.items() -> {0, (1, 2)}.
  • @MarkRansom 我没有跟踪答案中的所有功能依赖关系,但简而言之,dk_lookupdict_subscript(这是实现dict.__getitem__)和PyDict_GetItemWithError 使用(这是dictitems_contains 使用的)。
  • 谢谢!这完美地解释了代码的执行时间。感谢源代码参考!
【解决方案2】:

简答

项目视图中成员资格测试的时间复杂度为O(1)

查找的伪代码

这是成员资格测试的工作方式:

def dictitems_contains(dictview, key_value_pair):
    d = dictview.mapping
    k, v = key_value_pair
    try:
        return d[k] == v
    except KeyError:
        return False

实际代码

这是C source code

static int
dictitems_contains(_PyDictViewObject *dv, PyObject *obj)
{
    int result;
    PyObject *key, *value, *found;
    if (dv->dv_dict == NULL)
        return 0;
    if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 2)
        return 0;
    key = PyTuple_GET_ITEM(obj, 0);
    value = PyTuple_GET_ITEM(obj, 1);
    found = PyDict_GetItemWithError((PyObject *)dv->dv_dict, key);
    if (found == NULL) {
        if (PyErr_Occurred())
            return -1;
        return 0;
    }
    Py_INCREF(found);
    result = PyObject_RichCompareBool(found, value, Py_EQ);
    Py_DECREF(found);
    return result;
}

O(1) 复杂度的时间证据

无论字典大小如何(在这些情况下:100、1,000 和 10,000),我们都会获得相同的恒定查找时间。

$ python3.8 -m timeit -s 'd = dict.fromkeys(range(100))'  '(99, None) in d.items()'
5000000 loops, best of 5: 92 nsec per loop

$ python3.8 -m timeit -s 'd = dict.fromkeys(range(1_000))'  '(99, None) in d.items()'
5000000 loops, best of 5: 92.2 nsec per loop

$ python3.8 -m timeit -s 'd = dict.fromkeys(range(10_000))'  '(99, None) in d.items()'
5000000 loops, best of 5: 92.1 nsec per loop

查找调用 hash() 的证据

我们可以通过修补_hash_()来监控hash调用:

class Int(int):
    def __hash__(self):
        print('Hash called')
        return hash(int(self))

应用监控工具显示,在创建字典时会发生散列,并在对项目视图进行成员资格测试时再次发生:

>>> d = {Int(1): 'one'}
Hash called
>>> (Int(1), 'one') in d.items()
Hash called
True

【讨论】:

  • 感谢您的回答!尤其是在hash() 调用时,在items view 中查找具有可散列值的值。
猜你喜欢
  • 1970-01-01
  • 2023-03-13
  • 2015-01-19
  • 1970-01-01
  • 2018-11-24
  • 2014-05-27
  • 1970-01-01
  • 1970-01-01
  • 2021-07-03
相关资源
最近更新 更多