この記事は、アルゴリズムとデータ構造に関する学習ノートの第 3 部であり、継続的に更新される予定です。友達と一緒に読んで学んでください。わからないことや間違っていることがあれば、ご連絡ください
データ構造
データ構造とは、互いに 1 つ以上の特定の関係を持つデータ要素のコレクションを指します。多くの場合、データ構造が異なると、アプリケーション シナリオごとに異なる処理効率がもたらされます。このノートでは、誰もがデータ構造をマスターできるように、以下の 8 つのデータ構造を図を用いて理論的に紹介および説明します。
データ構造の分類
データ構造は論理構造と物理構造に分類できます。
論理構造とは、具体的な問題を抽象化したモデルであり、オブジェクト内のデータ要素間の関係を表現した抽象的な意味の構造です。一般的な論理構造には、線形構造と非線形構造(集合構造、ツリー構造、グラフ構造) があります。
- データ要素間に 1 対 1 の関係がある、配列、リンク リスト、スタック、キューなどの線形構造。
- ヒープやハッシュ テーブルを含むコレクション構造。データ要素は同じコレクションに属する以外の関係がありません。
- データ要素間に 1 対多の階層関係があるツリー構造 (ツリーなど)。
- 図に示すように、データ要素が多対多の関係にあるグラフ構造。
物理構造 はストレージ構造としても知られ、コンピュータ内の論理構造を実際に表現したものです。一般的な物理構造には、メモリ構造を表すシーケンシャル ストレージ構造とチェーン ストレージ構造、外部メモリとメモリ間の相互作用構造を表すインデックス ストレージ構造とハッシュ ストレージ構造が含まれます。
- 配列を含むシーケンシャル ストレージ構造では、連続したアドレスを持つストレージ ユニットにデータ要素が配置され、データ間の論理的および物理的関係は一貫しています。
- リンクされたリスト、ツリー、グラフなどのリンクされたストレージ構造は、データ要素を任意のストレージ ユニットに格納します。このストレージ ユニットのグループは連続的または不連続であるため、データ要素間の物理的関係はデータ要素を反映できません。それらの間の論理的関係は、隣接する要素のアドレス情報を格納するために、リンクされたストレージ構造にポインタが導入されます。
- インデックス ストレージ構造: ストレージ ノード情報を確立することに加えて、ノードのアドレスを識別するために追加のインデックス テーブルも確立されます。インデックス テーブルは、複数のインデックス エントリで構成されます。
- ハッシュ ストレージ構造: ハッシュ ストレージとも呼ばれ、ノードのストレージ アドレスはノードのキー コード値によって決定されます。
配列
配列とは、連続したメモリ空間の集合を利用して、同じ種類のデータの集合を格納する線形テーブルのデータ構造であり、最も基本的なデータ構造と言えます。
上の図に示すように、データはメモリの連続空間に順番に格納されます。0、1、2、... は添え字を表し、配列の隣接する要素間のメモリ アドレスの間隔は、通常、配列データ型なので、各データのメモリ アドレス (メモリ上の位置) は配列添字を通じて計算できるため、ターゲット データに直接アクセスしてランダム アクセスの目的を達成できます。データ要素のタイプに応じて、配列は整数配列、文字配列、浮動小数点配列、ポインター配列、および構造体配列に分類できます。配列は、1 次元、2 次元、および多次元の表現を持つこともできます。
アドバンテージ
- インデックスによる要素のクエリの速度は非常に高速です。
- インデックスによる配列の反復処理も便利です。
欠点がある
- 配列のサイズは作成後に決定され、拡張することはできません。
- 配列には 1 種類のデータのみを保存できます。
- 要素の挿入と削除の操作は非効率的です。
配列の番号が 1 ではなく 0 から始まるのはなぜですか?
配列のモデルの観点から見ると、「添え字」の最も正確な定義は「オフセット」であるはずです。つまり、配列をa
表すa[0]
オフセットが0の位置、つまり先頭アドレスを意味し、a[k]
オフセットがk type_sizeの位置を意味するので、メモリアドレスa[k]
の次の式を使用するだけで済みます。
番号付けが 1 から始まる場合、配列要素
a[k]
の 次のようになります。
上の 2 つの式と比較すると、番号付けは 1 から始まり、配列要素への各ランダム アクセスには減算演算が必要です。配列は非常に基本的なデータ構造であるため、添え字による配列要素へのランダム アクセスも非常に基本的なプログラミング演算です。効率を最適化します。できるだけ大きくします。したがって、減算演算を 1 つ節約するために、配列は 1 ではなく 0 から番号付けを開始することを選択します。
配列の挿入/削除が非効率的になるのはなぜですか?
配列内のデータは順序付けされているため、特定の位置に新しい要素を挿入する場合は、次のデータを移動する必要があります。最良のケースはΩ ( 1 ) \Omega(1)です。Ω ( 1 )、最悪の場合O ( N ) O(N)O ( N )、各位置に要素を挿入する確率が同じであるため、ケースの平均時間計算量は( 1 + 2 + ⋅ ⋅ ⋅ ⋅ ⋅ + n ) / n = Θ ( n ) (1+2+\ cdot \cdot\cdot+n)/n = \Theta(n)( 1+2+⋅⋅⋅+n ) / n=Θ ( n )。
C言語
C言語では配列が基本的なデータ型であり、配列変数を宣言することで配列を作成できます。添字を使用して配列内の要素にアクセスできます。簡単なサンプルコードを次に示します。
int arr[10]; // 声明一个包含10个元素的整型数组
arr[0] = 1; // 给第一个元素赋值为1
リンクされたリスト
連結リストは、データ要素が連鎖的に格納されるデータ構造であり、この格納構造は物理的に不連続であるという特徴があります。リンクされたリストは一連のデータ ノードで構成され、各データ ノードにはデータ フィールドとポインター フィールドの 2 つの部分が含まれます。このうち、ポインタフィールドには、データ構造の次の要素が格納されるアドレスが保存されます。
上の図は、先頭と末尾を持つ一般的な一方向リンク リストを示しています。逆方向リンク ポインタ フィールドまたはリンクの先頭と末尾を追加すると、二重リンク リストまたは一方向循環リンク リストを形成することもできます。
ポインタを介して次のデータ要素を検索してアクセスするため、リンクリストの自由度が高くなります。これは、ノードを追加および削除するときに、他のノードを変更せずに、前のノードのポインタ アドレスのみを変更する必要があることを示しています。しかし、何事にも両極端があり、ポインタは高い自由度をもたらしますが、必然的にデータ検索の効率や余分な領域の使用が犠牲になります。
アドバンテージ
- 1 つのノードで複数のデータ型を定義できます
- 挿入および削除操作がより効率的になる
欠点がある
アクセス効率が低い。
ヘッド ノード、ヘッド ポインター、ヘッド ノード
実際、上の図に示されているリンク リスト構造は完全ではありません。完全なリンク リストは、次の部分で構成される必要があります。 1. ヘッド ポインタ: 通常のポインタ。リンク リストの最初のノードを常に指すことを特徴とします。明らかに、ヘッド ポインタはリンク リストの位置を示すために使用されるため、後でリンク リストを見つけてテーブル内のデータを使用するのに便利です; 2. ノード: リンク リスト内のノードはヘッド ノードに分割されます。 、ヘッド ノードおよびその他のノード: ヘッド ノード: 実際にはデータを格納しない空のノード。通常はリンク リストの最初のノードとして使用されます。リンクされたリストの場合、ヘッド ノードは必要ありません。その機能は、いくつかの実際的な問題を解決するための便宜のためだけです。ヘッド ノード: ヘッド ノード (つまり、空のノード) のため、リスト内の最初のノードと呼ばれます。データを持つリンク リストがヘッド ノードになります。ヘッド ノードは、リンク リスト内の最初のデータ ノードの単なるタイトルであり、実際的な意味はありません。その他のノード: リンク リスト内の他のノード。
したがって、{1,2,3} を格納する完全なリンク リスト構造を次の図に示します。
注: リンク リストにヘッド ノードがある場合、ヘッド ポインタはヘッド ノードを指します。リンク リストにヘッド ノードがない場合、ヘッド ポインタはヘッド ノードを指します。
C言語
リンク リスト内の各ノードの特定の実装では、C 言語の構造体を使用する必要があります。具体的な実装コードは次のとおりです。
typedef struct Linklist{
int elem;//代表数据域
struct Linklist *next;//代表指针域,指向直接后继元素
}Linklist; //link为节点名,每个节点都是一个 link 结构体
一般に、リンク リストを作成するには typedef struct を使用します。これは、この方法で構造体変数を定義する場合、LinkList *a; を直接使用して構造体型変数を定義できるためです。
リンク リストを作成するには、次の作業が必要です: 1. ヘッド ポインターを宣言します (必要に応じて、ヘッド ノードを宣言できます); 2. データを格納する複数のノードを作成し、その先行ノードとの論理関係をいつでも確立します。作成プロセス ;
たとえば、{1,2,3,4} を格納し、ヘッド ノードを持たないリンク リストを作成する場合、C 言語の実装コードは次のようになります。
linklist * initlinklist(){
linklist * p=NULL;//创建头指针
linklist * temp = (linklist*)malloc(sizeof(linklist));//创建首元节点
//首元节点先初始化
temp->elem = 1;
temp->next = NULL;
p = temp;//头指针指向首元节点
//从第二个节点开始创建
for (int i=2; i<5; i++) {
//创建一个新节点并初始化
linklist *a=(linklist*)malloc(sizeof(linklist));
a->elem=i;
a->next=NULL;
//将temp节点与新建立的a节点建立逻辑关系
temp->next=a;
//指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对
temp=temp->next;
}
//返回建立的节点,只返回头指针 p即可,通过头指针即可找到整个链表
return p;
}
リンクリストへの要素の追加は、追加位置の違いにより、 1. リンクリストの先頭(ヘッドノード以降)にヘッドノードとして挿入する場合、 2. リンクリストの任意の位置に挿入する場合の 3 つに分けられます。リンク リストの中央; 3. リンク リストの最後のデータ要素としてリンク リストの最後に挿入します。
新しい要素の挿入位置は固定されていませんが、リンクされたリストに要素を挿入するという考え方は固定されており、指定した位置に新しい要素を挿入するには、次の 2 つの手順を実行するだけです。新しいノードのポインタを挿入位置に指定します。 2. 挿入位置より前のノードの次のポインタを挿入ノードに指定します。
リンクされたリストに要素を挿入する操作を実装する C 言語コードは次のとおりです。
//p为原链表,elem表示新数据元素,add表示新元素要插入的位置
linklist * insertElem(linklist * p,int elem,int add){
linklist * temp=p;//创建临时结点temp
//首先找到要插入位置的上一个结点
for (int i=1; i<add; i++) {
if (temp==NULL) {
printf("插入位置无效\n");
return p;
} //判断用户输入的插入位置是否有效
temp=temp->next;
}
//创建插入结点c
linklist * c=(linklist*)malloc(sizeof(linklist));
c->elem=elem;
//向链表中插入结点
c->next=temp->next;
temp->next=c;
return p;
}
指定されたデータ要素をリンク リストから削除することは、実際にはデータ要素を格納するノードをリンク リストから削除することになりますが、資格のあるプログラマとして、記憶域スペースに対して責任を負い、使用されなくなった記憶域スペースを適時に解放する必要があります。したがって、リンク リストからデータ要素を削除するには、次の 2 つの手順が必要です: 1. リンク リストからノードを削除します; 2. ノードを手動で解放し、ノードが占有しているストレージ スペースを再利用します。
その中で、リンク リストからノードを削除する実装は非常に簡単で、ノードの直接の先行ノードの温度を見つけて、1 行のプログラムを実行するだけです。
temp->next=temp->next->next;
したがって、リンク リストから要素を削除する C 言語の実装は次のようになります。
//p为原链表,add为要删除元素的值
linklist * delElem(linklist * p,int add){
linklist * temp=p;
//temp指向被删除结点的上一个结点
for (int i=1; i<add; i++) {
temp=temp->next;
}
linklist * del=temp->next;//单独设置一个指针指向被删除结点,以防丢失
temp->next=temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
free(del);//手动释放该结点,防止内存泄漏
return p;
}
リンクリストから削除されたノード del は、最終的に free 関数を通じて手動で解放されることがわかります。
リンクされたリストで指定されたデータ要素を見つけるために最も一般的に使用される方法は、リストの先頭から順にリスト内のノードをたどり、検索された要素と各ノードのデータ フィールドに格納されているデータ要素を比較することです。比較が成功するか、リンクリスト
したがって、リンク リスト内の特定のデータ要素を検索するための C 言語実装コードは次のとおりです。
//p为原链表,elem表示被查找元素、
int selectElem(linklist * p,int elem){
//新建一个指针t,初始化为头指针 p
linklist * t=p;
int i=1;
//由于头节点的存在,因此while中的判断为t->next
while (t->next) {
t=t->next;
if (t->elem==elem) {
return i;
}
i++;
}
//程序执行至此处,表示查找失败
return -1;
}
注: ヘッド ノードを使用してリンク リストをトラバースする場合、テスト データに対するヘッド ノードの影響を避ける必要があるため、リンク リストをトラバースする場合は、上記のコードでトラバーサル メソッドを確立して使用し、直接バイパスします。ヘッド ノードを使用して、リンク リストを効果的に走査します。
リンク リスト内の要素を更新するには、要素を格納しているノードをトラバースして見つけ、ノード内のデータ フィールドを変更するだけです。リンク リスト内のデータ要素を更新するための C 言語実装コードを直接指定します。
//更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
linklist *amendElem(linklist * p,int add,int newElem){
linklist * temp=p;
temp=temp->next;//在遍历之前,temp指向首元结点
//遍历到被删除结点
for (int i=1; i<add; i++) {
temp=temp->next;
}
temp->elem=newElem;
return p;
}
スキップテーブル*
ジャンプテーブルは魔法のようなデータ構造で、学部の教科書のほとんどのバージョンにはジャンプテーブルのデータ構造はなく、『アルゴリズム入門』と『アルゴリズム第4版』の2冊でも紹介されていません。ただし、ジャンプ テーブル内の要素の挿入、削除、検索の時間計算量は赤黒ツリーの時間計算量と同じであり、時間計算量は O ( log n ) O(\log n) です。O (ログ_n )であり、ジャンプ テーブルの実装は赤黒ツリーの実装よりも単純です。同時実行環境では、ジャンプ テーブルの操作はより局所的であり、ジャンプ テーブルのパフォーマンスが向上します。したがって、業界ではジャンプ テーブルがよく使用されます。
リンクリストはポインタフィールドを追加することで自由度が向上しますが、データクエリ効率の低下につながります。特にリンクされたリストの長さが非常に長い場合、データのクエリを最初からクエリする必要があり、効率が低くなります。ジャンプ テーブルの生成は、リンク リストが長すぎる問題を解決するために、リンク リストのマルチレベル インデックスを増やして、元のリンク リストのクエリ効率を高速化します (次の図を参照)。上の図では、ジャンプ テーブルは元の順序付きリンク
リスト インデックスに複数のレベルを追加します。プライマリ インデックスはn / 2 n/2です。n /2、二次インデックスはn / 4 n/4n /4、3次インデックスはn/8 n/8n /8 , · · インデックスを上方向に抽出することで検索効率が上がりますが、これも実は「時空間」のアルゴリズムです。したがって、スキップ リストは本質的に、二分検索を実行できる順序付けされたリンク リスト。
特性
- ジャンプ リストの本質は、バイナリ リストとリンク リストの概念を組み合わせた、多層の順序付きリンク リストです。
- ジャンプ リストの各ノードには 2 つのポインターが含まれており、1 つは同じリンク リスト内の次の要素を指し、もう 1 つは次のレイヤーの要素を指します。
- 要素がレベル i のリンク リストに表示される場合、その要素はレベル i より下のリンク リストにも表示されます。
- 最下位のリンクされたリストにはすべての要素が含まれます。
- ジャンプ テーブル クエリ、挿入、削除の時間計算量はO ( log n ) O(\log n)です。O (ログ_n )。
スキップリストの時間計算量解析
単一リンクリストでのクエリの時間計算量はO ( n ) O(n)です。O ( n ) 、今度はnnがあることを分析してくださいn個のノードには何レベルのインデックスがありますか?
2 つのノードごとに 1 つのノードが上位レベルのインデックスのノードとして抽出され、最初のレベルのインデックス ノードは約n / 2 n/2になります。そのうちn /2 、第 2 レベルのインデックスは約n/4 n/4n /4など、その後kk 番目kレベルのインデックス ポイントの数:n / 2 kn/2{^{k}}n /2k。
インデックスにhhがあるとします。レベルh、最高のインデックスは 2 ノードで、上記の例により次のように取得できます: n 2 h = 2 \frac{n}{2^h}=22hん=2求めます:h = log 2 n − 1 h=\log_2n-1h=ログ_2n−1 . 元のリンク リストの層が含まれる場合、スキップ リスト全体の高さは次のようになります:log 2 n \log_2nログ_2ん。
ジャンプ テーブル内の特定のデータをクエリするとき、各レイヤーがmmを横断する必要がある場合mノードの場合、ジャンプ テーブル内のデータのクエリの時間計算量は次のとおりです。O ( m ∗ log n ) O(m*\log n)O ( m∗ログ_n )、mmmの値は何ですか
探しているデータがxxであるとします。x、k番目kレベルのインデックスでは、yyyノードの後でx > y、x < zx > y、x < z であるバツ>y 、x<zなので、yykk 番目からのyの下向きポインタk − 1 k - 1へのkレベルのインデックスk−レベル1 のインデックス。k − 1 k - 1でk−レベル1インデックスでは、 yyy 和 z z zの間に別のノードを見つけますk − 1 k - 1k−第 1レベルのインデックスでは、最大 3 つのノードが走査されます。同様に、各レベルのインデックスは最大 3 つのノードのみを走査する必要があります。
上記の分析により、mmが得られます。mの値は3 です。係数は無視されるため、スキップ テーブル内のデータを検索する時間計算量はO ( log n ) O(\log n)O (ログ_n )の場合、検索の時間計算量は二分検索の時間計算量と同じです。
スキップリストの空間複雑性分析
ジャンプ テーブルは、要素の検索効率を向上させるために多くのレベルのインデックスを確立します。これは、「時間のための空間」の典型的なアイデアです。そのため、空間ではいくつかの犠牲が払われます。つまり、空間の複雑さはどれくらいですか?
元のリンク リストにnn が含まれている場合n要素、プライマリ インデックス要素の数はn / 2 n/2n /2、二次インデックス要素の数はn / 4 n/4n /4、第 3 レベルのインデックス要素の数はn / 8 n/8n /8 ... したがって、inode の合計は次のようになります:n / 2 + n / 4 + n / 8 + ... + 8 + 4 + 2 = n − 2 n/2 + n/4 + n/8 + ... + 8 + 4 + 2 = n-2n /2+n /4+n /8+…+8+4+2=n−2、空間計算量はO ( n ) O(n)O ( n )。
スキップテーブルの挿入と削除
データの挿入も非常に簡単に見えますが、ジャンプリストの元のリンクリストを順番に保持する必要があるので、要素を調べるように要素を挿入する位置を見つけます。したがって、挿入の全体的な時間計算量は、要素の検索の時間計算量O ( logn ) O(logn) になります。O ( l o g n )に、単一リンク リストに要素を挿入する時間計算量を加えたものはO ( 1 ) O(1)O ( 1 )、最終的に、時間計算量はO(logn) O(logn)O (ログオン)。 _ _ _
削除操作では、元のリンク リスト内のノードを削除するだけでなく、インデックス内のポイントも削除する必要があります。
スキップ リスト内の要素を削除する場合の時間計算量はどれくらいですか?
要素を削除するプロセスは、削除する要素 x が検索パス上で見つかった場合に削除操作が実行されることを除いて、要素を見つけるプロセスと似ています。ジャンプ リストでは、各インデックス層は実際には順序付けされた単一リンク リストであり、単一リンク リスト内の要素を削除する時間計算量はO ( 1 ) O(1)です。O ( 1 )、インデックス層の数はlog n \log nログ_n は、最大でもlog n \log nログ_n個の要素があるため、要素の削除にかかる合計時間には、要素を見つける時間と削除ログが含まれます n \log nログ_n要素の時間は O ( log n ) + O ( log n ) = 2 O ( log n ) O(\log n) + O(\log n) = 2 O(\log n)O (ログ_n )+O (ログ_n )=2 O (ログ_n )、定数部分を無視すると、要素を削除する時間計算量はO ( log n ) O(\log n)O (ログ_n )。
スキップテーブルの動的更新
ジャンプ リストに要素を挿入し続けると、2 つのインデックス ポイント間のノードが多すぎる可能性があります。ノードが多すぎると、インデックスを構築する利点が失われます。したがって、インデックスのサイズと元のリンク リストの間のバランスを維持する必要があります。つまり、ノードの数が増加し、それに応じてインデックスも増加し、2 つのインデックスとインデックスの間でノードが多すぎる状況を回避します。検索効率の低下。
ジャンプ テーブルは、ランダム関数によってこのバランスを維持します。ジャンプ テーブルにデータを挿入するとき、同時にインデックスにデータを挿入することを選択できます。その後、どのレベルのインデックスに挿入するかが決まります。これはランダム関数です。インデックスのどのレベルに挿入するかを決定するために必要です。
たとえば、新しい要素が挿入されるたびに、要素に第 1 レベルのインデックスを確立する確率が 1/2、第 2 レベルのインデックスを確立する確率が 1/4、確率が 1/ になるようにします。このようにして、ジャンプテーブルの劣化による効率の低下を効果的に防止することができる。
C言語
ジャンプ テーブルの C 言語実装については、[LeetCode.1206] ジャンプ テーブルの設計とhttps://blog.csdn.net/pcj_888/article/details/110723507を参照してください。
まとめ
配列、リンク リスト、ジャンプ リストの類似点と相違点も、インタビューで高頻度に検査されるポイントの 1 つです。以下は、配列、リンク リスト、スキップ リストを比較した表であり、それらの間の違いと関連性をよりよく理解するのに役立つことを願っています。
保管方法 | データ長 | 挿入/削除操作 | アクセス操作 | |
---|---|---|---|---|
配列 | 連続したメモリ空間 | 長さは固定されており、通常は動的に拡張できません。 | O ( n ) / O ( n ) O(n) / O(n)O ( n ) / O ( n ) | ランダムアクセスO ( 1 ) O(1)○ (1) |
リンクされたリスト | ポインタを介してノードを接続する非連続メモリ空間 | 長さは動的に変更可能 | O ( 1 ) / O ( 1 ) O (1) / O (1)O ( 1 ) / O ( 1 ) | シーケンシャルアクセスO ( n ) O(n)O ( n ) |
ジャンプ台 | バランスのとれたツリーに似たマルチレベルのインデックス構造 | 長さは動的に変更できますが、インデックスを保存するには追加のスペースが必要です | O ( log n ) / O ( log n ) O(\log n) / O(\log n)O (ログ_n ) / O (ログ_n ) | 高速アクセスO ( log n ) O(\log n)O (ログ_n ) |