データ構造内の単一リンクリスト

⚡ スマートサマリー

単方向連結リストは、線形かつ一方向のデータ構造であり、各ノードはデータと次のノードへの単一のポインタを格納します。そのため、走査は先頭から末尾にのみ進み、新しいノードが追加されるにつれてメモリが動的に割り当てられます。

  • 🧩 ノード構造: 各ノードは1つのデータフィールドと1つのデータフィールドを保持します。 次の 次のノードへのポインタ。末尾ノードの 次の ポインタがNULLです。
  • 📦 リストと配列: 要素数が不明な場合、ランダムアクセスが不要な場合、およびリストの途中への要素挿入が頻繁に行われる場合は、単方向連結リストが推奨されます。
  • 挿入物: ノードは、先頭、末尾、一致したノードの後、または次のポインタ書き換えを使用して一致したノードの前に追加できます。
  • 削除: 先頭ノード、末尾ノード、または検索済みノードを削除すると、隣接ノードへのポインタが更新され、解放されたメモリが解放されてメモリリークが防止されます。
  • 🔁 トラバーサル: 前の要素へのポインタが存在しないため、順方向の走査のみがサポートされており、単方向連結リストの逆方向の走査は不可能です。
  • 💻 C++ (NAIST) と Python Code: 完全な実装例では、挿入、削除、検索、走査ルーチンと実行可能な出力が示されています。
  • 📊 複雑: ヘッドの挿入または削除はO(1)であり、検索およびその他の挿入と削除はO(n)であり、空間計算量はO(n)である。

単方向リスト

単一リンクリストとは何ですか?

単方向連結リストは、データがノードに保存され、各ノードがリンクを介して次のノードに接続される線形かつ単方向のデータ構造です。各ノードにはデータフィールドと次のノードへのリンクが含まれています。単方向連結リストは一方向にしか走査できませんが、 二重リンクリスト 両方向への通行が可能です。

単方向連結リストのノード構造は以下のとおりです。

リンクリスト内のノードの構造

リンクリスト内のノードの構造

配列ではなくリンクリストを使う理由とは?

いくつかのシナリオでは、リンクリストが 配列:

  • 不明な要素数: コンパイル時に必要な要素数が不明な場合、リンクリストは要素が追加されるにつれて動的にメモリを割り当てます。
  • ランダムアクセス: ランダムなインデックスアクセスが必要ない場合は、リンクリストが適切な選択肢となります。
  • 途中で挿入: 配列の途中に要素を挿入するには、要素を移動させる必要があります。一方、リンクリストでは、わずかなポインタを書き換えるだけで、任意の位置に要素を挿入できます。

Opera単一リンクリストのオプション

単方向連結リストは、メモリを動的に割り当てるのに適しています。また、挿入、削除、検索、更新、2つのリストのマージ、走査など、連結リストの標準的な操作をサポートしています。

この記事では、以下の操作について説明します。

  • 頭から挿入
  • 末尾に挿入
  • ノードの後に​​挿入する
  • ノードの前に挿入する
  • ヘッドノードを削除する
  • 末尾ノードを削除する
  • ノードの検索と削除
  • リンクされたリストの走査

以下は、4つのノードを持つ連結リストの例です。

単一リンクリストの例

単一リンクリストの例

単方向連結リストの先頭への挿入

これは簡単な操作です。一般的には、単方向連結リストへの要素追加として知られています。新しいノードが作成され、リストの先頭に配置されます。

この操作を実行するには、次の2つの重要な条件を満たす必要があります。

  1. リストが空の場合、新しく作成されたノードがヘッドノードになり、その 次の ポインタがNULLです。
  2. リストが空でない場合、新しいノードがヘッドノードになり、その 次の ポインタは前の先頭ノードを指しています。

以下は、連結リストの先頭にノードを挿入するための擬似コードです。

function insertAtHead(head, value):
  newNode = Node(value)
  if head is NULL:
    head = newNode
    return head
  else:
    newNode.next = head
    return newNode

先頭に挿入する

先頭に挿入

単方向連結リストの末尾への挿入

連結リストの末尾にノードを挿入するのは、先頭に挿入するのと似ています。末尾ノードまでたどり、次にそのノードを に向けます。 次の 新しいノードへのポインタ。headがNULLの場合、新しいノードがheadになります。

ステップ1) 横断して 次の 現在のノードのポインタがNULLになります。

