見出し画像

データ構造-1:配列・連結リスト・ハッシュテーブル・セット

この記事は、chatGPTが書いています。
今回から、データ構造を扱います。
リンク先のnotebookで動作確認できます。ぜひ、動かしてみてください。


Python初学者必読!ワクワクするデータ構造入門

配列・連結リスト・ハッシュテーブル・セットをマスターしよう

はじめに

プログラミングに興味を持ったとき、「データ構造」というキーワードを耳にしたことがあるかもしれません。データ構造とは、データをどうやって「しまっておくか」を整理する方法のこと。適切なデータ構造を選ぶと、コードがスッキリしたり、実行速度がグッと速くなったりします。
この記事では、Pythonの代表的なデータ構造である「配列(list)」「連結リスト」「ハッシュテーブル(dict)」「セット(set)」をやさしく解説します。初心者の方から経験者の方まで、気軽に読んでいただき、「あ、こんな使い道もあったんだ!」と楽しんでもらえれば嬉しいです。


1. 配列(Array)~シンプルだけど強力!~

1.1 そもそも配列って何?

配列は、同じ種類のデータを連続して並べた構造です。連番の「住所」を使って、要素をひとつひとつ取り出せるのが特徴。たとえば「3番目の要素ちょうだい」とインデックスで指定すれば、サッとデータを取り出せます。

1.2 Pythonでの配列は「リスト」が主役

Pythonでは、いわゆる「配列」の代わりに list 型が最もよく使われます。

  • 柔軟性が高い:整数、文字列、オブジェクトなど、どんな型でも混在OK。

  • 可変長:要素数を自由に増やしたり減らしたりできる。

  • インデックスアクセスが速い:連続したメモリを確保しているイメージ。

1.3 コードで実感:リストの基本操作

# リストの宣言
numbers = [10, 20, 30, 40, 50]

# インデックスで要素を取り出す
print(numbers[0])   # 10
print(numbers[2])   # 30

# 要素を末尾に追加
numbers.append(60)
print(numbers)      # [10, 20, 30, 40, 50, 60]

# インデックス指定で要素を削除
del numbers[1]
print(numbers)      # [10, 30, 40, 50, 60]

リストは、まず使い方を覚えやすいのがメリット。唯一注意点としては、リストの中ほどに要素を挿入や削除すると、そこから後ろの要素を一斉にずらす必要があるので、要素数が多いときは少し重たくなることがあります。


2. 連結リスト(Linked List)~ノードを鎖のようにつなげる~

2.1 どんなデータ構造?

連結リストは、鎖のようにつながったノードの集まりです。各ノードが「データ」と「次のノードの参照(ポインタ)」を持っていて、先頭から次へ次へとたどりながらアクセスします。
(イメージ図:ノード A → B → C と矢印で指し示しながら連結している)

◆メリット

  • リスト途中への挿入や削除がスムーズ(つながりを付け替えるだけ)。

  • 連続したメモリ領域を確保する必要がなく、バラバラの場所にデータを置いてもOK。

◆デメリット

  • インデックス(n番目)で直接アクセスできない(先頭から順にたどる必要がある)。

  • Python標準の組み込みデータ構造には含まれていないので、自分でクラスを作る必要がある。

2.2 コード実例:単方向連結リストを作ってみよう

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None  # 次のノードへの参照

class LinkedList:
    def __init__(self):
        self.head = None  # リストの先頭を指す

    def append(self, data):
        """リストの末尾にデータを追加"""
        new_node = Node(data)
        if self.head is None:
            # リストが空だったら先頭に置く
            self.head = new_node
            return

        current = self.head
        while current.next:
            current = current.next
        current.next = new_node

    def insert_after(self, prev_data, new_data):
        """指定したデータのノードの後ろに新データを挿入"""
        current = self.head
        while current:
            if current.data == prev_data:
                new_node = Node(new_data)
                new_node.next = current.next
                current.next = new_node
                return
            current = current.next
        print(f"{prev_data} が見つかりませんでした。")

    def delete(self, data):
        """指定したデータのノードを削除"""
        current = self.head
        prev = None
        while current:
            if current.data == data:
                if prev is None:
                    # 先頭を削除
                    self.head = current.next
                else:
                    prev.next = current.next
                return
            prev = current
            current = current.next
        print(f"{data} が見つかりませんでした。")

    def print_list(self):
        """リストをすべて表示"""
        current = self.head
        while current:
            print(current.data, end=" -> ")
            current = current.next
        print("None")

