一般的なデータ構造とアプリケーション

序文

データ構造は、コンピューターがデータを保存および整理する方法です。仕事では通常、タスクをより効率的に完了できるように、カプセル化されたコレクション API を直接使用します。しかし、プログラマーとして、データ構造をマスターすることは非常に重要です。データ構造はアルゴリズムの理解と設計に役立ち、それによってプログラムの効率と信頼性が向上します。この記事では、いくつかの一般的なデータ構造を紹介します。これらのデータ構造の特性と利点を理解することで、さまざまなシナリオで適切なデータ構造をより適切に選択できるようになります。

データ構造の概要

一般的なデータ構造は、線形と非線形の 2 つのタイプに大別されます。

リニアなデータ構造はその名の通り、全体の構造が直線であるイメージです。配列、リンク リスト、スタック、キューなどが含まれます。
非線形データ構造には、ツリー、ヒープ、グラフなどが含まれます。

配列

配列は、次の図に示すように、複数の要素で構成されるコレクションです。

ここに画像の説明を挿入します

配列をメモリ上に格納する空間は連続的であり、各要素が一定量のメモリ空間を占有するため、配列を宣言するときに長さを指定する必要があり、そうしないと、どのくらいの空間を占有するかがわかりません。Java 言語を例に挙げると、配列が宣言されると、以下に示すように、配列変数は配列オブジェクトの開始アドレス (最初の要素の位置) を指します。

ここに画像の説明を挿入します

この観点から、配列内の要素をクエリする場合、この要素のメモリ アドレスは添字を使用して計算できます。たとえば、添字 2 の要素を検索する場合は、arr[2] のメモリ アドレスが計算されます。 = arr アドレスのメモリ + 2 * 要素サイズ。つまり、要素はメモリ アドレスを通じて直接アクセスでき、時間計算量は O(1) です。

ただし、配列には問題もあります。配列の長さが固定されているため、要素を追加または削除するには、元の配列を置き換えるために新しい配列を作成する必要があり、その結果、非常に複雑になります。たとえば、次のコードは、配列の末尾に要素を追加する方法を示しています。

int[] arr = {
    
    1, 2, 3, 4, 5};  
  
arr[arr.length] = 6; // 将要添加的元素放到数组的最后一个位置  
  
int[] newArr = new int[arr.length + 1]; // 创建一个新的数组,长度加1  
  
for (int i = 0; i < newArr.length; i++) {
    
      
    newArr[i] = arr[i]; // 将原数组中的元素复制到新数组中  
}  
  
arr = newArr; // 使用新数组替换原数组

メモリ内でのサンプル コードのアクティビティは次のとおりです。

ここに画像の説明を挿入します

Java には、一般的に使用される ArrayList、Vector、HashMap、ArrayBlockingQueue など、配列に基づくコレクションの基盤となる実装が多数あります。

リンクされたリスト

リンク リストは一連のノードで構成されます。各ノードには 2 つの部分が含まれます。1 つはデータ要素を格納するデータ フィールドで、もう 1 つは次のノードのアドレスを格納するポインタ フィールドです。Java を例にとると、ノードの構造は次のように表されます。

public class Node<T> {
    
    

    //存储数据元素的数据域
    private T value;

    //下一个节点地址的指针域
    private Node next;
}

各要素のポインタは次の要素を指すため、次の図に示すようにリンクされたリストが形成されます。

ここに画像の説明を挿入します

配列とは異なり、リンク リストはメモリ内の非連続空間であるため、コンピュータのメモリ空間を最大限に活用し、柔軟な動的メモリ管理を実現し、事前にデータ サイズを知る必要がある配列の欠点を解決できます。メモリ内のストレージは次のようになります

ここに画像の説明を挿入します

配列と比較すると、リンク リストの挿入および削除操作は O(1) の複雑さに達する可能性がありますが (チェーンの終端にあるポインタを次のノードまたは null にポイントするだけです)、ノードの検索や特定の番号付きノードへのアクセスは必要ありません。が必要です O(n) 時間がかかります。

