为什么我们使用数组而不是其他数据结构?


195

在编写程序时,我还没有见过这样一个实例:数组比另一种形式更适合存储信息。我确实已经发现,编程语言中增加的“功能”对此有所改进,并以此代替了它们。我现在可以看到,它们并没有被取代,而是被赋予了新的生命。

那么,基本上,使用数组有什么意义?

这不是为什么我们从计算机的角度使用数组,而是为什么从编程的角度使用数组(微妙的区别)。计算机如何处理阵列并不是问题的重点。


2
为什么不考虑计算机如何处理阵列?我们拥有房屋编号系统,因为我们拥有直的街道。数组也是如此。
lcn

您是什么意思“ 其他数据结构 ”或“ 另一种形式 ”?出于什么目的?
tevemadar '19

Answers:


771

是时候回到过去上一堂课了。虽然我们今天在我们的高级托管语言中对这些事情的考虑不多,但它们是建立在相同的基础上的,因此让我们看一下如何在C中管理内存。

在我深入探究之前,先对“ 指针 ”一词的含义进行快速解释。指针仅仅是“指向”内存中某个位置的变量。它不包含该内存区域的实际值,而是包含该内存的地址。将一块内存视为一个邮箱。指针将是该邮箱的地址。

在C语言中,数组只是具有偏移量的指针,偏移量指定要在内存中查找的距离。这提供了O(1)访问时间。

  MyArray   [5]
     ^       ^
  Pointer  Offset

所有其他数据结构要么以此为基础,要么不使用相邻的存储器进行存储,从而导致较差的随机访问查找时间(尽管不使用顺序存储器还有其他好处)。

例如,假设我们有一个数组,其中包含6个数字(6,4,2,3,1,5),在内存中看起来像这样:

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================

在数组中,我们知道每个元素在内存中彼此相邻。AC数组(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)搜索时间。

这是关于内存中数据结构的令人难以置信的高级概述,跳过了许多细节,但希望它能说明数组与其他数据结构相比的优缺点。


1
@Jonathan:更新了图表以指向第5个元素,但还将MyArray [4]更改为MyArray [5],因此仍然不正确,将索引更改回4并保持图表不变,您应该会很好。
罗伯特·格兰伯

54
这就是使我对“社区Wiki”
感到困惑的地方,

8
好答案。但是您描述的树是二叉搜索树-二叉树只是其中每个节点最多具有两个子节点的树。您可以拥有带有任意顺序的元素的二叉树。二进制搜索树按照您的描述进行组织。
gnud

1
很好的解释,但我无能为力...如果允许您将项目重新排序为二进制搜索树,为什么不对数组中的元素重新排序也可以在其中进行二进制搜索呢?您可能会更详细地介绍有关树的O(n)插入/删除,但有关数组的O(n)。
市场

2
二进制树表示形式不是O(log n),因为访问时间相对于数据集的大小呈对数增加吗?
伊万·普赖斯

73

对于O(1)不可访问的随机访问。


6
在哪一点?什么是O(1)?什么是随机访问?为什么不能被打败?还有一点吗?
杰森

3
O(1)表示恒定时间,例如,如果要获取数组的n-esim元素,则可以直接通过其索引器(array [n-1])对其进行访问,例如,使用链表找到头,然后依次转到n-1次,即线性时间O(n)的下一个节点。
CMS

8
Big-O表示法描述了算法的速度如何根据其输入的大小而变化。一个O(n)算法需要两倍的时间才能运行两倍的项目,而花费8倍的时间才能运行8倍的项目。换句话说,O(n)算法的速度随[续...]
Gareth

8
其输入的大小。O(1)表示输入('n')的大小不考虑算法的速度,无论输入大小如何,它都是恒定速度
Gareth

9
我看到您的O(1),然后抚养您O(0)。
克里斯·康威

23

并非所有程序都做相同的事情或在相同的硬件上运行。

这通常就是为什么存在各种语言功能的答案。数组是计算机科学的核心概念。用列表/矩阵/向量/任何高级数据结构替换数组将严重影响性能,并且在许多系统中完全不可行。在许多情况下,由于程序存在问题,应使用这些“高级”数据收集对象之一。

在业务编程中(我们大多数人都这样做),我们可以针对功能相对强大的硬件。在这种情况下,使用C#中的列表或Java中的Vector是正确的选择,因为这些结构使开发人员可以更快地实现目标,从而使这类软件的功能更加强大。

在编写嵌入式软件或操作系统时,阵列通常是更好的选择。虽然阵列提供的功能较少,但占用的RAM却更少,并且编译器可以更有效地优化代码以查找阵列。

我敢肯定,在这些情况下,我会遗漏许多好处,但我希望您能理解。


4
具有讽刺意味的是,在Java中,应该使用ArrayList(或LinkedList)代替Vector。这与矢量同步有关,这通常是不必要的开销。
ashirley

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.