すべてのデータ型は、ポインターを持つノードに要約されますか?


21

配列またはベクトルは単なる値のシーケンスです。リンクリストを使用して確実に実装できます。これは、次のノードへのポインタを持つ単なるノードの集まりです。

スタックとキューは、Intro CSコースで一般的に教えられる2つの抽象的なデータタイプです。クラスのどこかで、学生はリンクされたリストを基礎となるデータ構造として使用してスタックとキューを実装する必要があります。つまり、同じ「ノードのコレクション」の考え方に戻ります。

優先キューは、ヒープを使用して作成できます。ヒープは、ルートに最小値を持つツリーと考えることができます。BST、AVL、ヒープを含むあらゆる種類のツリーは、エッジで接続されたノードのコレクションと考えることができます。これらのノードは、1つのノードが別のノードを指すようにリンクされています。

すべてのデータ概念は、他の適切なノードへのポインターを持つノードのみに常に要約できるようです。そうですか?これが単純な場合、教科書は、データが単なるポインターを持つノードの集まりであると説明していないのはなぜですか?ノードからバイナリコードへの移行方法


5
ほのめかしている基本的なデータ構造は、「コンセル」と呼ばれます。好きなデータ構造を構築できます。特定の教科書の著者がコンスセルの説明を選択しなかった理由を知りたい場合は、なぜその選択をしたのかを著者に尋ねてください。ノードの配置の説明からバイナリコードに移行することは「コンパイル」と呼ばれ、「コンパイラ」のタスクです。
エリックリッパー

18
また、すべてのデータ構造が配列に要約されると主張することもできます。結局、それらはすべてメモリに格納されますが、これは1つの非常に大きな配列です。
BlueRaja-ダニーPflughoeft

10
インデックスを作成する場合、リンクリストを使用して配列を実装することはできません。O(1)
svick

5
申し訳ありませんが、「ノードとポインター」について話すと、すでにキッシュイーティングに負けていることになります。「すべての本物のプログラマーが知っているように、唯一の有用なデータ構造は配列です。文字列、リスト、構造、セット-これらはすべて配列の特殊なケースであり、あらゆる種類のプログラミング言語を台無しにすることなく同じように簡単に処理できます。合併症の「参考: 『本物のプログラマから、』パスカルを使用していないweb.mit.edu/humor/Computers/real.programmers
alephzero

3
...しかし、もっと深刻なのは、データ構造に関する重要なことは、データ構造の実装方法ではなく、データ構造でできることです。21世紀に自分で実装するのはプログラミングの演習に過ぎません。怠け者の教育者にとっては、そのような演習が簡単に採点できるという事実は、せいぜい無意味であり、学生が「 「車輪の再発明」は、実際のプログラミングで役立つ活動です。
アレフゼロ

回答:


14

基本的に、それがすべてのデータ構造の要約です。接続のあるデータ。ノードはすべて人工的なもので、実際には物理的には存在しません。これがバイナリ部分の出番です。C++でいくつかのデータ構造を作成し、オブジェクトがメモリのどこにあるかを確認する必要があります。データがメモリにどのように配置されるかを学ぶことは非常に興味深い場合があります。

非常に多くの異なる構造の主な理由は、それらがすべて何らかのものに特化していることです。たとえば、ページがメモリから取得される方法のため、リンクされたリストの代わりにベクトルを反復処理する方が一般的に高速です。リンクされたリストは、より大きなサイズの型を格納する方が適切です。これは、ベクトルが未使用のスロットに余分なスペースを割り当てる必要があるためです(ベクトルの設計に必要です)。

サイドノートとして、あなたが見たい興味深いデータ構造はハッシュテーブルです。説明しているNodes and Pointersシステムには完全には従いません。

TL; DR:コンテナは基本的にすべてのノードとポインターですが、非常に特定の用途があり、何かに対してはより良く、他に対してはより悪いです。


1
私の要点は、ほとんどのデータが実際にポインターを持つノードの束として表現できることです。ただし、それらは、(a)物理レベルでは、それが起こるものではなく、(b)概念レベルでは、リンクされたリストとして値を考えることは、基礎となるデータについて推論するのに有用ではないからではありません。いずれにせよ、思考を単純化するのはいずれにせよ単なる抽象化であるため、別の状況が機能する可能性がある場合でも、状況に最適な抽象化を選択することもできます。
derekchen14

