C++で連結リストを完全マスターする7つの実例付き解説

C++で連結リストを学ぶプログラマーの手引きのイメージC++
この記事は約16分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングでは、データ構造が非常に重要です。

C++を学ぶ上で、連結リストは基本的なデータ構造の一つであり、その理解はプログラマーにとって不可欠です。

この記事では、連結リストの概念を初心者でも理解できるように噛み砕いて説明し、段階的に応用へと進めていきます。

連結リストの基本から応用までを一つ一つ丁寧に解説することで、C++におけるデータ構造の理解を深めることができるでしょう。

●C++と連結リストの基礎

C++は、オブジェクト指向プログラミング言語の一つで、システムプログラミングやアプリケーション開発など幅広い分野で使用されています。

C++でプログラミングを行う際、データを効率的に管理し操作するためには、データ構造の知識が必要です。

連結リストは、データ構造の中でも基本的な部類に入り、データの追加や削除を柔軟に行えるのが特徴です。

○C++の基本的な概念

C++でのプログラミングには、変数、関数、クラスなどの基本的な概念があります。

これらの概念を理解し、適切に使用することが、効率的なプログラミングへの第一歩となります。

特に、クラスとオブジェクトの概念は、C++における連結リストを扱う上で中心的な役割を果たします。

○連結リストとは何か

連結リストは、一連のノード(要素)がポインタを介して連結されたデータ構造です。

各ノードはデータ部と次のノードを指すポインタ部から構成され、一方向または双方向に連結することができます。

C++において、連結リストは動的なデータ構造として非常に有用で、データの挿入や削除を効率的に行うことができます。

この特性は、静的な配列では難しい操作を可能にし、プログラミングの柔軟性を高める要因となっています。

●連結リストの構築方法

連結リストの構築は、C++プログラミングにおいて基本的なスキルの一つです。

連結リストは、ノードと呼ばれる要素がポインタを介して連結されており、各ノードはデータと次のノードへの参照(またはポインタ)を持ちます。

ここでは、連結リストを構築する基本的な方法を、実際のコード例を交えて詳細に解説します。

○サンプルコード1:基本的な連結リストの作成

まず、単純な形の連結リストを作成する方法から見ていきます。

下記のサンプルコードは、C++で単方向の連結リストを構築する基本的な方法を表しています。

#include <iostream>
using namespace std;

// ノードの構造を定義するクラス
class Node {
public:
    int data; // データを格納する変数
    Node* next; // 次のノードへのポインタ

    // コンストラクタ
    Node(int d) {
        data = d;
        next = nullptr;
    }
};

// 連結リストを操作するためのクラス
class LinkedList {
    Node* head; // リストの先頭ノードへのポインタ

public:
    LinkedList() {
        head = nullptr;
    }

    // リストの先頭にノードを追加する関数
    void addNode(int d) {
        Node* newNode = new Node(d);
        newNode->next = head;
        head = newNode;
    }

    // リストの内容を表示する関数
    void display() {
        Node* current = head;
        while (current != nullptr) {
            cout << current->data << " -> ";
            current = current->next;
        }
        cout << "null" << endl;
    }
};

int main() {
    LinkedList list;
    list.addNode(1);
    list.addNode(2);
    list.addNode(3);

    list.display(); // リストの内容を表示
    return 0;
}

このコードでは、Node クラスを用いてリストの各ノードを定義し、LinkedList クラスでリスト全体の操作を行います。

addNode 関数を使用してリストに新しいノードを追加し、display 関数でリストの内容を画面に表示します。

この例では、3つのノード(3, 2, 1)をリストに追加し、結果を表示しています。

○サンプルコード2:ノードの挿入

次に、リストの特定の位置にノードを挿入する方法を見ていきましょう。

下記のサンプルコードは、特定の位置にノードを挿入する機能をLinkedListクラスに追加する方法を表しています。

// リスト内の特定の位置にノードを挿入する関数
void insertNode(int pos, int d) {
    Node* newNode = new Node(d);
    if (pos == 0) {
        newNode->next = head;
        head = newNode;
        return;
    }
    Node* current = head;
    for (int i = 0; i < pos - 1 && current != nullptr; i++) {
        current = current->next;
    }
    if (current == nullptr) {
        cout << "位置がリストの範囲外です。" << endl;
        delete newNode;
        return;
    }
    newNode->next = current->next;
    current->next = newNode;
}

この関数では、指定された位置posに新しいノードを挿入します。

位置が0の場合はリストの先頭に挿入し、それ以外の場合は指定された位置に挿入します。

もし指定された位置がリストの範囲外である場合は、エラーメッセージを表示し、新しく作成したノードを削除します。