ステップ2) 指定された値で新しいノードを作成します。

ステップ3) 新しいノードを末尾ノードの次のノードとして割り当てます。

単一リストの末尾に挿入するための擬似コード:

function insertAtEnd(head, value):
  newNode = Node(value)
  if head is NULL:
    head = newNode
    return head
  while head.next is not NULL:
    head = head.next
  head.next = newNode
  newNode.next = NULL

尾部に挿入する

尾部に挿入

単方向連結リストのノードの後に​​挿入する

ノードの後に​​挿入する処理は、2つの段階から成ります。まず、目的のノードを検索し、次にそのノードの後に​​新しいノードを追加します。リストを走査して一致するノードが見つかったら、新しいノードを挿入します。

ステップ1) 現在のノードの値が検索項目と一致するまで走査する。

ステップ2) 新しいノードを設定します 次の 現在のノードへのポインタ 次の ポインター。

ステップ3) 現在のノードを指す 次の 新しいノードへのポインタ。

疑似コード:

function insertAfter(head, value, searchItem):
  newNode = Node(value)
  while head.value != searchItem:
    head = head.next
  newNode.next = head.next
  head.next = newNode

単一リンクリスト内のノードの後に​​ノードを挿入する

単一リンクリスト内のノードの後に​​ノードを挿入する

単方向連結リストのノードの前に挿入する

これはノードの後に​​挿入するのと似ています。次のノードが検索値と一致するまで走査し、その後、新しいノードをその前に挿入します。

ステップ1) 次のノードの値が検索項目と等しくなるまでトラバースします。

ステップ2) 新しいノードを作成し、その設定を行います。 次の 現在のノードへのポインタ 次の.

ステップ3) 現在のノードを指す 次の 新しいノードへ。

function insertBefore(head, value, searchItem):
  newNode = Node(value)
  while head.next.value != searchItem:
    head = head.next
  newNode.next = head.next
  head.next = newNode

単一リンクリストのノードの前にノードを挿入する

単一リンクリストのノードの前にノードを挿入する

単方向連結リストの先頭要素を削除する

パラメータとしてヘッドポインタが渡されます。ヘッドノードが削除され、次のノードが新しいヘッドになります。メモリリークを防ぐため、削除されたノードのメモリは解放する必要があります。

ステップ1) 先頭の次のノードを新しい先頭として割り当てます。

ステップ2) 前のヘッドノードに割り当てられていたメモリを解放します。

ステップ3) 新しいヘッド ノードを返します。

function deleteHead(head):
  temp = head
  head = head.next
  free(temp)
  return head

リンクされたリストの先頭を削除する

リンクリストの先頭を削除する

単方向連結リストの末尾を削除する

末尾ノードの削除は、先頭ノードの削除と似ています。違いは、リストの末尾まで走査する必要があることです。単方向連結リストでは、末尾ノードは、 次の ポインタがNULLの場合は、末尾ノードです。

ステップ1) 末尾ノードの直前まで走査する。現在のノードを保存する。

ステップ2) 次のノード(末尾)のメモリを解放します。

ステップ3) 現在のノードの次のノードをNULLに設定します。

function deleteTail(head):
  while head.next.next is not NULL:
    head = head.next
  free(head.next)
  head.next = NULL

単一リンクリストの末尾の削除

単一リンクリストの末尾の削除

単方向連結リストからノードを検索して削除する

この関数は、検索と削除の 2 つのタスクを実行します。リストの末尾まで走査します。一致するノードが見つかった場合は、それを削除し、前のノードを再リンクします。 次の ポインター。

ステップ1) リストの最後まで走査します。現在のノードが検索ノードと一致するかどうかを確認します。

ステップ2) 一致するものが見つかった場合は、現在のノードへのポインタを保存します。

ステップ3) その 次の 前のノードの が、現在のノードの次のノードになります。

ステップ4) 現在のノードを削除し、そのメモリを解放します。

function searchAndDelete(head, searchItem):
  while head.next.next is not NULL and head.next.value != searchItem:
    head = head.next
  temp = head.next
  head.next = head.next.next
  free(temp)

単一リンクリストからノードを検索して削除する

単一リンクリストからノードを検索して削除する

単方向連結リストを走査する

単方向連結リストは、先頭から末尾への走査のみをサポートします。前のノードへのポインタがないため、逆方向の走査は不可能です。各ノードは順番に訪問され、NULLに到達するまでその値が出力されます。

