是否每个数据类型都可以归结为带有指针的节点?


21

数组或向量只是一系列值。它们肯定可以通过链表实现。这只是一堆带有指向下一个节点的指针的节点。

堆栈和队列是Intro CS课程中通常教授的两种抽象数据类型。在课堂上的某个地方,学生通常不得不使用链接列表作为基础数据结构来实现堆栈和队列,这意味着我们回到了相同的“节点集合”的想法。

可以使用堆创建优先级队列。堆可以看作是一棵以最小值为根的树。各种树(包括BST,AVL,堆)都可以看作是由边连接的节点的集合。这些节点链接在一起,其中一个节点指向另一个节点。

似乎每个数据概念总是可以归结为仅带有指向其他某个适当节点的指针的节点。那正确吗?如果这么简单,为什么教科书不解释数据仅仅是一堆带有指针的节点呢?我们如何从节点转到二进制代码?


5
您提到的基本数据结构称为“ cons cell”;您可以从中构建自己喜欢的任何数据结构。如果您想知道为什么给定教科书的作者没有选择解释缺点单元格,请询问该作者为何做出这样的选择。从节点排列的描述转到二进制代码被称为“编译”,并且是“编译器”的任务。
埃里克·利珀特

18
您还可以争论所有数据结构归结为一个数组。毕竟,它们最终都存储在内存中,这只是一个非常大的数组。
BlueRaja-Danny Pflughoeft

10
如果要保持索引则不能使用链表实现数组。O(1)
svick

5
抱歉,但是谈论“节点和指针”意味着您已经屈从于吃乳蛋饼。“ 正如所有Real Programmer所知,唯一有用的数据结构是Array。字符串,列表,结构,集合-这些都是数组的特例,可以轻松地以这种方式对待,而不会弄乱您的编程语言。并发症 “参考:‘真正的程序员不使用帕斯卡’,从web.mit.edu/humor/Computers/real.programmers
alephzero

3
...但是更重要的是,关于数据结构的重要事情是您可以使用它们做什么,而不是如何实现它们。在21世纪,自己实施编程只是编程练习,而对于懒惰的教育者来说,这样的练习易于评分的事实胜过这样的事实:最好的情况是毫无意义的,如果鼓励学生认为“重新发明轮子”是现实编程中的一项有用的活动。
alephzero

Answers:


14

好吧,这基本上就是所有数据结构的基础。带有连接的数据。这些节点都是人工的-它们实际上并不实际存在。这就是二进制部分的所在。您应该在C ++中创建一些数据结构,并检查对象在内存中的位置。了解数据在内存中的布局方式可能非常有趣。

这么多不同结构的主要原因是它们都专注于一件事或另一件事。例如,由于页面是如何从内存中拉出的,通常迭代向量而不是链接列表的速度更快。链接列表最好存储较大的类型,因为向量必须为未使用的插槽分配额外的空间(这在向量的设计中是必需的)。

附带说明一下,您可能要看的一个有趣的数据结构是哈希表。它并没有完全遵循您所描述的“节点和指针”系统。

TL; DR:容器基本上是所有节点和指针,但具有非常特定的用途,对某些事物有利,而对其他事物则不利。


1
我的观点是,大多数数据确实可以表示为一堆带有指针的节点。但是,这并不是因为(a)在物理层面上不是这样,并且(b)在概念层面上,将值视为链接列表对于推理基础数据没有太大用处。无论如何,全都是抽象,以简化我们的思维,因此,即使另一种可能可行,也最好为一种情况选择最佳的抽象。
derekchen14年

13

似乎每个数据概念总是可以归结为仅带有指向其他某个适当节点的指针的节点。

哦,亲爱的 你伤害了我。

就像我试图在其他地方解释的那样(“ 二进制搜索树和二进制堆之间有什么区别? ”),即使对于固定的数据结构,也有几个层次可以理解。

就像您提到的优先级队列一样,如果只想使用它,它就是一种抽象数据类型。您可以使用它来知道它存储的对象类型以及可以要求它执行的问题。一袋物品就是更多的数据结构。在下一级,其著名的实现,即二进制堆,可以理解为二进制树,但是出于效率的考虑,最后一级被实现为数组。那里没有节点和指针。

例如,对于肯定看起来像节点和指针(边)的图,您有两个基本表示形式,邻接数组和邻接列表。不是我想象的所有指针。

在真正尝试理解数据结构时,您必须研究它们的优点和缺点。有时,表示形式使用数组来提高效率(空间或时间),有时使用指针(出于灵活性)。这适用即使当你有很好的“罐头”的实现类似于C ++ STL,也因为那里你可以有时会选择底层表示。那里总是有一个权衡。