上で紹介した一方向のリンク リストには、最初から最後までしかたどることができないという欠点があります。最後から 2 番目のノードを削除する場合は、最初からのみたどることができます。より柔軟な操作とより高い効率を実現するために、二重リンク リストがあり、その構造は次のようになります。

ここに画像の説明を挿入します

構造が二重リンク リストの場合、最後から 2 番目のノードを削除するには、末尾ノードの前のノードを見つけて削除するだけです。Java の LinkedList は、二重リンク リストの実装です。

キューとスタック

配列やリンクリストは主にデータの格納構造やアクセス方法に着目し、キューやスタックはデータの処理順序やロジックに着目するなど、それぞれに特徴があります。

キューの特性は先入れ先出し (FIFO) です。キューに入る最初の要素が最初にアクセスまたは取り出されます。要素を追加する場合、要素はキューの最後にキューに入れられ、先頭でデキューされます。その表現は以下のようになります

ここに画像の説明を挿入します

スタックの特性は先入れ後出し (FILO) です。スタックにプッシュされた最初の要素が最後にアクセスまたは取り出されるか、スタックにプッシュされた最後の要素が最初にアクセスされます。取り出した。スタックでは、スタックの最上部でのみ挿入および削除操作が可能です。

非常に鮮明な説明は、「スタックを弾倉と考えることができます。装填された最初の弾丸が底部に押し込まれ、発射されると上部から飛び出すことになります。」です。

ここに画像の説明を挿入します

両方の基盤となる実装では、特定のニーズやシナリオに応じて、基盤となるデータ構造として配列またはリンク リストを選択できます。たとえば、Java の ArrayBlockingQueue は配列を通じて実装されるブロッキング キューであり、LinkedBlockingQueue はキューを通じて実装されるノンブロッキング キューです。

ツリーは非線形構造であり、階層関係を持つ n 個の限定されたノードの集合です。ツリーにも、バイナリー ツリー、バランス ツリー、2-3-4 ツリー、赤黒ツリー、B ツリー、B+ ツリーなど、さまざまな種類があります。

二分木は、各ノードが最大 2 つの部分木を持つ木構造です。通常、二分探索木の実装に使用されます。その特徴は、左の子ノードの値がルート ノードの値より小さく、右側の子ノードの値がルート ノードの値より大きくなります。Java を例にとると、二分探索木の構造は次のように表されます。

public class Node {
    
    

    //当前节点的值
    private int value;

    //父节点、左子节点、右子节点
    private Node parent,left,right;

}

式は以下のようになります

ここに画像の説明を挿入します

クエリの時間計算量は O(log n) であり、リンク リストと比較してクエリ効率が大幅に向上します。ただし、最悪の場合、次のような O(n) に縮退する可能性があります。

ここに画像の説明を挿入します


この状況を回避するために、AVL ツリーが誕生しました。AVL ツリーは自己平衡型の二分探索ツリーであり、操作の挿入および削除時に、各ノードの左右のサブツリー間の高さの差が 1 を超えないように、左または右の回転によって構造を自動的に調整します。ツリーのバランスを維持することにより、クエリの時間計算量が O(log n) になることも保証されます。

次の図を例にすると、ノード 5 を挿入すると、ノード 7 の左右のサブツリーの高さの差は 2 になります。このとき、ノード 7 はツリーのバランスを保つために右回転する必要があります。

ここに画像の説明を挿入します
右回転とは、ノードを回転点として、その左の子ノードが親ノードになり、左の子ノードの右の子ノードが左の子ノードになり、右の子ノードは変更されないことを意味します。
同様に、左回転とは、ノードを回転点として、その右の子ノードがその親ノードになり、右の子ノードの左の子ノードがその右の子ノードになり、左の子ノードは変更されないことを意味します。