13

すべてのデータ概念は、他の適切なノードへのポインターを持つノードのみに常に要約できるようです。

ああ、親愛なるいや。あなたは私を傷つけています。

他の場所で説明しようとしたように(「バイナリ検索ツリーとバイナリヒープの違いは何ですか?」)、固定データ構造であっても、それを理解するためのいくつかのレベルがあります。

あなたが言及した優先度キューのように、それを使用したいだけなら、それは抽象データ型です。格納するオブジェクトの種類、および実行するように依頼できる質問を把握して使用します。それはアイテムの袋としてのデータ構造です。次のレベルでは、その有名な実装であるバイナリヒープはバイナリツリーとして理解できますが、最後のレベルは効率的な理由で配列として実装されています。ノードとポインターはありません。

また、たとえばノードやポインタ(エッジ)のように見えるグラフの場合も、2つの基本的な表現、隣接配列と隣接リストがあります。私が想像するすべての指針ではありません。

データ構造を本当に理解しようとすると、それらの長所と弱点を研究する必要があります。表現では、効率(空間または時間)のために配列を使用する場合があります(柔軟性のため)。これは、C ++ STLのような優れた「缶詰」実装がある場合でも保持れます。これは、基礎となる表現を選択できる場合があるためです。そこには常にトレードオフがあります。


10

f:RR

連続関数を定義するためだけに、これらすべてを言うことを期待する人はいません。さもないと、誰も作業を完了できません。誰かが退屈な仕事をしてくれたことを「信頼」しているだけです。

考えられるすべてのデータ構造は、基になる計算モデルが扱う基本オブジェクト、ランダムアクセスマシンを使用する場合はレジスタの整数、チューリングマシンを使用する場合はテープのシンボルに要約されます。

抽象化を使用するのは、心が些細な問題から解放され、より複雑な問題について話すことができるからです。これらの構造が機能することを「信頼」することは完全に合理的です。細部に至るまでスパイラルダウンすることは、特別な理由がない限り、議論に何も加えない無駄な練習です。


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メソッド)と同じものです。

上記のブールのチャーチエンコーディングは、実際には、Smalltalkなどのオブジェクト指向言語で条件が実装される方法であることに注意してください。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のようなオブジェクト指向言語で実際に条件が実装される方法です。Smalltalkには、言語構造としてブール値、条件、またはループはありません。これらはすべて、純粋にライブラリとして実装されています。心はまだ吹き飛ばされていませんか?ウィリアム・クックは、ずっと前に明らかだったはずの何かを指摘しましたが、実際には気づかれていませんでした。 λ計算は必然的にオブジェクト指向です。エルゴ、λ計算では最古と...ある
イェルクWミッターク

…最も純粋なオブジェクト指向言語!
ヨルグWミットタグ

1
Smalltalkでの悪い日はC ++での良い日を打ち負かす:-)
ボブジャービス-モニカの復活

@JörgWMittagあなたの結論があなたの仮定に基づいているとは思わない、あなたの仮定が真実だとは思わない、そしてあなたの結論が真実だとは絶対に思わない。
マイルルーティング

4

多くの(ほとんど?)データ構造は、ノードとポインターで構築されます。配列は、一部のデータ構造のもう1つの重要な要素です。

最終的に、すべてのデータ構造はメモリ内の単なる単語の束、または単なるビットの束です。重要なのは、それらがどのように構成され、どのように解釈して使用されるかです。


2
最終的に、ビットは、ワイヤ上の電気信号の束、光ファイバケーブル内の光信号、またはプラッタ上の特定の磁化粒子、または特定の波長の電波、または、またはです。質問は、どのくらい深く行きたいですか?:)
ワイルドカード

2

はい、データ構造の実装は常にノードとポインターに要約されます。

しかし、なぜそこで停止するのですか?ノードとポインターの実装は、ビットに要約されます。

ビットの実装は、電気信号、磁気ストレージ、おそらく光ファイバケーブルなどに要約されます(一言で言えば、物理学です)。