# 動作テスト
linked_list = LinkedList()
linked_list.append(10)
linked_list.append(20)
linked_list.append(30)
linked_list.print_list()  # 10 -> 20 -> 30 -> None

linked_list.insert_after(20, 25)
linked_list.print_list()  # 10 -> 20 -> 25 -> 30 -> None

linked_list.delete(25)
linked_list.print_list()  # 10 -> 20 -> 30 -> None

自分で実装する手間はありますが、各ノードを「つなげる・外す」だけで済むので、データの増減が激しい場面では非常に便利です。


3. ハッシュテーブル(Hash Table)~キーで素早く探せる魔法の仕組み~

3.1 ハッシュテーブルって?

ハッシュテーブルは、キーと値をペアで管理し、キーから値を素早く見つけるためのデータ構造です。

  • ハッシュ関数を使い、キーを何らかの数値(ハッシュ値)に変換し、その数値を利用してデータを格納・検索します。

  • 平均的に O(1) 時間で検索できるとされ、数が多くてもサクサク動く頼れる存在です。

3.2 Pythonの辞書(dict)こそがハッシュテーブル

Pythonの dict は、内部でハッシュテーブルを使っています。 キーと値を好きなだけ詰め込んで、取り出したいときは「キー」を使って一発でアクセスします。

person_info = {
    "name": "Alice",
    "age": 25,
    "city": "Tokyo"
}

# 値の取得
print(person_info["name"])   # Alice

# キーと値を追加
person_info["job"] = "Engineer"
print(person_info)           # {"name": "Alice", "age": 25, "city": "Tokyo", "job": "Engineer"}

# 値の更新
person_info["age"] = 26
print(person_info["age"])    # 26

# キーの削除
del person_info["city"]
print(person_info)           # {"name": "Alice", "age": 26, "job": "Engineer"}

# dictのすべてのキーと値を処理
for key, value in person_info.items():
    print(key, ":", value)

例えば辞書を使えば、ユーザー情報や設定ファイルのオプション管理など、ありとあらゆる場面で役立ちます。まさにPythonプログラミングの要となるデータ構造です。


4. セット(Set)~重複なしで効率的!~

4.1 セットの特徴

セットは、「重複しない要素を集めたデータ構造」。

  • 内部的にハッシュテーブルを使用しているため、要素の追加や存在チェックが高速

  • 同じ値を何度追加しても1つしか保存されず、「ユニークな値だけ扱いたい」という場合にぴったりです。

  • 集合演算(和集合・積集合・差集合など)をサクッと扱えるので、数学的な処理が必要なときにも便利。

4.2 サンプルコード:セットを使いこなそう

fruits = {"apple", "banana", "orange"}
print(fruits)  # {'apple', 'banana', 'orange'}

# 要素の追加
fruits.add("grape")
print(fruits)  # {'apple', 'banana', 'orange', 'grape'}

# 重複した要素を追加しても変化なし
fruits.add("apple")
print(fruits)  # {'apple', 'banana', 'orange', 'grape'}

# 要素の削除
fruits.remove("banana")
print(fruits)  # {'apple', 'orange', 'grape'}

# 集合演算
set_a = {1, 2, 3}
set_b = {2, 3, 4}

print(set_a.union(set_b))        # {1, 2, 3, 4}  (和集合)
print(set_a.intersection(set_b)) # {2, 3}       (積集合)
print(set_a.difference(set_b))   # {1}          (差集合)

# 要素の存在チェック
print("orange" in fruits)  # True
print("banana" in fruits)  # False

「メールアドレスの重複をなくしたい」「一意なIDがいくつあるか知りたい」なんて時に、セットが活躍します。


5. まとめ~自分に合ったデータ構造を選ぼう~

  • 配列(list):

    • インデックスを用いて高速にアクセス可能。

    • 挿入・削除をリスト中間で頻繁に行うとやや非効率に。

  • 連結リスト:

    • 要素の途中挿入や削除が得意。

    • 代わりにインデックスアクセスは苦手。Pythonでは自作クラスが必要。

  • ハッシュテーブル(dict):

    • キーで一発アクセス。検索・追加・削除が基本 O(1)。

    • タスク管理やオブジェクトの保存など、多彩な用途に使える。

  • セット(set):

    • 重複なしの要素管理。ハッシュテーブルを利用。

    • 存在チェックも集合演算も高速。

