レッスンに戻る時間です。これらのことについては、今日の派手なマネージ言語ではあまり考えていませんが、同じ基盤の上に構築されているので、Cでメモリがどのように管理されるかを見てみましょう。
始める前に、「ポインタ」という用語の意味を簡単に説明します。ポインタは、メモリ内の場所を「指す」単なる変数です。このメモリ領域の実際の値は含まれていません。メモリアドレスが含まれています。メモリのブロックをメールボックスと考えてください。ポインタは、そのメールボックスへのアドレスになります。
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)と見なされます。配列が大きくなると、検索のコストが増加します。
非シーケンシャルデータ構造を使用すると利点が得られる場合があると私が言った上記を覚えていますか?データの検索はこれらの利点の1つであり、最良の例の1つはバイナリツリーです。
バイナリツリーは、リンクリストに似たデータ構造ですが、単一のノードにリンクする代わりに、各ノードは2つの子ノードにリンクできます。
==========
| 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つありますが、残りの2つを調べる必要はありませんでした。ノード(およびその子)には、探している値を含めることができないためです。これにより、最悪の場合はすべてのノードにアクセスする必要があることを意味する検索時間が得られますが、最良の場合には、ノードのごく一部にアクセスするだけで済みます。
これは、配列がビートになる場所であり、O(1)アクセス時間にもかかわらず、線形O(N)検索時間を提供します。
これはメモリ内のデータ構造に関する非常に高レベルの概要であり、多くの詳細をスキップしていますが、うまくいけば、他のデータ構造と比較した配列の長所と短所を示しています。