【问题标题】:Can most of the data structures be implemented using vectors?大多数数据结构可以使用向量实现吗?
【发布时间】:2013-09-21 16:54:15
【问题描述】:

我使用 C++ 向量来实现堆栈、队列、堆、优先级队列和有向加权图。在书籍和参考资料中,我看到了这些数据结构的大类,所有这些都可以使用向量来实现。 (使用指针可能更灵活)

我们是否也可以使用向量实现更高级的数据结构?
如果是,为什么 C++ 书籍仍然使用指针解释长类的概念?

是要记住低层次的想法,这样更生动还是让学生具备这种指针的用法?

【问题讨论】:

  • 如果您只对 C++ 上下文中的答案感兴趣,请相应地标记您的问题。
  • 如果数据结构需要数组对象,则使用向量。如果没有,那就不要。例如,maps 不能使用向量并符合标准。
  • @AlexChamberlain:我想看看这方面的参考资料。 std::map 需要一个分配器来管理它的内存,我不明白为什么 vector 不能作为一个分配器。
  • 我不认为实现基于vector 的分配器在问题的意义上是一种“有效”的方法,因为它是围绕使用vector 会导致更简单的主张而构建的代码(而使用分配器的代码和分配器代码一样复杂!)。否则不可能根据vector 实现标准容器,例如stackO(1) 用于在前面推送/弹出,而vector 不是。因此,如果您将vector“伪装”为stack,它根本不会做它应该做的事情。
  • 向量用于包含相同类型的对象。例如,我不能使用向量来包含:字符串名称、整数年龄、浮点率。

标签: c++ data-structures vector


【解决方案1】:

的确,许多数据结构可以在向量(数组,为了回答这个问题)之上实现,基本上它们都可以,因为每个计算任务都可以实现在图灵机上运行更基本的数据访问能力(或者,在现实世界中,您可能会说您使用指针实现的任何程序最终都运行在具有简单数组的虚拟内存空间的 CPU 上,因此您可以将其称为一个巨大的数组) .然而,它并不总是聪明的。两个主要原因:

  1. 性能/时间复杂度 - 向量根本无法提供 O(1) 中的所有基本操作。有一个快速初始化的解决方案,但是尝试将值随机插入到一个大向量中,看看你的表现有多糟糕——那是因为你必须一遍又一遍地将所有元素移动一个位置。列表可以在一次操作中完成。当然,其他结构也有自己的性能缺陷,但这就是使用这些基本构建块设计复杂数据结构的美妙之处。

  2. 1234563但还有更复杂的结构。举个例子一棵树,一个简单的完整二叉树可以很容易地用向量实现,因为父子关系可以很容易地转换为索引算术,但是如果树不完整并且每个节点有不同数量的子节点怎么办?现在,您可能会说它仍然可以完成(例如,任何图形都可以通过邻接矩阵或邻接列表使用向量来实现),但是当您可以使用指针链接进行更简单的实现时,这样做几乎没有意义。想想用一个数组做一个 AVL 滚动。 :颤抖:

请注意,第二个参数很可能归结为性能(“嘿,这是一种尴尬的方法,但我仍然设法使用向量!”),但不仅如此 - 它会使您的代码复杂化,使您的代码混乱数据结构设计,并可能使其更容易出现错误。

现在,“但是”出现了——尽管使用该语言为您提供的所有可能的工具很有意义,但将基于向量的结构用于性能关键任务已被广泛接受。查看几乎所有科学 CPU 基准测试,其中大多数最终都依赖于向量(未引用,但如果有人感兴趣,我可以进一步详细说明。可以说,即使是众所周知的 *graph*500 也能做到这一点) .

原因不是它是最佳编程实践,而是它更适合 CPU 内部结构,并从硬件中获得更多“汁液”。这是由于空间局部性 - CPU 非常喜欢它,因为它允许内存单元并行访问(在数组中你总是知道下一个元素在哪里,在列表中你必须等到当前元素被获取),而且发出流/跨步预取,以减少未来请求的延迟。 我不能说这总是一个好的做法,当你遍历一个图时,即使你使用数组实现,访问仍然很不规则,但这仍然是一种非常常见的做法。