●連結リストの操作

連結リストの構築後、その操作はプログラミングにおける重要なステップです。

ここでは、連結リスト内のノードの削除やリストの走査といった基本的な操作について説明します。

これらの操作をマスターすることで、リストをより柔軟に扱うことが可能になります。

○サンプルコード3:ノードの削除

連結リストからノードを削除するには、削除したいノードを見つけて、その前後のノードを適切にリンクし直す必要があります。

下記のコードは、指定された値を持つノードをリストから削除する例です。

void deleteNode(int d) {
    Node* current = head;
    Node* previous = nullptr;

    // 削除するノードを見つける
    while (current != nullptr && current->data != d) {
        previous = current;
        current = current->next;
    }

    // ノードが見つからなかった場合
    if (current == nullptr) {
        cout << d << " を持つノードは存在しません。" << endl;
        return;
    }

    // 先頭のノードを削除する場合
    if (previous == nullptr) {
        head = current->next;
    } else {
        previous->next = current->next;
    }

    delete current;
}

この関数では、まず削除したいノードをリスト内で探し、見つけたらそのノードをリストから外します。

先頭のノードを削除する場合と、それ以外の場合で処理が異なります。

○サンプルコード4:リストの走査

連結リストを走査することは、リスト内のデータにアクセスし、操作する基本的な方法です。

リストを先頭から末尾まで順にたどり、各ノードのデータを表示することで、リストの構造や内容を理解するのに役立ちます。

void traverse() {
    Node* current = head;
    while (current != nullptr) {
        cout << current->data << " -> ";
        current = current->next;
    }
    cout << "null" << endl;
}

このtraverse関数は、リストの先頭から始まり、各ノードを順にたどり、そのデータを表示します。リストの最後に到達すると、ループは終了します。

●連結リストの応用例

連結リストはその柔軟性から多様な応用が可能です。

ここでは、C++での連結リストを用いたいくつかの応用例を見ていきましょう。

具体的には、逆順リストの作成、リストの並べ替え、重複ノードの削除について説明します。

これらの応用を理解することで、連結リストをさらに深く掘り下げることができます。

○サンプルコード5:逆順リストの作成

連結リストのノードを逆順に並べ替えることで、データの操作や分析の幅を広げることができます。

下記のサンプルコードは、連結リストを逆順に並べ替える方法を表しています。

void reverse() {
    Node* current = head;
    Node* previous = nullptr;
    Node* next = nullptr;

    while (current != nullptr) {
        next = current->next;
        current->next = previous;
        previous = current;
        current = next;
    }
    head = previous;
}

この関数では、現在のノード、前のノード、次のノードを使ってリストを逆順に再構築します。

リストの各ノードを順に進みながら、各ノードの次のノードへのポインタを前のノードに変更していきます。

○サンプルコード6:並べ替え

リスト内のノードを特定の基準(例えば数値の大きさ)に従って並べ替えることも、連結リストの重要な応用の一つです。

下記のサンプルコードは、数値を含む連結リストを昇順に並べ替える一例を表しています。

void sort() {
    if (head == nullptr || head->next == nullptr) {
        return; // 並べ替えが必要ない場合
    }

    Node *current, *index;
    int temp;

    for(current = head; current->next != nullptr; current = current->next) {
        for(index = current->next; index != nullptr; index = index->next) {
            if(current->data > index->data) {
                temp = current->data;
                current->data = index->data;
                index->data = temp;
            }
        }
    }
}

この関数では、二重ループを用いてリスト内の各ノードを比較し、必要に応じてノードの値を交換しています。

この方法は、単純ながら効果的にリストを並べ替えることができます。

○サンプルコード7:重複ノードの削除

連結リストから重複するノードを削除することで、データの整合性を保つことができます。

下記のサンプルコードは、重複するノードを削除する方法を表しています。

void removeDuplicates() {
    Node *current = head;
    Node *index, *temp;

    while (current != nullptr && current->next != nullptr) {
        index = current;

        while (index->next != nullptr) {
            if (current->data == index->next->data) {
                temp = index->next;
                index->next = index->next->next;
                delete temp;
            } else {
                index = index->next;
            }
        }
        current = current->next;
    }
}

この関数では、外側のループでリストを順に進みながら、内側のループで各ノードの重複を確認しています。

重複が見つかった場合、そのノードをリストから削除し、メモリを解放しています。

●注意点と対処法

C++で連結リストを使用する際には、いくつかの重要な注意点があります。

これらを理解し、適切に対処することが、効率的で安全なプログラミングを行う上で不可欠です。

