为啥我们使用数组而不是其他数据结构?
Posted
技术标签:
【中文标题】为啥我们使用数组而不是其他数据结构?【英文标题】:Why do we use arrays instead of other data structures?为什么我们使用数组而不是其他数据结构? 【发布时间】:2010-09-28 09:20:33 【问题描述】:在我进行编程时,我还没有看到数组比其他形式更适合存储信息的实例。我确实认为编程语言中添加的“功能”已经对此进行了改进,并因此取代了它们。我现在看到它们并没有被替换,而是被赋予了新的生命,可以这么说。
那么,基本上,使用数组有什么意义呢?
这并不是我们从计算机的角度使用数组的原因,而是从编程的角度来看我们为什么要使用数组(一个细微的区别)。计算机如何处理数组并不是问题的重点。
【问题讨论】:
为什么不考虑计算机对阵列的作用?我们有一个门牌号系统,因为我们有 STRAIGHT 条街道。数组也是如此。 “其他数据结构”或“另一种形式”是什么意思?出于什么目的? 【参考方案1】:是时候回去上课了。虽然我们今天在花哨的托管语言中没有过多考虑这些事情,但它们建立在相同的基础上,所以让我们看看 C 中的内存是如何管理的。
在我深入研究之前,快速解释一下术语“pointer”的含义。指针只是一个“指向”内存中某个位置的变量。它不包含该内存区域的实际值,它包含它的内存地址。将一块内存想象成一个邮箱。指针将是该邮箱的地址。
在 C 中,数组只是一个带有偏移量的指针,偏移量指定在内存中查找的距离。这提供了O(1) 访问时间。
MyArray [5]
^ ^
Pointer Offset
所有其他数据结构要么建立在此之上,要么不使用相邻内存进行存储,从而导致随机访问查找时间不佳(尽管不使用顺序内存还有其他好处)。
例如,假设我们有一个包含 6 个数字 (6,4,2,3,1,5) 的数组,在内存中它看起来像这样:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
在数组中,我们知道每个元素在内存中彼此相邻。 C 数组(此处称为 MyArray
)只是指向第一个元素的指针:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray
如果我们想查找MyArray[4]
,内部会这样访问它:
0 1 2 3 4
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray + 4 ---------------/
(Pointer + Offset)
因为我们可以通过向指针添加偏移量来直接访问数组中的任何元素,所以我们可以在相同的时间内查找任何元素,而不管数组的大小。这意味着获取MyArray[1000]
将花费与获取MyArray[5]
相同的时间。
另一种数据结构是链表。这是一个线性指针列表,每个指针指向下一个节点
======== ======== ======== ======== ========
| Data | | Data | | Data | | Data | | Data |
| | -> | | -> | | -> | | -> | |
| P1 | | P2 | | P3 | | P4 | | P5 |
======== ======== ======== ======== ========
P(X) stands for Pointer to next node.
请注意,我将每个“节点”都放入了自己的块中。这是因为不能保证它们在内存中(并且很可能不会)相邻。
如果我想访问P3,我不能直接访问它,因为我不知道它在内存中的什么位置。我只知道根(P1)在哪里,所以我必须从 P1 开始,然后按照每个指针指向所需的节点。
这是一个 O(N) 查找时间(查找成本随着每个元素的添加而增加)。与到达 P4 相比,到达 P1000 的成本要高得多。
更高级的数据结构,例如哈希表、堆栈和队列,都可能在内部使用一个数组(或多个数组),而链表和二叉树通常使用节点和指针。
您可能想知道为什么有人会使用需要线性遍历的数据结构来查找值而不是仅使用数组,但它们有其用途。
再次获取我们的数组。这一次,我想找到包含值 '5' 的数组元素。
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^ ^ ^ ^ ^ FOUND!
在这种情况下,我不知道要为指针添加什么偏移量才能找到它,所以我必须从 0 开始,然后一直向上直到找到它。这意味着我必须执行 6 次检查。
因此,在数组中搜索一个值被认为是 O(N)。搜索成本随着数组变大而增加。
还记得上面我说过有时使用非顺序数据结构可以有优势吗?搜索数据是这些优势之一,最好的例子之一就是二叉树。
二叉树是一种类似于链表的数据结构,但不是链接到单个节点,而是每个节点可以链接到两个子节点。
==========
| Root |
==========
/ \
========= =========
| Child | | Child |
========= =========
/ \
========= =========
| Child | | Child |
========= =========
Assume that each connector is really a Pointer
当数据被插入二叉树时,它使用几个规则来决定在哪里放置新节点。基本概念是,如果新值大于父母,则将其插入左侧,如果低于父母,则将其插入右侧。
这意味着二叉树中的值可能如下所示:
==========
| 100 |
==========
/ \
========= =========
| 200 | | 50 |
========= =========
/ \
========= =========
| 75 | | 25 |
========= =========
在二叉树中搜索 75 的值时,由于这种结构,我们只需要访问 3 个节点(O(log N)):
75 是否小于 100?看右节点 75 是否大于 50?查看左节点 还有 75 个!即使我们的树中有 5 个节点,我们也不需要查看剩下的两个,因为我们知道它们(和它们的子节点)不可能包含我们正在寻找的值。这给了我们一个搜索时间,在最坏的情况下意味着我们必须访问每个节点,但在最好的情况下我们只需要访问一小部分节点。
这就是数组的优势所在,尽管访问时间为 O(1),但它们提供线性 O(N) 搜索时间。
这是对内存中数据结构的令人难以置信的高级概述,跳过了很多细节,但希望它能说明数组与其他数据结构相比的优势和劣势。
【讨论】:
@Jonathan:您更新了图表以指向第 5 个元素,但您也将 MyArray[4] 更改为 MyArray[5] 所以它仍然不正确,将索引更改回 4 并保留图表照原样,你应该很好。 这就是我对“社区维基”的困扰,这篇文章值得“适当”的代表 不错的答案。但是您描述的树是二叉搜索树-二叉树只是每个节点最多有两个孩子的树。您可以拥有一个包含任意顺序元素的二叉树。二叉搜索树的组织方式如您所描述。 很好的解释,但我不禁挑剔...如果允许将项目重新排序为二叉搜索树,为什么不能重新排序数组中的元素所以二进制搜索也可以在其中工作吗?您可能会更详细地了解树的 O(n) 插入/删除,但数组的 O(n)。 二叉树表示不是 O(log n),因为访问时间相对于数据集的大小呈对数增长?【参考方案2】:查看数组优势的一种方法是查看数组的 O(1) 访问能力在哪里需要并因此大写:
在应用程序的查找表中(用于访问某些分类响应的静态数组)
Memoization(已经计算出复杂的函数结果,这样就不用再计算函数值了,比如log x)
需要图像处理的高速计算机视觉应用 (https://en.wikipedia.org/wiki/Lookup_table#Lookup_tables_in_image_processing)
【讨论】:
【参考方案3】:并非所有程序都做相同的事情或在相同的硬件上运行。
这通常是为什么存在各种语言功能的答案。数组是一个核心的计算机科学概念。用列表/矩阵/向量/任何高级数据结构替换数组会严重影响性能,并且在许多系统中是完全不切实际的。由于有问题的程序,在许多情况下应该使用这些“高级”数据收集对象之一。
在业务编程中(我们大多数人都会这样做),我们可以针对功能相对强大的硬件。在这些情况下,使用 C# 中的 List 或 Java 中的 Vector 是正确的选择,因为这些结构允许开发人员更快地完成目标,从而使此类软件具有更多功能。
在编写嵌入式软件或操作系统时,数组通常可能是更好的选择。虽然数组提供的功能较少,但它占用的 RAM 较少,编译器可以更有效地优化代码以查找数组。
我确信我忽略了这些案例的一些好处,但我希望你明白这一点。
【讨论】:
具有讽刺意味的是,在 Java 中您应该使用 ArrayList(或 LinkedList)而不是 Vector。这与正在同步的向量有关,这通常是不必要的开销。【参考方案4】:对于 O(1) 的随机访问,无法破解。
【讨论】:
在哪一点上?什么是 O(1)?什么是随机访问?为什么打不过?还有一点? O(1) 表示恒定时间,例如,如果您想获取数组的 n-esim 元素,您只需通过其索引器 (array[n-1]) 直接访问它,使用以链表为例,你必须找到头,然后依次n-1次到达下一个节点,即O(n),线性时间。 Big-O 表示法描述了算法的速度如何根据其输入的大小而变化。 O(n) 算法运行两倍的项目需要两倍的时间,运行 8 倍的项目需要 8 倍的时间。换句话说,O(n) 算法的速度随 [cont...] 其输入的大小。 O(1) 意味着输入 ('n') 的大小不会影响算法的速度,它是一个恒定的速度,与输入大小无关 我看到了你的 O(1),并提高了你 O(0)。以上是关于为啥我们使用数组而不是其他数据结构?的主要内容,如果未能解决你的问题,请参考以下文章
C:为啥你可以通过值传递(给函数)一个结构,而不是一个数组?
为啥即使我们尝试获取数据而不是更新/提交新数据,GraphQL 也会查询 POST 请求?