总而言之,从字面上理解这个问题 - 他们中的大多数都可以(对于“大多数”的给定定义,好吗?),但如果意图是“为什么教指针”,我相信你可以在为了了解您的限制以及您可以和应该使用的内容 - 您需要了解的不仅仅是数组甚至指针。一个好的程序员应该对所有事情都略知一二——操作系统设计、CPU 设计等。除非你真正了解你正在运行的结构,否则你无法做任何体面的事情,不幸的是(或不)包含很多指针

【讨论】:

  • 在向量顶部实现链表、树和其他链接数据结构实际上相当简单。只需将向量用作 RAM,将索引用作指针。当然,您必须执行自己的分配和取消分配,但简单的池易于实现、性能良好且不使用额外空间。其他地方提到的迭代器失效问题只存在因为标准分配器使用“真实”指针而不是向量中的索引,这是自己的实现可以而且应该做的事情。
  • 对,我在上面提到过,但这基本上是几乎所有方面的列表。您必须使用一些“next_index”字段来获得 O(1) 插入,因此会像使用正确的列表一样“遭受”碎片化。它仍然归结为连续数组方法与“链接”数据结构方法,无论以何种方式实现它们中的任何一种
  • 我不确定您是否误解了我的描述,是否误解了您的反对意见,或者您是否误用了“碎片化”一词。有关我所指方案的详细说明,请参阅altdevblogaday.com/2011/02/12/alternatives-to-malloc-and-new(最后一个)。如果您说它不像访问连续内存地址那样对缓存友好,那是对的,但这并不奇怪。 任何随机访问,无论是否在模拟另一个数据结构的过程中,都有相同的“问题”。
  • 感谢您的链接,看起来很有趣。我指的是随机访问与连续结构的性能。正如我所说 - 如果不是为了性能,您当然可以在数组中实现任何东西,就像 RAM 示例一样,但在现实世界中,您想要性能更好的东西。顺便说一句,我对此的兴趣来自研究通过硬件预取优化链接数据结构的方法,假设它们在程序和算法上下文中更“自然”,但由于性能问题经常被丢弃。跨度>
【解决方案2】:

您可以使用std::vector 作为后备存储来实现一种allocator。如果你这样做了,所有基础计算机科学的标准数据结构都可以在向量之上实现。但是,它几乎不会让您从使用指针中解放出来:向量实际上只是带有一些有用的附加操作的内存块,最显着的是扩展能力。

更重要的是:如果您不了解指针,您也不会了解如何将vector 用于高级数据结构。 vector 是一个有用的抽象,但它遵循 C++ 规则“你没有得到你不付出的”,所以它也是一个非常“瘦”的抽象,你根据您必须编写的代码量来支付抽象成本。

(Jonathan Wakely 在 cmets 中指出,当您在 vector 之上实现分配器数据结构时,您将无法获得 C++ 标准库对分配器数据结构所要求的确切保证。原则上,向量只是一种处理内存块的方法。)

【讨论】:

  • 我不认为“使用内部使用向量的分配器的红黑树”算作使用向量实现红黑树。你仍然需要实现红黑树的逻辑,这是红黑树特有的复杂部分,不能使用向量。
  • @JonathanWakely:如果需要,可以使用vector 更直接地实现红黑树;我已经将树结构实现为数组以防止内存碎片。
  • 您如何提供std::map 所具有的迭代器失效保证和算法复杂性保证?
  • @JonathanWakely:复杂性保证是根据对比较函数的调用次数而言的,它将被准确保留。但是迭代器保证是一个好点,我会编辑答案。
  • 我知道他们是,我的意思是您不能将 both 与数组一起使用。您可以使用 O(log n) 查找,也可以使用稳定的迭代器,而不是两者兼而有之。
【解决方案3】:

如果您正在学习 C++,您需要熟悉指针以及如何使用它们,即使有更多更高级别的概念可以为您完成这项工作。 是的,可以使用向量或列表来实现大多数数据结构,如果您刚开始学习编程,那么您自己知道如何编写这些数据结构可能是个好主意。

话虽如此,生产代码应该始终使用标准库,除非有充分的理由不这样做。

【讨论】:

    猜你喜欢
    • 2017-05-26
    • 1970-01-01
    • 1970-01-01
    • 2016-08-08
    • 2021-11-04
    • 1970-01-01
    • 1970-01-01
    • 2014-11-29
    • 2015-05-27
    相关资源
    最近更新 更多