Arrays 数组和哈希映射如何在访问中保持恒定时间?
具体来说:给定一个散列(或数组索引),机器如何在固定时间内获取数据 在我看来,即使经过所有其他内存位置(或其他任何位置),也需要相当于所经过位置数量的时间(线性时间)。一位同事曾勇敢地向我解释这一点,但当我们开始讨论电路时,他不得不放弃 例如:Arrays 数组和哈希映射如何在访问中保持恒定时间?,arrays,algorithm,data-structures,hash,computer-science,Arrays,Algorithm,Data Structures,Hash,Computer Science,具体来说:给定一个散列(或数组索引),机器如何在固定时间内获取数据 在我看来,即使经过所有其他内存位置(或其他任何位置),也需要相当于所经过位置数量的时间(线性时间)。一位同事曾勇敢地向我解释这一点,但当我们开始讨论电路时,他不得不放弃 例如: my_array = new array(:size => 20) my_array[20] = "foo" my_array[20] # "foo" 位置20中“foo”的访问是恒定的,因为我们知道“foo”在哪个桶中。我们是如何神奇地到达那个
my_array = new array(:size => 20)
my_array[20] = "foo"
my_array[20] # "foo"
位置20中“foo”的访问是恒定的,因为我们知道“foo”在哪个桶中。我们是如何神奇地到达那个水桶而不经过其他所有人的?要想到达一个街区的20号住宅,你还必须经过另外19号…然后才能理解,你必须看看内存是如何组织和访问的。你可能得看看一个机器人的工作方式。问题是,您不必经过所有其他地址才能找到内存中所需的地址。你可以跳到你想要的那个。否则,我们的计算机会非常慢。与图灵机器不同,图灵机器必须按顺序访问内存,计算机使用随机访问内存或RAM,这意味着如果它们知道数组的起始位置,并且知道要访问数组的第20个元素,它们就知道要查看内存的哪一部分
与其说是在街上开车,不如说是在共享邮箱中为你的公寓选择正确的邮箱。2件事很重要:
1+2=O(1)可以找到数据的地方大O不是这样工作的。它应该是一个特定算法和函数使用多少计算资源的度量。它并不是用来测量所使用的内存量,如果你说的是遍历内存,它仍然是一个恒定的时间。如果我需要找到数组的第二个插槽,只需向指针添加偏移量即可。现在,如果我有一个树结构,我想找到一个特定的节点,你现在谈论的是O(logn),因为它在第一次遍历时没有找到它。平均来说,需要O(logn)才能找到该节点 我们是如何神奇地做到这一点的 不经过所有其他的桶 在路上 “我们”根本不“去”水桶。RAM的物理工作方式更像是在所有存储桶都在收听的频道上广播存储桶的号码,而被调用的存储桶将向您发送其内容 计算在CPU中进行。理论上,CPU与所有内存位置的“距离”相同(实际上并非如此,因为缓存会对性能产生巨大影响)
如果您想了解详细信息,请阅读。让我们用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只是一个内存地址)。引用类型的数组是这些内存地址的数组,因此访问作为数组中元素的对象是一个两步过程;首先计算t