ステップ1) NULLに到達するまで各ノードを走査する。

ステップ2) 現在のノードの値を出力します。

function traverse(head):
  while head is not NULL:
    print head.value
    head = head.next

単方向リンクリストの例 C++

#include<iostream>
using namespace std;
struct Node{
  int data;
  struct Node *next;
};
void insertAtHead(Node* &head, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  if(head != NULL){
    newNode->next = head;
  }
  head = newNode;
  cout<<"Added "<<newNode->data<<" at the front"<<endl;
}
void insertEnd(Node* &head, int value){
  if(head == NULL){
    insertAtHead(head, value);
    return;
  }
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *temp = head;
  while(temp->next != NULL){
    temp = temp->next;
  }
  temp->next = newNode;
  cout<<"Added "<<newNode->data<<" at the end"<<endl;
}
void searchAndDelete(Node **headPtr, int searchItem){
  Node *temp = NULL;
  if((*headPtr)->data == searchItem){
    temp = *headPtr;
    *headPtr = (*headPtr)->next;
    free(temp);
  } else {
    Node *currentNode = *headPtr;
    while(currentNode->next != NULL){
      if(currentNode->next->data == searchItem){
        temp = currentNode->next;
        currentNode->next = currentNode->next->next;
        free(temp);
        break;
      } else {
        currentNode = currentNode->next;
      }
    }
  }
  cout<<"Deleted Node\t"<<searchItem<<endl;
}
void insertAfter(Node* &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next != NULL && head->data != searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" after node\t"<<searchItem<<endl;
}
void insertBefore(Node* &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next != NULL && head->next->data != searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" before node\t"<<searchItem<<endl;
}
void traverse(Node *headPointer){
  Node* tempNode = headPointer;
  cout<<"Traversal from head:\t";
  while(tempNode != NULL){
    cout<<tempNode->data;
    if(tempNode->next)
      cout<<" --> ";
    tempNode = tempNode->next;
  }
  cout<<endl;
}
int main(){
  Node *head = NULL;
  insertAtHead(head, 5);
  insertAtHead(head, 6);
  insertAtHead(head, 7);
  insertEnd(head, 9);
  traverse(head);
  searchAndDelete(&head, 6);
  traverse(head);
  insertAfter(head, 7, 10);
  insertBefore(head, 9, 11);
  traverse(head);
}

出力

Added 5 at the front
Added 6 at the front
Added 7 at the front
Added 9 at the end
Traversal from head:    7 --> 6 --> 5 --> 9
Deleted Node    6
Traversal from head:    7 --> 5 --> 9
Inserted 10 after node  7
Inserted 11 before node 9
Traversal from head:    7 --> 10 --> 5 --> 11 --> 9

単方向リンクリストの例 Python

class Node:
  def __init__(self, data=None, next=None):
    self.data = data
    self.next = next
class SinglyLinkedList:
  def __init__(self):
    self.head = None
  def insertAtHead(self, value):
    newNode = Node(data=value)
    if self.head is not None:
      newNode.next = self.head
    self.head = newNode
    print(f'Added {newNode.data} at the front.')
  def insertAtEnd(self, value):
    if self.head is None:
      self.insertAtHead(value)
      return
    newNode = Node(value)
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    print(f'Added {newNode.data} at the end.')
  def searchAndDelete(self, searchItem):
    if self.head is None:
      return
    if self.head.data == searchItem:
      self.head = self.head.next
      print(f'Deleted node\t{searchItem}')
      return
    currentNode = self.head
    while currentNode.next is not None:
      if currentNode.next.data == searchItem:
        currentNode.next = currentNode.next.next
        print(f'Deleted node\t{searchItem}')
        return
      currentNode = currentNode.next
  def insertAfter(self, searchItem, value):
    newNode = Node(data=value)
    temp = self.head
    while temp.next is not None and temp.data != searchItem:
      temp = temp.next
    newNode.next = temp.next
    temp.next = newNode
    print(f'Inserted {value} after node\t{searchItem}')
  def insertBefore(self, searchItem, value):
    newNode = Node(data=value)
    temp = self.head
    while temp.next is not None and temp.next.data != searchItem:
      temp = temp.next
    newNode.next = temp.next
    temp.next = newNode
    print(f'Inserted {value} before node\t{searchItem}')
  def traverse(self):
    temp = self.head
    print("Traversing from head:\t", end="")
    while temp:
      print("{}\t".format(temp.data), end="")
      temp = temp.next
    print()