AVL はローテーションによってツリーのバランスを保ちますが、頻繁に挿入や削除が行われるシナリオでは、頻繁なローテーションはパフォーマンスの低下につながるため、この問題を解決するために赤黒ツリーが提案されました。

赤黒の木については誰でもよく知っているはずで、面接でもよく聞かれるはずですが、理解しているかどうかは別問題です。

赤黒ツリーは自己平衡型二分探索ツリーでもあり、ノードの色を使用してツリーのバランスを確保します。赤黒木はAVLに比べて分かりにくいですが、最初の疑問は「右巻きも左巻きもあるんじゃないの?めんどくさい。節点の色が変わるのに誰が混乱させているの?」ということです。 。

赤黒ツリーについてはまた別途記事を書きますが、結論から言うと、赤黒ツリーはAVLツリーに比べて回転数が少ないため、挿入や削除などの操作が多い場合に最適です。では、赤黒ツリーがよく使われます。たとえば、HashMap は誰もが知っています。下の図は、AVL ツリーと赤黒ツリーに 9、7、6、10、5、8、4、2、1、0 を順番に挿入したもので、両者の間には構造的な違いがあることがわかります。 。

ここに画像の説明を挿入します


上で述べたツリーはすべてバイナリ ツリーです。つまり、各ノードには 2 つの分岐のみがあり、それらはすべて順序付けされています。分岐が 2 つしかないため、これはバイナリ ツリーの一般的な問題でもあります。データが増えるとツリーの高さが高くなります。この状況は、データベースやファイル システムなどのシナリオには適していません。

上で述べたツリー構造はすべてバイナリ ツリーであり、各ノードには子ノードが 2 つだけあり、それらはすべて順序付けされています。データ量が増加し続けると、バイナリ ツリーの高さも徐々に増加し、クエリの効率が低下します。ディスク I/O 操作を伴うシナリオでは、ツリーの高さが高くなるほど、クエリの実行が困難になります。

上記の問題を解決するために、マルチツリー構造が採用され、ツリーの高さを効果的に削減し、クエリ効率を向上させることができます。

一般的なマルチツリーには、データベースやファイルシステムでよく使われる2-3-4ツリー、B-tree、B+ツリーなどがあり、その表現は以下の通りです。

ここに画像の説明を挿入します

B+ ツリーは B ツリーの拡張であり、ディスクやその他のストレージ デバイスでの使用により適しています。B+ ツリーでは、リーフ以外のノードにはデータ情報が格納されず、キーワードと子ノード ポインタのみが格納されるため、インデックスなどのより有効なデータが格納されます。同時に、各リーフ ノードは隣接するリーフ ノードへのポインタを指すため、データベース範囲内でのクエリが非常に効率的になります。

ヒープ

ヒープは、各ノードがその各子ノード以上 (以下) であることを特徴とする特殊なツリー データ構造です。

一般的なヒープには、バイナリ ヒープ、フィボナッチ ヒープなどが含まれます。バイナリ ヒープは、最大ヒープと最小ヒープに分割できる完全なバイナリ ツリーです。最大ヒープ内の各ノードは、その子ノード以上です。最小ヒープ すべてのノードin はその子ノード以下です。以下の図の左側は最大ヒープの表現を示しており、右側は完全なバイナリ ツリーに準拠していません (左から右に挿入されたノードは完全なバイナリ ツリーです)。

ここに画像の説明を挿入します

ヒープのルート ノードは常に最大または最小であるため、ヒープは優先キューとしてよく使用されます。

要約する

多くのプログラミング言語では、さまざまなタイプのコレクション クラスが提供されています。Java を例に挙げると、一般的に使用されるコレクションには List、Set、Queue、Map が含まれ、それらの基礎となる実装は配列、リンク リスト、ツリーなどのデータ構造です。したがって、データ構造を理解することで、これらのコレクションをより適切に選択して使用できるようになり、問題を解決するためにより効率的なデータ構造を自分たちで設計することもできます。

おすすめ

転載: blog.csdn.net/qq_28314431/article/details/133853068
おすすめ