これは「すべてのデータ構造がノードとポインターに要約される」という陳述の不条理です。それは本当ですが、それは実装にのみ関係しています。


Chris Date 実装モデルを非常によく区別していますが、彼のエッセイは特にデータベースを対象としています。

モデルと実装の間に単一の境界線がないことに気付いたら、もう少し先に進むことができます。これは、「抽象層」の概念に似ています(同一でない場合)。

特定の抽象化レイヤーでは、「下にある」レイヤー(構築しているレイヤー)は、対処する抽象化またはモデルの単なる「実装の詳細」です。

ただし、抽象化の下位層自体には実装の詳細があります。

ソフトウェアのマニュアルを読むと、そのソフトウェアが「提示する」抽象化層について学習していることになります。この抽象化層では、独自の抽象化を構築できます(またはメッセージの送信などのアクションを実行します)。

ソフトウェアの実装の詳細を学ぶと、作成者が構築した抽象化をどのように支えたかがわかります。「実装の詳細」には、データ構造とアルゴリズムなどが含まれます。

ただし、物理コンピューターで実際に「ビット」、「バイト」、および「ストレージ」がどのように機能するかは、電圧測定が特定のソフトウェアの「実装の詳細」の一部と見なされません

要約すると、データ構造は、アルゴリズムとソフトウェアに関する推論と実装のための抽象化層です。 この抽象化レイヤーは、ノードやポインターなどの下位レベルの実装の詳細に基づいて構築されているという事実は真実ですが、抽象化レイヤー内では無関係です。


システムを本当に理解するための大部分は、抽象化レイヤーがどのように適合するかを把握することです。そのため、データ構造の実装方法を理解することが重要です。 しかし、それら実装されているという事実は、データ構造の抽象化が存在しないことを意味しません


2

配列またはベクトルは単なる値のシーケンスです。リンクリストを使用して確実に実装できます。これは、次のノードへのポインタを持つ単なるノードの集まりです。

配列またはベクトルはリンクリストを使用して実装できますが、ほとんど実装する必要はありません。

nnΘ(n)Θ(logn)Θ(1)(つまり、ランダムアクセスメモリのシーケンシャルブロック)。また、CPUでは、実際の配列へのアクセスは実装がはるかに簡単で実行が高速であり、別々のノード間のポインターにスペースを浪費する必要がないため、格納に必要なメモリが少なくなります。

Θ(n)Θ(1)Θ(1)平均して、配列の実際に割り当てられたサイズを、たとえば2の最も近い累乗に切り上げるだけで、最大で一定の余分なメモリファクタのコストで。ただし、多くの挿入および/または削除を行う必要がある場合リストの中央にある要素の場合、物理配列はデータ構造の最適な実装ではない可能性があります。ただし、かなり頻繁に、挿入と削除を安価なスワップに置き換えることができます。

ツールボックスに物理的に連続した配列を含めるためにスコープを少し広げると、実際のほとんどすべてのデータ構造を、ノードおよびポインターと一緒に実際に実装できます。

Θ(1)反転操作)。ただし、実際には、これらの機能は、実装の複雑さや標準のガベージコレクションスキームとの非互換性などの欠点を克服するのに十分なほど有用ではありません。


1

これが単純な場合、教科書は、データが単なるポインターを持つノードの集まりであると説明していないのはなぜですか?

それは「データ」の意味ではないからです。あなたは抽象的なアイデアと実装を混同しています。「データ」は非常に抽象的なアイデアです。「情報」の単なる別名です。ポインターを持つリンクされたノードの束(別名、「リンクされたデータ構造」)は、はるかに具体的なアイデアです。これは、情報を表現および編成する特定の方法です。

一部のデータ抽象化は、「リンク」実装に非常に適しています。明示的なノードとポインター(またはノードとポインターの同型)を使用せずに、完全に一般的なツリーの分岐の性質を実装する良い方法は多くありません。浮動小数点数が思い浮かびます。

スタックとキューはその中間に位置します。スタックのリンクされた実装を行うことが非常に理にかなっている場合があります。配列と単一の「スタックポインタ」を使用する方がはるかに適切な場合もあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.