10

让我们用数学进行类比。考虑下面的句子:“ 是一个连续函数”。函数实际上是根据关系定义的,而关系是根据集合定义的。实数集是唯一的完全完整的有序字段:所有这些概念都用更简单的术语定义。为了谈论连续性,您需要邻域的概念,该概念是相对于拓扑结构定义的,等等,一直到ZFC的公理。F[R[R

没有人会期望您说所有这些只是为了定义一个连续的函数,否则没人会根本无法完成任何工作。我们只是“相信”有人为我们做了无聊的工作。

您可能想到的每个数据结构都可以归结为基础计算模型处理的基本对象,如果使用随机访问计算机,则某些寄存器中的整数,如果使用图灵机,则某些磁带上的符号。

我们之所以使用抽象,是因为它们使我们摆脱了琐碎的事情,使我们可以谈论更复杂的问题。仅仅“信任”那些结构是完全合理的:深入到每个细节都是-除非您有特定的理由这样做,否则这种徒劳的练习不会给您的论点增加任何东西。


10

这是一个反例:在λ演算中,每种数据类型都可以归结为函数。λ演算没有节点或指针,它仅有的就是函数,因此必须使用函数来实现。

这是使用ECMAScript编写的将布尔值编码为函数的示例:

const T   = (thn, _  ) => thn,
      F   = (_  , els) => els,
      or  = (a  , b  ) => a(a, b),
      and = (a  , b  ) => a(b, a),
      not = a          => a(F, T),
      xor = (a  , b  ) => a(not(b), b),
      iff = (cnd, thn, els) => cnd(thn, els)();

这是一个缺点列表:

const cons = (hd, tl) => which => which(hd, tl),
      first  = list => list(T),
      rest   = list => list(F);

可以将自然数实现为迭代器函数。

集合与其特征函数(即contains方法)相同。

请注意,上面的布尔值的Church Encoding实际上是在诸如Smalltalk之类的OO语言中实现条件的方式,这些语言不具有布尔值,条件或循环作为语言构造,而仅将它们实现为库功能。Scala中的一个示例:

sealed abstract trait Boolean {
  def apply[T, U <: T, V <: T](thn: => U)(els: => V): T
  def(other: => Boolean): Boolean
  def(other: => Boolean): Boolean
  val ¬ : Boolean

  final val unary_! = ¬
  final def &(other: => Boolean) =(other)
  final def |(other: => Boolean) =(other)
}

case object True extends Boolean {
  override def apply[T, U <: T, V <: T](thn: => U)(els: => V): U = thn
  override def(other: => Boolean) = other
  override def(other: => Boolean): this.type = this
  override final val ¬ = False
}

case object False extends Boolean {
  override def apply[T, U <: T, V <: T](thn: => U)(els: => V): V = els
  override def(other: => Boolean): this.type = this
  override def(other: => Boolean) = other
  override final val ¬ = True
}

object BooleanExtension {
  import scala.language.implicitConversions
  implicit def boolean2Boolean(b: => scala.Boolean) = if (b) True else False
}

import BooleanExtension._

(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3

2
@Hamsteriffic:试试看:实际上就是在诸如Smalltalk这样的OO语言中实现条件的方式。Smalltalk没有布尔值,条件或循环作为语言构造。所有这些纯粹是作为库实现的。头脑还没吹吗?威廉·库克(William Cook)指出了应该在很久以前就显而易见但并未真正注意到的一些事情:由于面向对象全部是关于行为抽象的,而行为抽象是λ微积分中唯一存在的一种抽象,因此所有用λ演算必然是OO。ERGO,λ演算是最古老和...
约尔格W¯¯米塔格

……最纯正的OO语言!
约尔格W¯¯米塔格

1
Smalltalk的糟糕表现胜过C ++的美好日子:-)
Bob Jarvis-恢复Monica的

@JörgWMittag我不认为您的结论是根据您的假设得出的,我也不认为您的假设是正确的,而且我绝对不认为您的结论是正确的。
Miles Rout

4

许多(大多数?)数据结构是由节点和指针构成的。数组是某些数据结构的另一个关键元素。

最终,每个数据结构只是内存中的一堆单词,或者只是一堆位。重要的是它们的结构方式以及我们如何解释和使用它们。


2
最终,位是一束电线上的电信号,或者是光缆中的光信号,或者是盘片上的特定磁化粒子,或者是特定波长或或或的无线电波。所以问题是,您想走多深?:)
通配符

2

数据结构的实现总是归结为节点和指针,是的。

但是为什么要停在那里?节点和指针的实现归结为位。

比特的实现归结为电信号,磁存储,也许还有光缆等(总之,物理学)。

这是语句“所有数据结构都归结为节点和指针”的荒谬说法。的确如此,但它仅与实现有关。


Chris Date 非常有能力区分实现模型,尽管他的文章特别针对数据库。

我们可以走得更远一点,如果我们认识到,没有一个单一的模型和实现之间的分界线。这与“抽象层”的概念相似(如果不相同)。

在给定的抽象层中,位于您“下面”的层(正在构建的层)仅仅是要处理的抽象或模型的 “实现细节” 。

但是,较低的抽象层本身具有实现细节。

如果您阅读某软件的手册,那么您正在学习该软件“表示”的抽象层,您可以在其上构建自己的抽象(或仅执行诸如发送消息之类的操作)。

如果您了解该软件的实现细节,您将了解创建者如何为他们构建的抽象提供基础。“实施细节”可以包括数据结构和算法等等。

但是,您不会将电压测量视为任何特定软件的“实现细节”的一部分,即使这是“位”,“字节”和“存储”在物理计算机上实际工作方式的基础。

总之,数据结构是用于推理和实现算法和软件的抽象层。 这个抽象层是基于较低层的实现细节(例如节点和指针)构建的,这是事实,但在抽象层中是无关紧要的。


真正理解系统的很大一部分是掌握抽象层如何组合在一起。因此,了解数据结构的实现方式非常重要 但是事实是,它们实现并不意味着不存在数据结构的抽象


2

数组或向量只是一系列值。它们肯定可以通过链表实现。这只是一堆带有指向下一个节点的指针的节点。

数组或向量可以用链表来实现,但几乎绝不应该如此。

那是因为访问 ñ链表中的第-th个元素需要遍历 ñ 指针,因此需要 Θñ时间。基于树的实现(仍然仅由节点和指针组成)效率更高,只需要Θ日志ñ 是时候访问任意叶子了,但是还远远不够 Θ1个通过使用实际数组(即随机存取存储器的顺序块)提供的访问时间。同样,在CPU上,访问实际数组更容易实现且执行起来更快,并且由于不必浪费单独节点之间的指针上的任何空间,因此存储它所占用的内存更少。

当然,实际的物理阵列也有其缺点:值得注意的是,它们需要 Θñ 是时候插入新元素了,链表可以在其中做些什么 Θ1个时间(如果您已经有一个指向相邻元素的指针)。物理数组末尾的插入可以摊销到Θ1个平均而言,仅通过将数组的实际分配大小四舍五入到2的最接近的幂即可,而最多以恒定的额外内存为代价,但是如果您需要进行很多插入和/或删除操作,在列表中间的元素中,物理数组可能不是您数据结构的最佳实现。但是,您经常可以用交换来替换插入和删除,这很便宜。

如果稍微扩大范围,在工具箱中包括物理连续数组,则几乎所有实用的数据结构实际上都可以与那些以及节点和指针一起实现。

实际上,我想不起,我想不出任何通用的数据结构 尽管在某些情况下通过跳出该范式可以节省一些空间或时间效率,但需要其他任何东西的。作为这种情况的说明性示例,请考虑XOR链表,该XOR链表允许通过替换各个下一个和上一个节点来使用(渐近地)使用比单向链表更多的空间来实现双向可遍历链表指针按位异或,还具有其他一些方便的功能(例如Θ1个逆向操作)。但是,实际上,这些功能很少有用,足以克服其缺点,其中包括额外的实现复杂性以及与标准垃圾回收方案的不兼容。


1

如果这么简单,为什么教科书不解释数据仅仅是一堆带有指针的节点呢?

因为那不是“数据”的意思。您正在将抽象思想与实现混为一谈。“数据”是一个高度抽象的概念:它只是“信息”的别称。一堆带有指针的链接节点(又称“链接数据结构”)是一个更为具体的想法:这是表示和组织信息的一种特定方式。

一些数据抽象非常适合“链接”的实现。不使用显式节点和指针(或节点和指针的某些同构)来实现完全通用树的分支性质的好方法并不多。但是,还有其他一些您永远不会使用节点和指针来实现的抽象。浮点数浮现在脑海。

堆栈和队列介于两者之间。有时候,进行堆栈的链接实现很有意义。在其他情况下,使用数组和单个“堆栈指针”更为有意义。

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.