为什么通过键访问字典的元素是O(1),即使哈希函数可能不是O(1)?
为什么通过键访问字典元素的时间复杂度为O(1),尽管哈希函数的时间复杂度可能不为O(1)?
这是因为字典的访问时间复杂度与集合的大小无关,无论集合有多大,访问成员所需的时间几乎相同。例如,一个有5个成员的字典访问一个成员可能需要约0.002毫秒,而一个有25个成员的字典访问一个成员也需要类似的时间。O(1)表示算法复杂度与集合大小有关,而不是与实际执行的语句或函数有关。
但是,如果哈希函数很差,可能会导致桶中有很多值,这样O(1)的时间复杂度就不再成立。这并不一定意味着哈希函数不好,可能是输入数据精心设计的结果。这就是为什么这里的O(1)是摊销复杂度,而不是“真正”的复杂度。
这并不意味着每个成员都需要相同的时间,它只是(大致)意味着访问时间的上限不随集合大小增长而增加。考虑哈希表如何处理冲突。类似地,对于二叉搜索树查找项目的时间复杂度为O(log2 n),因为最坏情况下的时间复杂度是与N的大小的对数相关,但是根节点附近的项目所需的时间会比叶子节点附近的项目少。
这并不是“摊销”概念在O(1)中的真正含义。"摊销"的概念是为了解释这样一个事实:如果你向集合中添加元素,大约1/N的添加操作将需要重新分配新的支持数组,这是一个O(N)的操作,因此你可以在O(N)的时间内执行N次添加操作,从而实现摊销的O(1)添加操作,而单次添加操作的时间复杂度实际上也是O(N)(非摊销情况)。这是对渐近复杂度的另一个解释,它假设哈希值足够好分布。
字典是一种常用的数据结构,它允许通过键来访问元素。一个常见的问题是,为什么通过键访问字典的元素的时间复杂度是O(1),即使哈希函数的时间复杂度可能不是O(1)呢?
问题的出现是因为哈希函数本身可能会有很多操作。然而,这些操作的数量取决于键的大小,而不是哈希表的大小。计算哈希函数的操作数对于插入到有十个或者一万个条目的表中的键来说是相同的。这就是为什么调用哈希函数通常被认为是O(1)的原因。对于固定大小的键(整数值和固定长度的字符串),这种方法效果很好。对于变长键,这种方法提供了一个实际上的上限的良好近似。
然而,对于哈希表的访问时间来说,一般来说是O(k),其中k是哈希键的大小的上限。此外,除非至少有一个项目由至少log(n)位表示,否则不可能拥有n个不同项的哈希表。如果不限制输入的位数,那么所有操作都是指数级的。但这并不是一个非常有趣或有用的结果,对吧?
事实上,哈希表中的项数不能超过可以唯一分配键并适应指针大小变量的项数。
总结起来,问题的出现是因为哈希函数的操作数取决于键的大小以及被哈希的数据的大小。键的大小是哈希表查找时间的线性因素,因此时间复杂度是O(k),其中k是键的大小。如果将k理解为上限,则实际上是O(1)。
因此,通过哈希表的键来访问元素的时间复杂度是O(1),即使哈希函数的时间复杂度可能不是O(1)。这是因为哈希函数的操作数与键的大小相关,而与哈希表的大小无关。通过限制键的大小,可以保证哈希表的访问效率。这就是为什么访问字典的元素的时间复杂度是O(1)的原因。
字典是一种常见的数据结构,它可以通过键值对的形式存储和访问数据。在字典中,根据键访问元素的时间复杂度被认为是O(1),即无论数据的大小如何,访问元素的时间都是恒定的。但是,哈希函数的时间复杂度可能不是O(1),那为什么访问字典的元素时间复杂度仍然是O(1)呢?
首先,O(1)并不意味着立即完成。O(1)表示时间复杂度是常数级的,与数据的大小无关。哈希函数确实需要一定的时间来计算,但这个时间不会随着集合的大小而增加。
然而,有可能编写一个与集合大小相关的哈希函数。尽管这样做是愚蠢和牵强的,但确实是可能的。搜索哈希集合的操作实际上是建立在计算哈希的时间复杂度是O(1)的假设上的,这几乎总是成立的,但并非一定如此。
甚至并不是那么愚蠢和牵强。一个自定义的列表实现如果希望允许包含相同项的两个列表被视为相等,可以重写GetHashCode()
方法以某种方式组合项的哈希码。如果我要实现这样一个类,我会一开始就实现GetHashCode()
方法。当然,我之后会对其进行修改。
这将是一个O(m)的哈希,其中m是内部集合的大小。但它仍然与外部集合(实际的基于哈希的结构)的大小无关。如果要使集合中的项查看它们当前所在的同一哈希集合中的所有项,那么这些项的哈希码就会变为O(n)(或n的任何函数)。那将是相当愚蠢和牵强的。
哦,你是指这个。是的,那将是愚蠢的。:)我无法想出任何可能的情况,你可能会希望这样做。
哈希的一般目的是避免O(n)的搜索时间,因此创建一个O(n)的哈希函数将完全违背这个目的。你可以这样做,但这就像使用Peano数递归实现加法一样:可能,但实际上并不实用。
当然,你是正确的,这是一个坏主意,但它是可能的,所以当你无法控制哈希时,你确实需要考虑这种可能性。
就我而言,这就像考虑数组索引是O(n)的可能性一样。如果数组是作为链表实现的,这种情况可能发生,但我不会担心这个。
虽然数组访问的时间复杂度不可能是O(N),但GetHashCode()
的时间复杂度可能具有任意的复杂度,只是最好是O(1)。
GetHashCode()
方法不接收哈希表作为参数,那么它怎么可能依赖于表的大小?
这虽然不常见,但并非没有先例,一个对象可能有一个对其容器的引用。如果有需要,你可以显式地传递它,但它也可能通过多层间接传递获得。
请记住,对于哈希来说,作为字典的键,它需要是稳定的,即在对象添加到字典后不会改变。否则,你将违反大多数字典实现的约定。此外,请记住,至少对于Java而言,约定是if a.equals(b) then hash(a)==hash(b)
。如果将集合作为哈希函数的一部分,那么如果集合发生更改导致哈希值改变,整个字典都将无法正常工作。