プログラムの要件やデータ量、操作の内容によって「何をメインにするか」を選ぶと、パフォーマンスや可読性が変わってきます。どのデータ構造も「向き不向き」があり、それぞれの特徴を把握して使い分けるのが大切です。

おわりに

いかがでしたか?

  • リストはとにかく使い勝手が良いので、最初に習得するにはもってこい。

  • 連結リストは、アルゴリズムの勉強で一度は実装してみると理解が深まります。

  • 辞書やセットは、開発の現場で実務的に大活躍するので覚えておくと超便利です。

これらのデータ構造を知っていると、問題解決のヒントや、より洗練されたコードを書くきっかけになります。ぜひ色々と試しながら、自分のプログラミングに活かしてみてください!
もしこの記事がお役に立ったなら、友達や同僚のプログラマにもシェアしていただけると嬉しいです。では、楽しいPythonライフをお送りください!

補足:連結リストのclassを解説


1. 全体像

このコードには、2つのクラスがあります。

  1. Node

    • 連結リストの“ひとつ分のデータ”を表すクラス。

    • 自分のデータ (data) と、次に繋がるノード (next) を持っている。

  2. LinkedList

    • 複数の Node を「鎖」のように繋げて管理するクラス。

    • リストの先頭を表す head という変数を持つ。

    • 要素の追加・挿入・削除・表示などの操作を行うためのメソッドを持っている。


2. Node クラス

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None  # 次のノードへの参照
  • class Node:

    • 連結リストの中の「1つの要素(ノード)」を表すクラスです。

  • def __init__(self, data):

    • Node クラスのコンストラクタ(初期化メソッド)。

    • クラスを使ってノードを作ったときに、自動的に呼び出されます。

  • self.data = data

    • このノードが持つ「実際のデータ」を data という変数に保持します。

    • self は「自分自身のインスタンス(オブジェクト)」を指す特別なキーワードです。

  • self.next = None

    • このノードが指す「次のノード(Nodeオブジェクト)への参照」を持ちます。

    • 初期化時はまだ「次がどのノードかわからない」ので、None にしておくのが一般的です。


3. LinkedList クラス

3.1 クラスの冒頭

class LinkedList:
    def __init__(self):
        self.head = None  # リストの先頭を指す
  • class LinkedList:

    • 連結リスト全体を管理するクラスです。

  • def __init__(self):

    • LinkedList クラスのコンストラクタ(初期化メソッド)です。

  • self.head = None

    • リストの先頭ノードへの参照を持つ変数。

    • 連結リストが空(要素がない)ときは先頭が存在しないので、 None で初期化しています。

3.2 append メソッド

def append(self, data):
    """リストの末尾にデータを追加"""
    new_node = Node(data)
    if self.head is None:
        # リストが空だったら先頭に置く
        self.head = new_node
        return

    current = self.head
    while current.next:
        current = current.next
    current.next = new_node
  • 目的

    • リストの末尾に新しいノード(Nodeオブジェクト)を追加します。

  • new_node = Node(data)

    • 新しいノードを作成し、そのノードの data に引数 data をセットします。

  • if self.head is None:

    • もし head が None であれば、リストが空(まだ要素がない)ということ。

    • その場合は、作ったノードを先頭(self.head)に設定して終わり。

  • current = self.head から while current.next:

    • リストの最後まで移動するために、current という変数でノードを一つずつ辿っていきます。

    • while current.next: は「次のノードがある限り」進む、という意味。

    • 最後のノードに到達したら、そのノードの next は None になっているのでループを抜けます。

  • current.next = new_node

    • 最後のノードの next に、新しいノードをつなげれば「末尾への追加」が完了です。

3.3 insert_after メソッド

def insert_after(self, prev_data, new_data):
    """指定したデータのノードの後ろに新データを挿入"""
    current = self.head
    while current:
        if current.data == prev_data:
            new_node = Node(new_data)
            new_node.next = current.next
            current.next = new_node
            return
        current = current.next
    print(f"{prev_data} が見つかりませんでした。")
  • 目的

    • リストの中の「特定のデータ」を持ったノードの「後ろ」に新しいノードを挿入します。

  • current = self.head

    • 先頭から探索を開始します。

  • while current:

    • current が None になるまでノードを一つずつ辿ります。

  • if current.data == prev_data:

    • いま見ているノードのデータが、挿入先として指定した prev_data と一致したら、そこで操作を実行します。

  • new_node = Node(new_data)

    • 新しいノードを作成し、持たせるデータを new_data に。

  • new_node.next = current.next / current.next = new_node

    • 新しいノードが、いまのノード(current)の「次」を先に覚えてから、current の next に新しいノードをつなげます。

    • ノードの「つながり」を入れ替えることで、挿入が実現します。

  • print(f"{prev_data} が見つかりませんでした。")

    • リストを全部回しても prev_data が見つからなかった場合にメッセージを表示します。