singlyLinkedList = SinglyLinkedList()
singlyLinkedList.insertAtHead(5)
singlyLinkedList.insertAtHead(6)
singlyLinkedList.insertAtHead(7)
singlyLinkedList.insertAtEnd(9)
singlyLinkedList.traverse()
singlyLinkedList.searchAndDelete(6)
singlyLinkedList.traverse()
singlyLinkedList.insertAfter(7, 10)
singlyLinkedList.insertBefore(9, 11)
singlyLinkedList.traverse()

出力

Added 5 at the front.
Added 6 at the front.
Added 7 at the front.
Added 9 at the end.
Traversing from head:   7       6       5       9
Deleted node    6
Traversing from head:   7       5       9
Inserted 10 after node  7
Inserted 11 before node 9
Traversing from head:   7       10      5       11      9

単連結リストの複雑さ

複雑さには、時間計算量と空間計算量の2種類があります。単方向連結リストの場合、最悪の場合と平均的な場合の時間計算量は同じです。

最良の場合の時間計算量:

  • 先頭への挿入はO(1)で実行できます。リスト内部の走査は不要です。
  • 対象要素が先頭ノードにある場合、検索と削除はO(1)で実行できます。

平均的な時間計算量:

  • 連結リスト内への挿入にはO(n)かかる。 n は要素の総数です。
  • 検索と削除もO(n)の計算量になる可能性がある。なぜなら、対象となる要素は末尾ノードまでのどこにでも存在する可能性があるからだ。

単方向連結リストの空間計算量

単方向連結リストは動的にメモリを割り当てます。 n 要素を割り当てる n メモリユニット。したがって、空間計算量はO(n)です。

単方向連結リストの応用例

単方向連結リストは、前方のみの走査と動的メモリが有用な多くの場面で使用されます。

  • スタックとキュー: ノードから構築されるLIFOスタックおよびFIFOキューの基盤となるストレージ。
  • ハッシュテーブルの連鎖: 衝突は、バケットごとにエントリを単方向連結リストに連結することで解決されます。
  • 隣接リスト: 疎グラフは、各頂点に対して隣接ノードの単方向連結リストを使用します。
  • シンボルテーブル: コンパイラとインタプリタは、スコープごとに識別子を連結して単方向連結リストを作成します。
  • メモリ割り当て関数: 無料リスト割り当てツール track個の空きブロックを単方向連結リストとして格納します。

よくあるご質問

単方向連結リストは、AIフレームワーク内でトレーニングサンプル、ミニバッチ、および空きメモリブロックを連結し、ストリーミング入力用の動的なキューと、モデルの需要に応じて拡張可能なロックフリーのデータパイプラインを実現します。

はい。GitHub CopilotとGPTはC言語で完全な単方向連結リストを生成できます。 C++, Java, Pythonまたは Java挿入、削除、反転、サイクル検出、および単体テストを含むスクリプト。

単方向連結リストは、次の要素へのポインタを1つ持ち、前方のみを走査します。双方向連結リストは、次の要素へのポインタと前の要素へのポインタの両方を持ち、両方向に走査しますが、ノードあたりのメモリ使用量が多くなります。

一般的な用途としては、スタックやキューの実装、ハッシュテーブルの連鎖、疎グラフの隣接リスト、コンパイラのシンボルテーブル、フリーリストアロケータ、軽量エディタのアンドゥ履歴などが挙げられる。

先頭での挿入または削除はO(1)です。末尾での挿入、検索、特定の位置での挿入、および特定のノードの削除はすべて、先頭からの走査が必要となるため、O(n)のコストがかかります。

リンクリストは実行時にサイズが拡大縮小し、位置が分かれば挿入や削除はO(1)で実行でき、連続したメモリ領域を必要としません。配列はO(1)のランダムアクセスと優れたキャッシュ局所性を提供します。

prev、curr、nextという3つのポインタを使ってリストを走査します。各ステップで、curr.nextを保存し、curr.nextをprevに向け、prevとcurrを前方にシフトします。prevを新しい先頭として返します。

フロイドのウサギとカメのアルゴリズムは、異なる速度で移動する2つのポインタを使用します。2つのポインタが出会うと、リストにサイクルが存在します。そうでない場合、速い方のポインタがNULLに到達するため、サイクルは存在しません。