
コンピュータサイエンスにおいて、リンクリストは、メモリ内の物理的な配置によって順序が決まるのではなく、各要素が次の要素を指す線形データ要素の集合です。これは、一連のノードで構成されるデータ構造です。最も基本的な形式では、各ノードにはデータと、シーケンス内の次のノードへの参照(つまりリンク)が含まれています。この構造により、反復処理中にシーケンス内の任意の位置から要素を効率的に挿入または削除できます。より複雑なバリアントでは、追加のリンクが追加され、任意の位置へのノードのより効率的な挿入または削除が可能になります。リンクリストの欠点は、データアクセス時間がリスト内のノード数に対して線形であることです。ノードは直列にリンクされているため、任意のノードにアクセスするには、前のノードに事前にアクセスする必要があります(パイプライン処理が困難になります)。ランダムアクセスなどの高速アクセスは実現できません。配列は、リンクリストと比較してキャッシュの局所性が優れています。
連結リストは、最も単純で一般的なデータ構造の一つです。リスト、スタック、キュー、連想配列、S式など、他の多くの一般的な抽象データ型を実装するために使用できますが、連結リストを基盤とせずにこれらのデータ構造を直接実装することも珍しくありません。
従来の配列に対するリンクリストの主な利点は、データ項目をメモリやディスク上に連続して格納する必要がないため、構造全体を再割り当てまたは再編成することなく、リスト要素を簡単に挿入または削除できることです。一方、実行時に配列を再構築するのははるかにコストのかかる操作です。配列では、データはメモリに連続して格納されます。つまり、データは前のデータの次の空きメモリ位置に格納されます。しかし、リンクリストでは、データは連続したメモリ位置に格納されず、代わりに他のノードのメモリ アドレスへの参照を保持するノードが順番に格納されます。リンクリストでは、リスト内の任意の場所でノードを挿入および削除でき、リストの走査中に追加または削除されるリンクの前のリンクをメモリに保持することで、一定回数の操作でこれを行うことができます。
一方、単純な連結リストはそれ自体ではデータへのランダムアクセスや効率的なインデックス付けができないため、リストの最後のノードを取得したり、特定のデータを含むノードを見つけたり、新しいノードを挿入する場所を特定したりするなど、多くの基本的な操作ではリストの要素のほとんど、あるいはすべてを反復処理する必要がある場合があります。
情報のリンクリストはデジタル時代より2000年以上も前に始まり、遅くともホメロスの時代にはパピルス巻物を写本する書記が、巻物の最後に次の巻物の最初の単語(後にラテン語の現在分詞reclamans(複数形reclamantes、文字通り「叫び返す」またはその名詞化された対応語)として知られる)を書き込むことで、読者や後続の写字生に読む順序の指示を与えることがよくありました。[ 1 ]この慣習はヨーロッパで書籍の機械印刷が 始まった初期の頃に再び現れ、印刷業者が印刷された各ページの最後に、次のページの最初の単語に対応する「キャッチワード」を含めるのが一般的でした。この慣習により、印刷業者は、各verso (ヨーロッパの言語では読者の左側)ページが正しく前のrectoページの裏に印刷されていること、および照合と製本の際に各rectoページが正しく前のversoページに続くように配置されていることを検証する能力が向上しました。[ 2 ]
コンピュータ科学の文脈におけるリンクリストの最初の実装は、1955年から1956年にかけて、ランド研究所とカーネギーメロン大学のアレン・ニューウェル、クリフ・ショー、ハーバート・A・サイモンによって、彼らの情報処理言語(IPL)の主要なデータ構造として開発されました。IPLは、論理理論マシン、汎用問題解決装置、コンピュータチェスプログラムなど、初期の人工知能プログラムの開発に著者らによって使用されました。彼らの研究に関する報告は、1956年にIRE Transactions on Information Theoryに掲載され、1957年から1959年にかけて、1957年と1958年の西部合同コンピュータ会議の議事録、1959年の情報処理(第1回ユネスコ国際情報処理会議の議事録)など、いくつかの会議議事録に掲載されました。リストノードを表すブロックと、連続するリストノードを指す矢印で構成される、現在では古典的な図は、ニューウェルとショーによる「Programming the Logic Theory Machine」に掲載されています。 WJCC、1957年2月。ニューウェルとサイモンは、「人工知能、人間の認知心理学、リスト処理に基礎的な貢献をした」として、1975年にACMチューリング賞を受賞しました。自然言語処理における機械翻訳の問題は、マサチューセッツ工科大学(MIT)のビクター・イングヴェに、言語学分野のコンピュータ研究のためのCOMITプログラミング言語でデータ構造としてリンクリストを使用するように促しました。この言語に関する「機械翻訳のためのプログラミング言語」と題されたレポートは、1958年にMechanical Translation誌に掲載されました。
リンクリストの初期の例としては、ハンス・ペーター・ルーンが1953年1月にIBMの社内メモで、連鎖ハッシュテーブルにリンクリストを使用することを提案した例がある。[ 3 ]
LISP(リストプロセッサの略)は、 1958年にジョン・マッカーシーがMIT在籍中に開発し、1960年にACMの通信誌に「記号式の再帰関数とその機械による計算、パートI」というタイトルの論文でその設計を発表しました。LISPの主要なデータ構造の一つは、リンクリストです。
1960年代初頭までに、連結リストと、これらの構造を主要なデータ表現として使用する言語の両方の有用性は十分に確立されていた。MITリンカーン研究所のバート・グリーンは、1961年3月にIRE Transactions on Human Factors in Electronics誌に「記号操作のためのコンピュータ言語」と題するレビュー論文を発表し、連結リスト方式の利点をまとめた。その後、ボブロウとラファエルによる「リスト処理コンピュータ言語の比較」というレビュー論文が、1964年4月にCommunications of the ACM誌に掲載された。
Technical Systems Consultants(当初はインディアナ州ウェストラファイエット、後にノースカロライナ州チャペルヒル)が開発したいくつかのオペレーティングシステムは、ファイル構造として単方向リンクリストを使用していた。ディレクトリエントリはファイルの最初のセクターを指し、ファイルの後続部分はポインタをたどることで特定された。この手法を採用したシステムには、Flex( Motorola 6800 CPU用)、mini-Flex(同じCPU用)、Flex9( Motorola 6809 CPU用)などがある。TSCが開発し、カリフォルニア州のSmoke Signal Broadcastingが販売した派生版では、同様の方法で双方向リンクリストが使用されていた。
IBMがSystem 360/370マシン向けに開発したTSS/360オペレーティングシステムは、ファイルシステムカタログに二重リンクリストを使用していた。ディレクトリ構造はUnixに似ており、ディレクトリにはファイルや他のディレクトリを含めることができ、任意の深さまで拡張可能だった。
連結リストの各レコードは、「要素」または「ノード」と呼ばれることが多い。
各ノードにおいて次のノードのアドレスを含むフィールドは、通常「ネクストリンク」または「ネクストポインタ」と呼ばれます。残りのフィールドは、「データ」、「情報」、「値」、「貨物」、「ペイロード」フィールドとして知られています。
リストの「先頭」は最初のノードです。リストの「末尾」は、先頭以降のリストの残りの部分、またはリストの最後のノードのいずれかを指します。Lispや一部の派生言語では、次のノードはリストの「 cdr」(/'kʊd.əɹ/と発音)と呼ばれ、先頭ノードのペイロードは「car」と呼ばれることがあります。
単方向連結リストは、「値」フィールドと「次」フィールドを持つノードで構成されます。「次」フィールドは、ノードの並びにおける次のノードを指します。単方向連結リストに対して実行できる操作には、挿入、削除、および走査があります。

以下のC言語コードは、単方向連結リストの末尾に「値」を持つ新しいノードを追加する方法を示しています。
#include <stdlib.h>// 連結リストの各ノードは構造体です。先頭ノードはリストの最初のノードです。typedef struct Node {整数値;struct Node * next ;}ノード;Node * addNodeToTail ( Node * head , int value ) {// Node ポインタを宣言し、リストの末尾に追加される新しい Node を指すように初期化します (つまり、新しい Node のメモリ アドレスを持つようになります)。Node * temp = ( Node * ) malloc ( sizeof * temp ); /// stdlib の 'malloc'。temp -> value = value ; // 新しいノードの値フィールドにデータを追加します。temp -> next = NULL ; // 無効なリンクを nil に初期化します。if ( ! head ) {head = temp ; // リンクリストが空の場合(つまり、head ノード ポインタが null ポインタの場合)、head ノード ポインタを新しい Node を指すようにします。}それ以外{Node * p = head ; // head ノードポインタを Node ポインタ 'p' に代入します。while ( p -> next ) {p = p -> next ; // p が最後のノードになるまでリストを走査します。最後のノードは常に NULL を指します。}p -> next = temp ; // 以前の最後のノードを新しいノードを指すようにします。}return head ; // ヘッドノードポインタを返します。}双方向リンクリストでは、各ノードは次のノードへのリンクに加えて、シーケンス内の「前の」ノードを指す2つ目のリンクフィールドを持ちます。この2つのリンクは、「前方」と「後方」、または「次」と「前」と呼ばれます。

XORリンクと呼ばれる手法を用いると、各ノードに単一のリンクフィールドを使用して二重リンクリストを実装できます。ただし、この手法はアドレスに対するビット演算機能を必要とするため、一部の高級言語では利用できない場合があります。
多くの最新のオペレーティングシステムは、アクティブなプロセス、スレッド、その他の動的オブジェクトへの参照を維持するために二重リンクリストを使用します。[ 4 ]ルートキットが検出を回避するための一般的な戦略は、これらのリストから自分自身をリンク解除することです。[ 5 ]
「多重リンクリスト」では、各ノードに2つ以上のリンクフィールドが含まれており、各フィールドは、異なる順序(名前順、部署順、生年月日順など)で並べられた同じデータセットを接続するために使用されます。二重リンクリストは多重リンクリストの特殊なケースと見なすことができますが、2つ以上の順序が互いに逆であるため、よりシンプルで効率的なアルゴリズムが実現できるため、通常は別のケースとして扱われます。
連結リストの最後のノードでは、リンクフィールドには多くの場合、それ以上のノードがないことを示す特殊な値であるヌル参照が含まれます。あまり一般的ではない慣習として、リンクフィールドをリストの最初のノードを指すようにすることもあります。その場合、リストは「循環的」または「循環連結」と呼ばれ、そうでない場合は「オープン」または「線形」と呼ばれます。これは、最後のノードポインタが最初のノードを指しているリストです(つまり、最後のノードの「次のノードへのリンク」ポインタは、最初のノードのメモリ アドレスを持っています)。

循環二重リンクリストの場合、最初のノードはリストの最後のノードも指しています。
実装によっては、最初のデータレコードの前または最後のデータレコードの後に、追加の「番兵」または「ダミー」ノードが追加される場合があります。この慣例により、すべてのリンクを安全に逆参照できること、およびすべてのリスト(データ要素を含まないリストであっても)に必ず「最初」ノードと「最後」ノードが存在することが保証されるため、一部のリスト処理アルゴリズムが簡素化され、高速化されます。
空のリストとは、データレコードを一切含まないリストのことです。これは通常、ノードがゼロ個であることと同じです。番兵ノードが使用されている場合、リストに番兵ノードのみが含まれている場合、そのリストは空であるとみなされます。
リンクフィールドは、ノードの物理的な一部である必要はありません。データレコードが配列に格納され、インデックスによって参照される場合、リンクフィールドはデータレコードと同じインデックスを持つ別の配列に格納できます。
最初のノードへの参照によってリスト全体にアクセスできるため、その参照はリストの「アドレス」、「ポインタ」、または「ハンドル」と呼ばれることがよくあります。連結リストを操作するアルゴリズムは通常、入力リストへのハンドルを取得し、結果リストへのハンドルを返します。実際、このようなアルゴリズムの文脈では、「リスト」という言葉はしばしば「リストハンドル」を意味します。ただし、場合によっては、リストの最初と最後のノードを指す2つのリンクで構成されるハンドルでリストを参照する方が便利な場合もあります。
上記に挙げた選択肢は、ほぼあらゆる方法で任意に組み合わせることができるため、番兵のない循環二重リンクリスト、番兵のある循環単方向リンクリストなどを作成できます。
コンピュータプログラミングや設計におけるほとんどの選択肢と同様に、あらゆる状況に最適な方法は存在しません。リンクリストデータ構造は、あるケースではうまく機能するかもしれませんが、別のケースでは問題を引き起こす可能性があります。以下は、リンクリスト構造に関連する一般的なトレードオフのリストです。
動的配列は、すべての要素をメモリ上に連続して割り当て、現在の要素数を保持するデータ構造です。動的配列用に確保された領域を超えると、再割り当てと(場合によっては)コピーが行われますが、これはコストのかかる操作です。
リンクリストは、動的配列に比べていくつかの利点があります。リストの特定の位置への要素の挿入または削除は、ポインタが既にノード(削除対象のノードの前、または挿入位置の前)を指している場合、定数時間で実行できます(この参照がない場合はO(n)になります)。一方、動的配列にランダムな位置に要素を挿入する場合、平均して要素の半分、最悪の場合はすべての要素を移動する必要があります。要素を何らかの方法で「空き」としてマークすることで、配列から要素を定数時間で「削除」することはできますが、これにより断片化が発生し、反復処理のパフォーマンスが低下します。
さらに、連結リストには使用可能なメモリの総量によってのみ制限されるものの、任意の数の要素を挿入できます。一方、動的配列は最終的に基となる配列データ構造がいっぱいになり、再割り当てが必要になります。これはコストのかかる操作であり、メモリが断片化している場合は不可能な場合もあります。ただし、再割り当てのコストは挿入ごとに平均化でき、再割り当てによる挿入のコストは依然として償却O(1)になります。これは配列の末尾に要素を追加する際には役立ちますが、データの連続性を維持するためにデータが移動するため、中間位置への挿入(または中間位置からの削除)には依然として非常に高いコストがかかります。多くの要素が削除された配列は、スペースの無駄遣いを避けるためにサイズ変更が必要になる場合もあります。
一方、動的配列(および固定サイズの配列データ構造)は定数時間でランダムアクセスが可能ですが、連結リストは要素へのシーケンシャルアクセスしかできません。実際、単方向連結リストは一方向にしか簡単に走査できません。そのため、ヒープソートのようにインデックスで要素を素早く検索することが有用なアプリケーションには、連結リストは適していません。また、配列や動的配列は参照の局所性が最適であるため、データキャッシュを有効活用でき、多くのマシンでシーケンシャルアクセスが連結リストよりも高速です。
リンクリストのもう 1 つの欠点は、参照に必要な追加のストレージです。リンクのストレージのオーバーヘッドがデータのサイズの 2 倍以上になる可能性があるため、文字やブール値などの小さなデータ項目のリストには実用的ではないことがよくあります。対照的に、動的配列はデータ自体 (およびごく少量の制御データ) のスペースのみを必要とします。[注 1 ]また、新しい要素ごとに個別にメモリを割り当てると遅くなり、単純なアロケータでは無駄になる可能性があり、この問題は一般的にメモリ プールを使用して解決されます。
ハイブリッドソリューションの中には、2つの表現形式の利点を組み合わせようとするものがあります。展開されたリンクリストは、各リストノードに複数の要素を格納することで、キャッシュのパフォーマンスを向上させつつ、参照のためのメモリオーバーヘッドを削減します。CDRコーディングも同様に、参照を、参照レコードの末尾を超えて拡張される実際の参照データに置き換えることで、これらの利点を両方実現します。
動的配列と連結リストの長所と短所を際立たせる良い例として、ヨセフス問題を解決するプログラムを実装してみましょう。ヨセフス問題は、円になって立つ人々のグループによって行われる選挙方法です。あらかじめ決められた人物から始めて、円をn回数えます。n番目の人物に到達したら、その人物を円から外し、円を閉じます。このプロセスは、1人だけが残るまで繰り返されます。残った人が選挙に勝ちます。これは、連結リストと動的配列の長所と短所を示しています。人々を円形連結リスト内の接続されたノードと見なすと、連結リストがノードをいかに簡単に削除できるかがわかります(異なるノードへのリンクを再配置するだけで済むため)。しかし、連結リストは次に削除する人物を見つけるのが苦手で、その人物が見つかるまでリストを検索する必要があります。一方、動的配列は、ノード(または要素)の削除が苦手です。すべての要素を個別に1つずつ上に移動させなければ、1つのノードを削除できないためです。しかし、配列内の位置を直接参照することで、円の中のn番目の人物を見つけるのは非常に簡単です。
リストランキング問題とは、連結リスト表現を効率的に配列に変換する問題である。従来のコンピュータにとっては容易な問題だが、並列アルゴリズムを用いてこの問題を解決するのは複雑であり、多くの研究の対象となってきた。
バランスのとれた木は、リンクリストと同様のメモリアクセスパターンとスペースオーバーヘッドを持ちながら、インデックス付けがはるかに効率的で、ランダムアクセスにO(n)ではなくO(log n)の時間しかかかりません。ただし、バランスを維持するための木の操作オーバーヘッドのため、挿入と削除の操作はよりコストがかかります。木が自動的にバランス状態を維持する仕組みも存在します。AVL木や赤黒木などがその例です。
二重リンクリストと循環リストは単方向リンク線形リストに比べて利点があるが、線形リストにもいくつかの利点があり、状況によってはそちらの方が好ましい場合もある。
単方向連結線形リストは、同じ型のより小さなオブジェクトへのポインタを含むため、再帰的なデータ構造です。そのため、単方向連結線形リストに対する多くの操作(2つのリストのマージや要素の逆順列挙など)は、反復コマンドを使用するソリューションよりもはるかに単純な再帰アルゴリズムで実現できます。これらの再帰的なソリューションは、双方向連結リストや循環連結リストにも適用できますが、一般的に追加の引数とより複雑な基本ケースが必要になります。
線形単方向連結リストでは、サブリストの共通の末尾部分を2つの異なるリストの末尾部分として使用できる末尾共有も可能です。具体的には、リストの先頭に新しいノードが追加された場合、以前のリストは新しいリストの末尾として引き続き使用できます。これは、永続的なデータ構造の簡単な例です。繰り返しますが、これは他のバリアントでは当てはまりません。ノードが2つの異なる循環リストや双方向連結リストに属することは決してありません。
特に、末尾の番兵ノードは、単方向リンクの非循環リスト間で共有できます。同じ末尾の番兵ノードを、そのようなすべてのリストに使用できます。たとえば、 Lispnilでは、すべての適切なリストは、またはで表される特別なノードへのリンクで終わります()。
高度な手法の利点は、多くの場合、アルゴリズムの複雑さに限られ、効率性には関係しません。特に循環リストは、通常、最初のノードと最後のノードを指す2つの変数を持つ線形リストで、追加コストなしでエミュレートできます。
二重リンクリストは、ノードごとに多くのスペースを必要とし(XORリンクを使用しない限り)、基本操作のコストも高くなります。しかし、双方向でリストに高速かつ容易に順次アクセスできるため、操作が容易な場合が多いです。二重リンクリストでは、ノードのアドレスさえあれば、一定回数の操作でノードを挿入または削除できます。単方向リンクリストで同じ操作を行うには、そのノードへのポインタのアドレスが必要です。これは、最初のノードの場合はリスト全体のハンドル、前のノードの場合は前のノードのリンクフィールドのいずれかです。一部のアルゴリズムでは、双方向のアクセスが必要です。一方、二重リンクリストは末尾共有を許可せず、永続的なデータ構造として使用することはできません。
循環連結リストは、多角形の頂点、 FIFO(先入れ先出し)順で使用および解放されるバッファプール、ラウンドロビン方式でタイムシェアリングされるべきプロセス群など、本質的に循環的な配列を表現するための自然な選択肢となり得ます。これらのアプリケーションでは、任意のノードへのポインタがリスト全体へのハンドルとして機能します。
循環リストでは、最後のノードへのポインターから、1つのリンクをたどるだけで最初のノードにも簡単にアクセスできます。そのため、リストの両端へのアクセスが必要なアプリケーション(例えば、キューの実装など)では、循環構造を用いることで、2つのポインターではなく、1つのポインターで構造を操作できます。
循環リストは、各部分の最後のノードのアドレスを指定することで、定数時間で2つの循環リストに分割できます。この操作は、2つのノードのリンクフィールドの内容を交換することによって行われます。2つの異なるリストの任意の2つのノードに同じ操作を適用すると、2つのリストが1つに結合されます。この特性により、クワッドエッジやフェイスエッジなどのアルゴリズムやデータ構造が大幅に簡素化されます。
空の循環リスト(そのような概念が意味を成す場合)を表す最も単純な方法は、リストにノードがないことを示すヌルポインタを用いることです。この方法を用いない場合、多くのアルゴリズムはこの特殊なケースをテストし、個別に処理する必要があります。一方、空の線形リストを表すのにヌルを用いる方がより自然であり、特殊なケースも少なくて済みます。
アプリケーションによっては、単方向連結リストが有用な場合があります。単方向連結リストは、循環型と線形型、あるいは初期セグメントが線形である循環型など、様々な形状をとることができます。このようなリストを検索したり操作したりするアルゴリズムでは、意図せず無限ループに陥らないように注意する必要があります。よく知られている方法の一つは、リストを半分の速度または2倍の速度で走査する2つ目のポインタを用意し、両方のポインタが同じノードに到達した場合に循環が見つかったと判断するというものです。
番兵ノードは、すべての要素に対して次または前のノードが存在すること、および空のリストにも少なくとも1つのノードが存在することを保証することで、特定のリスト操作を簡素化できます。また、リストの末尾に適切なデータフィールドを持つ番兵ノードを使用することで、リストの末尾判定の一部を省略することもできます。たとえば、リストをスキャンして特定の値xを持つノードを探す場合、番兵のデータフィールドをxに設定することで、ループ内でリストの末尾判定を行う必要がなくなります。別の例として、2つのソート済みリストをマージする場合、番兵のデータフィールドが +∞ に設定されていれば、次の出力ノードの選択において空のリストに対する特別な処理は不要になります。
しかし、番兵ノードは余分なスペースを消費し(特に多くの短いリストを使用するアプリケーションの場合)、他の操作(新しい空のリストの作成など)を複雑にする可能性があります。
しかし、循環リストを単に線形リストのシミュレートするために使用する場合は、各リストの最後のデータノードと最初のデータノードの間に単一の番兵ノードを追加することで、この複雑さの一部を回避できます。この規則では、空のリストは番兵ノードのみで構成され、次のノードへのリンクを介して自身を指します。リストハンドルは、リストが空でない場合は番兵ノードの前の最後のデータノードへのポインタ、リストが空の場合は番兵ノード自体へのポインタになります。
同じ手法は、二重リンク線形リストを単一の番兵ノードを持つ循環二重リンクリストに変換することで、その処理を簡素化するためにも使用できます。ただし、この場合、ハンドルはダミーノード自体への単一のポインタである必要があります。[ 8 ]
リンクリストをその場で操作する場合、以前の代入で無効化された値を使用しないように注意する必要があります。そのため、リンクリストのノードを挿入または削除するアルゴリズムはやや複雑になります。このセクションでは、単方向、双方向、および循環リンクリストからノードをその場で追加または削除するための擬似コードを示します。全体を通して、 nullはリストの末尾マーカーまたは番兵を表すために使用され、これはさまざまな方法で実装できます。
ノードデータ構造には2つのフィールドがあります。また、 firstNodeという変数があり、これは常にリストの最初のノードを指し、空のリストの場合はnullになります。
レコードノード { data; // ノードに格納されるデータNode next //次のノードへの参照[ 4 ] 、最後のノードの場合は null }レコードリスト { Node firstNode // リストの最初のノードを指す。空のリストの場合は null }単方向連結リストの走査は簡単で、最初のノードから始めて、末尾に到達するまで各リンクをたどっていくだけです。
node := list.firstNode nodeがnullでない 間(node.dataを使って何か処理を行う) node := node.next
以下のコードは、単方向連結リスト内の既存のノードの後に新しいノードを挿入します。図は動作原理を示しています。既存のノードの前に新しいノードを直接挿入することはできません。代わりに、前のノードの位置を記録しておき、その後に新しいノードを挿入する必要があります。

function insertAfter( Node node, Node newNode) // node の後に newNode を挿入する newNode.next := node.next node.next := newNode
リストの先頭に挿入するには、別の関数が必要です。これには、firstNode を更新する必要があります。
function insertBeginning( List list, Node newNode) // 現在の最初のノードの前にノードを挿入します newNode.next := list.firstNode list.firstNode := newNode
同様に、指定されたノードの次のノードを削除する関数と、リストの先頭からノードを削除する関数があります。図は前者を示しています。特定のノードを見つけて削除するには、やはり前の要素を追跡する必要があります。

function removeAfter( Node node) // このノードより後のノードを削除 obsoleteNode := node.next node.next := node.next.next 不要になったノードを削除します
function removeBeginning( List list) // 最初のノードを削除 obsoleteNode := list.firstNode list.firstNode := list.firstNode.next // 削除されたノードの先を指す destroy obsoleteNode
リストの最後のノードを削除すると、がにremoveBeginning()設定されることに注意してください。list.firstNodenull
逆方向に反復処理することができないため、効率的なinsertBefore操作removeBeforeは不可能です。特定のノードの前にリストに挿入するには、リストを走査する必要があり、最悪の場合の実行時間はO(n)になります。
連結リストを別の連結リストに追加する処理は、末尾への参照をリスト構造の一部として保持しない限り非効率になることがあります。これは、末尾を見つけるために最初のリスト全体を走査し、その後、2番目のリストをこの末尾に追加する必要があるためです。したがって、2つの線形連結リストの長さがそれぞれの場合、リストへの追加は漸近的に時間計算量がLisp系の言語では、リストへの追加はappend手続きによって提供されます。
連結リスト操作の多くの特殊ケースは、リストの先頭にダミー要素を含めることで排除できます。これにより、リストの先頭に特殊ケースがなくなり、と の両方insertBeginning()がremoveBeginning()不要になります。つまり、すべての要素またはノードは別のノードの隣に配置されます(最初のノードもダミーノードの隣に配置されます)。この場合、リスト内の最初の有用なデータは にあります。list.firstNode.next
循環連結リストでは、すべてのノードがヌルを使用せずに連続した円状に連結されます。先頭と末尾を持つリスト(キューなど)の場合、リストの最後のノードへの参照を格納します。最後のノードの次のノードが最初のノードになります。要素は定数時間でリストの末尾に追加したり、先頭から削除したりできます。
循環リンクリストは、単方向リンクまたは双方向リンクのいずれかになります。
どちらのタイプの循環連結リストも、任意のノードからリスト全体を走査できるという利点があります。これにより、 firstNodeとlastNode を格納する必要がなくなる場合が多くなりますが、リストが空になる可能性がある場合は、空のリストを表す特別な表現が必要になります。例えば、リスト内のノードを指す、または空の場合はnull となるlastNode変数などです。ここでは、そのようなlastNodeを使用しています。この表現により、空でないリストでのノードの追加と削除が大幅に簡素化されますが、空のリストは特別なケースとなります。
someNodeが空でない単方向循環リスト内のノードであると仮定すると、このコードはsomeNodeから始めてそのリストを反復処理します。
function iterate(someNode) if someNode ≠ null node := someNode する node.value を使って何かを行う node := node.next ノードがsomeNodeと異なる間
「 while node ≠ someNode」という条件はループの最後に記述する必要があることに注意してください。この条件をループの先頭に移動すると、リストにノードが1つしかない場合に処理が失敗します。
この関数は、指定されたノード「node」の後に、循環連結リストにノード「newNode」を挿入します。「node」がnullの場合は、リストが空であるとみなします。
function insertAfter( Node node, Node newNode) if node = null // リストが空であると仮定します newNode.next := newNode それ以外 newNode.next := node.next node.next := newNode必要に応じてlastNode変数 を更新する
「L」が循環連結リストの最後のノードを指す変数(リストが空の場合は null)であるとします。「newNode」をリストの末尾に追加するには、次のようにします。
insertAfter(L, newNode) L := newNode
リストの先頭に「newNode」を挿入するには、次のようにします。
insertAfter(L, newNode) if L = null L := newNode
この関数は、指定されたノード「node」の前に値「newVal」をO(1)の時間で挿入します。「node」と次のノードの間に新しいノードが作成され、その新しいノードに「node」の値が入れられ、さらに「newVal」が「node」に入れられます。このように、firstNode変数のみを持つ単方向連結循環リストは、先頭と末尾の両方にO(1)の時間で挿入できます。
function insertBefore( Node node, newVal) if node = null // リストが空であると仮定します newNode := new Node(data:=newVal, next:=newNode) else newNode := new Node(data:=node.data, next:=node.next) node.data := newVal node.next := newNode必要に応じてfirstNode変数 を更新してください。
この関数は、サイズが1より大きいリストから、null以外のノードをO(1)の時間で削除します。次のノードからデータをコピーして削除したノードに挿入し、次のノードをスキップするようにノードのnextポインタを設定します。
function remove( Node node) if node ≠ null and size of list > 1 removedData := node.data node.data := node.next.data node.next = node.next.next 削除されたデータを返す
参照をサポートしない言語でも、ポインタを配列インデックスに置き換えることでリンクを作成できます。この方法は、レコードの配列を保持し、各レコードに配列内の次の(場合によっては前の)ノードのインデックスを示す整数フィールドを持たせるというものです。配列内のすべてのノードを使用する必要はありません。レコードもサポートされていない場合は、代わりに並列配列を使用できる場合が多くあります。
例として、ポインタの代わりに配列を使用した以下のリンクリストレコードを考えてみましょう。
record Entry { integer next; // 配列内の次のエントリのインデックスinteger prev; // 前のエントリ (二重リンクの場合) string name; real balance; }これらの構造体の配列を作成し、最初の要素のインデックスを格納する整数変数を用意することで、連結リストを構築できます。
整数リスト ヘッダー エントリレコード[1000]
要素間のリンクは、次の(または前の)セルの配列インデックスを、特定の要素内の「次へ」または「前へ」フィールドに配置することによって形成されます。例:
上記の例では、ListHeadはリストの最初のエントリの位置である 2 に設定されます。エントリ 3 と 5 ~ 7 はリストに含まれていないことに注意してください。これらのセルは、リストへの追加に使用できます。ListFree整数型の変数を作成することで、使用可能なセルを追跡するためのフリーリストを作成できます。すべてのエントリが使用されている場合は、新しいエントリをリストに格納する前に、配列のサイズを増やすか、一部の要素を削除する必要があります。
以下のコードはリストを走査し、名前と口座残高を表示します。
i := listHead while i ≥ 0 // リストをループ処理 print i, Records[i].name, Records[i].balance // エントリを出力 i := Records[i].next
選択を迫られた場合、このアプローチの利点は以下のとおりです。
しかし、このアプローチには大きな欠点が1つあります。それは、ノードごとに専用のメモリ空間を作成・管理する必要があることです。これにより、以下のような問題が発生します。
こうした理由から、この手法は主に動的メモリ割り当てをサポートしていない言語で使用されます。また、配列作成時にリストの最大サイズが既知であれば、これらの欠点は軽減されます。
LispやSchemeなど、多くのプログラミング言語には単方向連結リストが組み込まれています。多くの関数型言語では、これらのリストはノードから構築され、各ノードはconsまたはconsセルと呼ばれます。consには2つのフィールドがあります。carは、そのノードのデータへの参照であり、cdrは、次のノードへの参照です。consセルは他のデータ構造を構築するためにも使用できますが、これが主な用途です。
抽象データ型やテンプレートをサポートする言語では、リンクリストを構築するためのリンクリストADTやテンプレートが利用可能です。それ以外の言語では、リンクリストは通常、参照とレコードを組み合わせて構築されます。
連結リストを作成する際、リストのデータを連結リストのノードに直接格納する(内部ストレージと呼ばれる)か、データへの参照のみを格納する(外部ストレージと呼ばれる)かの選択を迫られます。内部ストレージには、データへのアクセス効率の向上、全体的なストレージ容量の削減、参照の局所性の向上、リストのメモリ管理の簡素化(リストのデータはリストノードと同時に割り当ておよび解放される)といった利点があります。
一方、外部ストレージは、データのサイズに関係なく同じデータ構造とマシンコードをリンクリストに使用できるという点で、汎用性が高いという利点があります。また、同じデータを複数のリンクリストに簡単に配置できます。内部ストレージの場合、ノードデータ構造に複数の次の参照を含めることで同じデータを複数のリストに配置できますが、その場合は各フィールドに基づいてセルを追加または削除するための個別のルーチンを作成する必要があります。外部ストレージを使用することで、内部ストレージを使用する要素の追加のリンクリストを作成し、追加のリンクリストのセルに、データを含むリンクリストのノードへの参照を格納することが可能です。
一般的に、複数のデータ構造を連結リストに含める必要がある場合は、外部ストレージを使用するのが最善策です。複数のデータ構造を1つの連結リストにのみ含める必要がある場合は、外部ストレージを使用する汎用的な連結リストパッケージが利用可能でない限り、内部ストレージの方が若干優れています。同様に、同じデータ構造に格納できる異なるデータセットを1つの連結リストに含める場合は、内部ストレージで問題ありません。
一部の言語で使用できる別のアプローチとしては、異なるデータ構造を用意するものの、すべての構造に、次の参照(二重リンクリストの場合は前の参照も含む)などの初期フィールドを同じ場所に配置する方法があります。各データタイプごとに個別の構造を定義した後、他のすべての構造で共有される最小限のデータを含む汎用構造を定義し、それを構造の先頭に配置します。次に、最小限の構造を使用してリンクリスト型の操作を実行する汎用ルーチンを作成しますが、個別のルーチンで特定のデータを処理します。このアプローチは、複数の種類のメッセージを受信するものの、すべて同じフィールドセット(通常はメッセージタイプを示すフィールドを含む)で始まるメッセージ解析ルーチンでよく使用されます。汎用ルーチンは、新しいメッセージが受信されたときにキューに追加し、メッセージを処理するためにキューから削除するために使用されます。その後、メッセージタイプフィールドを使用して、特定の種類のメッセージを処理するための適切なルーチンを呼び出します。
内部ストレージを使用して、家族とそのメンバーのリンクリストを作成する場合、構造は次のようになります。
レコードメンバー{ // 家族のメンバーnext ; string firstName; integer age; } record family { // 家族自体family next; string lastName; string address; member members // この家族のメンバーリストの先頭 }内部ストレージを使用して家族とそのメンバーの完全なリストを印刷するには、次のように入力します。
aFamily := Families // 家族リストの先頭から開始while aFamily ≠ null // 家族リストをループ処理 家族に関する情報を印刷する aMember := aFamily.members // この家族のメンバーのリストの先頭を取得while aMember ≠ null // メンバーのリストをループ処理 会員に関する情報を印刷する aMember := aMember.next aFamily := aFamily.next
外部ストレージを使用すると、以下の構造を作成できます。
レコードノード{ // 一般的なリンク構造ノードネスト; ポインタデータ// ノード内のデータへの一般的なポインタ } レコードメンバー{ // 家族メンバーの構造string firstName; integer age } record family { // 家族の構造string lastName; string address; node members // この家族のメンバーリストの先頭 }外部ストレージを使用して家族とそのメンバーの完全なリストを印刷するには、次のように入力します。
famNode := Families // 家族リストの先頭から開始while famNode ≠ null // 家族リストをループ aFamily := (family) famNode.data // ノードから家族を抽出 家族に関する情報を印刷する memNode := aFamily.members // 家族メンバーのリストを取得while memNode ≠ null // メンバーのリストをループ処理 aMember := (member)memNode.data // ノードからメンバーを抽出 会員に関する情報を印刷する memNode := memNode.next famNode := famNode.next
外部ストレージを使用する場合、ノードからレコードを抽出して適切なデータ型にキャストするために、追加の手順が必要になることに注意してください。これは、ファミリーのリストとファミリー内のメンバーのリストの両方が、同じデータ構造(ノード)を使用する2つのリンクリストに格納されており、この言語にはパラメトリック型がないためです。
メンバーが所属できるファミリーの数がコンパイル時に分かっている限り、内部ストレージで問題ありません。しかし、メンバーを任意の数のファミリーに含める必要があり、その具体的な数が実行時にしか分からない場合は、外部ストレージが必要になります。
連結リスト内の特定の要素を見つけるには、たとえソート済みであっても、通常はO( n )の時間(線形探索)が必要です。これは、連結リストが他のデータ構造に比べて抱える主な欠点の1つです。上記で説明したバリエーションに加えて、検索時間を改善する2つの簡単な方法を以下に示します。
順序付けされていないリストにおいて、平均検索時間を短縮するためのシンプルなヒューリスティックの一つに、要素が見つかったらリストの先頭に移動させる「先頭移動」というヒューリスティックがあります。この方法は、シンプルなキャッシュを作成する際に便利で、最近使用した項目を最も早く見つけられるようにします。
もう一つの一般的なアプローチは、より効率的な外部データ構造を使用してリンクリストに「インデックス」を付けることです。例えば、リンクリストのノードへの参照を要素とする赤黒木やハッシュテーブルを構築できます。このようなインデックスは、1つのリスト上に複数作成できます。欠点は、ノードが追加または削除されるたび(あるいは少なくとも、そのインデックスが再び使用される前)に、これらのインデックスを更新する必要がある場合があることです。
ランダムアクセスリストは、リスト内の任意の要素を読み取ったり変更したりするための高速ランダムアクセスをサポートするリストです。[ 9 ]可能な実装の 1 つは、特殊な特性を持つツリーのリストを含む、スキューバイナリ数システムを使用したスキューバイナリランダムアクセスリストです。これにより、最悪の場合定数時間のヘッド/コンス操作と、インデックスによる要素への最悪の場合対数時間のランダムアクセスが可能になります。[ 9 ]ランダムアクセスリストは、永続データ構造として実装できます。[ 9 ]
ランダムアクセスリストは、同じO(1)の先頭操作と末尾操作をサポートするという点で、不変リンクリストと見なすことができる。[ 9 ]
ランダムアクセスリストの簡単な拡張として、min-listがあります。これは、リスト全体の最小要素を定数時間で取得する追加の操作を提供します (変更の複雑さはありません)。[ 9 ]
スタックとキューはどちらもリンクリストを用いて実装されることが多く、サポートされる操作の種類が制限されるだけである。
スキップリストは、多数の要素を素早くスキップして次の層へ移動するために、ポインタの階層を追加した連結リストです。このプロセスは最下層まで続き、それが実際のリストとなります。
二分木は、要素自体が同じ性質の連結リストである一種の連結リストと見なすことができます。その結果、各ノードは、1つまたは2つの他の連結リストの最初のノードへの参照を含むことができ、それらの連結リストとその内容が、そのノードの下にあるサブツリーを形成します。
展開されたリンクリストとは、各ノードにデータ値の配列が含まれるリンクリストのことです。これにより、より多くのリスト要素がメモリ上で連続して配置されるため、キャッシュのパフォーマンスが向上し、リストの各要素に格納する必要のあるメタデータが少なくなるため、メモリのオーバーヘッドが削減されます。
ハッシュテーブルは、ハッシュテーブル内の同じ位置にハッシュされる項目の連鎖を格納するために、リンクリストを使用する場合があります。
ヒープは連結リストと順序付けの特性の一部を共有していますが、ほとんどの場合、配列を使用して実装されます。ノード間の参照の代わりに、次のデータと前のデータのインデックスは、現在のデータのインデックスを使用して計算されます。
自己組織化リストは、何らかのヒューリスティックに基づいてノードを再配置し、頻繁にアクセスされるノードをリストの先頭に配置することで、データ取得の検索時間を短縮します。
{{cite book}}:|work=無視されました (ヘルプ)