特に、メモリ管理とポインタの使用には細心の注意を払う必要があります。

○メモリ管理の重要性

C++では、プログラマがメモリ管理を直接行う必要があります。

特に、動的に確保したメモリは、使用後に適切に解放することが重要です。

連結リストでは、ノードを動的に生成するため、メモリリークを避けるためには、不要になったノードを適切に削除し、確保したメモリを解放する必要があります。

例えば、ノードを削除する際には、delete 演算子を使用してメモリを解放することが重要です。

void deleteNode(int value) {
    Node* temp = head;
    Node* prev = nullptr;

    while (temp != nullptr && temp->data != value) {
        prev = temp;
        temp = temp->next;
    }

    if (temp == nullptr) return; // ノードが見つからない場合

    if (prev == nullptr) {
        head = temp->next;
    } else {
        prev->next = temp->next;
    }

    delete temp; // メモリ解放
}

この例では、指定した値を持つノードを見つけ、それをリストから取り除いた後に、そのノードが占めていたメモリを解放しています。

○ポインタの正しい使用

ポインタはC++において非常に強力なツールですが、誤った使用はプログラムのクラッシュや予期しない動作の原因となります。

連結リストを扱う際には、特にポインタの使用に注意する必要があります。

ポインタを使用する際には、常にポインタが有効なオブジェクトを指していることを確認し、野良ポインタ(どの有効なオブジェクトも指していないポインタ)の使用を避けることが重要です。

また、ポインタが指すオブジェクトのライフサイクルに注意し、メモリ解放後のポインタを参照しないようにする必要があります。

例えば、下記のような処理を行う際は、特に注意が必要です。

  • リスト内のノードを参照するためのポインタの使用
  • ノードの追加や削除の際のポインタの操作
  • リストの破棄時に全てのノードのメモリを正しく解放すること

これらのポインタ関連の注意点を遵守することで、メモリリークや不正アクセスを防ぎ、安全かつ効率的なプログラムを作成することができます。

●連結リストのカスタマイズ方法

連結リストは多様な用途に応じてカスタマイズすることが可能です。

C++では、テンプレート機能を使ってより汎用的なリストを作成したり、パフォーマンスの向上を図る方法があります。

ここでは、これらのカスタマイズ方法について掘り下げていきます。

○テンプレートを用いた汎用的なリスト

C++のテンプレートを用いることで、任意のデータ型で動作する汎用的な連結リストを作成することができます。

これにより、同じリスト構造を異なるデータ型に適用することが容易になります。

下記のサンプルコードは、テンプレートを用いた汎用的な連結リストの実装例です。

template <typename T>
class Node {
public:
    T data;
    Node* next;

    Node(T d) : data(d), next(nullptr) {}
};

template <typename T>
class LinkedList {
    Node<T>* head;

public:
    LinkedList() : head(nullptr) {}

    void addNode(T data) {
        Node<T>* newNode = new Node<T>(data);
        newNode->next = head;
        head = newNode;
    }

    // その他の関数...
};

このコードでは、NodeLinkedList クラスをテンプレートとして定義しています。

これにより、どのような型のデータも保持できるリストを作成できます。

例えば、int 型のデータを保持するリストや、ユーザー定義型を保持するリストなど、柔軟に対応可能です。

○パフォーマンスの向上

連結リストのパフォーマンスを向上させるためには、いくつかの最適化手法があります。

例えば、リストの走査効率を上げるために二重ポインタや双方向リストを使用することが考えられます。

また、ノードの挿入や削除の際に発生するコストを減らすために、テールポインタを使用してリストの最後に迅速にアクセスできるようにすることも有効です。

template <typename T>
class LinkedList {
    Node<T>* head;
    Node<T>* tail;

public:
    LinkedList() : head(nullptr), tail(nullptr) {}

    void addNode(T data) {
        Node<T>* newNode = new Node<T>(data);
        if (tail != nullptr) {
            tail->next = newNode;
        }
        tail = newNode;
        if (head == nullptr) {
            head = newNode;
        }
    }

    // その他の関数...
};

このコードでは、リストの末尾にある tail ポインタを使って、新しいノードを効率的に追加しています。

これにより、リストの最後にアクセスする際の時間コストを削減することが可能になります。

まとめ

この記事では、C++での連結リストの基本から応用までを幅広く解説しました。

初心者から上級者まで、連結リストの構築、操作、カスタマイズ方法について詳細なサンプルコードとともに紹介しました。

メモリ管理やポインタの使用に関する注意点も重視し、連結リストの効率的な使用を目指しました。

この知識を活用することで、C++におけるデータ構造の理解が深まるでしょう。