3.4 delete メソッド

def delete(self, data):
    """指定したデータのノードを削除"""
    current = self.head
    prev = None
    while current:
        if current.data == data:
            if prev is None:
                # 先頭を削除
                self.head = current.next
            else:
                prev.next = current.next
            return
        prev = current
        current = current.next
    print(f"{data} が見つかりませんでした。")
  • 目的

    • リストの中から、指定した data を持つノードを削除します。

  • current = self.head / prev = None

    • current は「今見ているノード」、prev は「一つ前のノード」を指します。

    • 先頭からノードを移動して探すための準備です。

  • while current:

    • current が None になるまで、ノードを辿ります。

  • if current.data == data:

    • もし、いま見ているノードが削除対象だったら、以下のどちらかを行います。

  • prev = current / current = current.next

    • 見るノードを一つ次にずらす前に、prev を今のノードに更新して、current を「次のノード」へ移動します。

  • print(f"{data} が見つかりませんでした。")

    • 全部探索したけど該当するデータが見つからなかったときのメッセージ。

3.5 print_list メソッド

def print_list(self):
    """リストをすべて表示"""
    current = self.head
    while current:
        print(current.data, end=" -> ")
        current = current.next
    print("None")
  • 目的

    • 連結リストの全ノードを順番に表示して、最終的に None(末端)で終わるイメージを出しています。

  • current = self.head

    • 先頭ノードから表示を始めます。

  • while current:

    • current が None になるまでループ。「今のノードがある限り」という意味。

  • print(current.data, end=" -> ")

    • 今のノードが持つデータを表示し、矢印っぽい文字列 > をつけて次へ続くことを示しています。

  • current = current.next

    • 次のノードに進みます。

  • print("None")

    • リストの最後には None と表示して、リストの終わりだとわかるようにしています。


4. 動作テスト

# 動作テスト
linked_list = LinkedList()
linked_list.append(10)
linked_list.append(20)
linked_list.append(30)
linked_list.print_list()  # 10 -> 20 -> 30 -> None

linked_list.insert_after(20, 25)
linked_list.print_list()  # 10 -> 20 -> 25 -> 30 -> None

linked_list.delete(25)
linked_list.print_list()  # 10 -> 20 -> 30 -> None
  1. linked_list = LinkedList()

    • 新しい LinkedList インスタンスを作り、変数 linked_list に代入。

  2. linked_list.append(10), append(20), append(30)

    • 順番に末尾へノードを追加。

    • 内部では「空なら先頭に」「空じゃなかったら末尾まで辿って挿入」という処理が走ります。

  3. linked_list.print_list()

    • ノードを先頭から辿って表示し、 None まで表示されます。

  4. insert_after(20, 25)

    • データが「20」のノードを探し、そのすぐ後ろに「25」のノードを挿入。

  5. delete(25)

    • データが「25」のノードを探して削除。削除するとノードが鎖から外れる構造になり、リストから消える。

こうして一連の挿入・表示・削除が正しく動作しているかを確認できます。


まとめ

  • Nodeクラス: 連結リストを構成する1要素を表すクラス。

    • self.data:ノードが保持するデータ

    • self.next:次のノードへの参照(None ならリストの終わり)

  • LinkedListクラス: 複数のノードを繋げ、リストとして管理するクラス。

    • self.head:先頭ノードへの参照(空の場合は None)

    • append():末尾にノードを追加

    • insert_after():指定データを持つノードの後ろに挿入

    • delete():指定データを持つノードを削除

    • print_list():リスト全体を表示

クラスのキモは、「オブジェクトが持つデータ(変数)と、それを操作する機能(メソッド)をひとまとめにする」 という点です。連結リストをクラスで作ることで、ノードの追加・削除・表示といった処理をひとつのまとまりで管理できるようになります。
以上で、連結リストのクラス実装についてのやさしい解説は終わりです。クラスを理解すると、プログラムの構造をきれいに分けられるようになるので、ぜひ使いこなしてみてください!

いいなと思ったら応援しよう!