数组和哈希映射如何在访问中保持恒定时间?

时间:2011-01-13 23:25:45

标签: arrays algorithm data-structures hash computer-science

具体来说:给定哈希(或数组索引),机器如何在恒定时间内获取数据?

在我看来,即使通过所有其他内存位置(或其他任何内容),也需要花费相当于传递位置数量的时间(因此线性时间)。一位同事曾勇敢地向我解释这一点,但在我们开始接触电路时不得不放弃。

示例:

my_array = new array(:size => 20)
my_array[20] = "foo"
my_array[20] # "foo"

在位置20中访问“foo”是不变的,因为我们知道哪个桶“foo”在。我们是如何神奇地到达那个桶而不通过所有其他人的途中?要到达一个街区#20的房子,你仍然需要通过其他19 ...

6 个答案:

答案 0 :(得分:18)

  

我们是如何神奇地做到的   没有通过所有其他人的桶   在途中?

“我们”根本不“去”桶。 RAM物理工作的方式更像是在所有存储桶监听的通道上广播存储桶的号码,而被调用的号码将向您发送其内容。

计算发生在CPU中。从理论上讲,CPU与所有内存位置的距离相同(实际上并非如此,因为缓存可能会对性能产生巨大影响)。

如果您想了解详细信息,请阅读"What every programmer should know about memory"

答案 1 :(得分:10)

然后要了解你必须看看如何组织和访问内存。您可能需要查看address decoder的工作方式。问题是,你不必通过所有其他地址来获取你想要的内存。你实际上可以跳到你想要的那个。否则我们的电脑会非常慢。

答案 2 :(得分:6)

与必须按顺序访问内存的图灵机不同,计算机使用随机存取存储器或RAM,这意味着如果他们知道数组的起始位置,并且他们知道他们想要访问数组的第20个元素,他们知​​道要看什么部分的记忆。

这不像是在街上开车,而更像是在共享邮箱中为公寓选择正确的邮箱。

答案 3 :(得分:1)

有两件事很重要:

  1. my_array有关于内存中计算机必须跳转到何处以获取此数组的信息。
  2. index * sizeof类型从数组的开头偏移。
  3. 1 + 2 = O(1)其中可以找到数据

答案 4 :(得分:-1)

Big O不能那样工作。它应该是衡量特定算法和函数使用多少计算资源的指标。它并不意味着衡量使用的内存量,如果你正在谈论遍历那个内存,它仍然是一个恒定的时间。如果我需要找到数组的第二个槽,那就是向指针添加一个偏移量。现在,如果我有一个树结构,并且我想找到一个特定的节点,你现在正在讨论O(log n),因为它在第一次传递时没有找到它。 平均需要O(log n)才能找到该节点。

答案 5 :(得分:-1)

让我们用C / C ++术语讨论这个问题;还有一些关于C#数组的其他知识,但它与这一点并不相关。

给定一个16位整数值数组:

short[5] myArray = {1,2,3,4,5};

真正发生的是计算机在内存中分配了一块空间。这个内存块是为该数组保留的,正好是保存完整数组所需的大小(在我们的例子中是16 * 5 == 80位== 10个字节),并且是连续的。这些事实是给予的;如果在任何给定时间内您的环境中任何一个或任何一个都不是真的,那么由于访问权限,您通常会面临程序崩溃的风险。

因此,给定这种结构,变量myArray实际上是在幕后,是内存块开始的内存地址。这也是第一个元素的开始。每个附加元素按顺序排列在第一个元素之后。为myArray分配的内存块可能如下所示:

00000000000000010000000000000010000000000000001100000000000001000000000000000101
^               ^               ^               ^               ^
myArray([0])    myArray[1]      myArray[2]      myArray[3]      myArray[4]

它被认为是访问存储器地址并读取恒定字节数的恒定时间操作。如上图所示,如果你知道三件事,你可以得到每个人的记忆地址;内存块的开始,每个元素的内存大小以及所需元素的索引。因此,当您在代码中请求myArray[3]时,该请求将通过以下等式转换为内存地址:

myArray[3] == &myArray+sizeof(short)*3;

因此,通过恒定时间计算,您已经找到了第四个元素(索引3)的内存地址,以及另一个常量时间操作(或者至少被认为是这样;实际访问复杂性是硬件细节和快速足够你不应该在乎)你可以阅读那段记忆。如果你想知道的话,为什么大多数C风格语言的集合索引从零开始;数组的第一个元素从数组本身的位置开始,没有偏移量(sizeof(anything)* 0 == 0)

在C#中,有两个显着的差异。 C#数组有一些用于CLR的头信息。标头首先出现在内存块中,并且此标头的大小是常量且已知,因此寻址方程只有一个关键区别:

myArray[3] == &myArray+headerSize+sizeof(short)*3;

C#不允许您直接引用其托管环境中的内存,但运行时本身将使用类似这样的内容来执行堆内存访问。

第二件事,也是大多数C / C ++的共同点,是某些类型总是“通过引用”处理。您必须使用new关键字创建的任何内容都是引用类型(并且有一些对象,如字符串,它们也是引用类型,尽管它们看起来像代码中的值类型)。实例化时,引用类型将放在内存中,不会移动,通常不会被复制。因此,在幕后,表示该对象的任何变量只是内存中对象的内存地址。数组是引用类型(记住myArray只是一个内存地址)。引用类型的数组是这些内存地址的数组,因此访问作为数组元素的对象是一个两步过程;首先计算数组中元素的内存地址,然后得到它。这是另一个内存地址,它是实际对象的位置(或者至少是它的可变数据;复制类型在内存中的结构如何是另一个可能的蠕虫)。这仍然是一个固定时间的操作;只需两步而不是一步。