目次
効率的なプログラムを書くためには、データ構造の理解が不可欠だ。データ構造とは、データを効率的に保存・操作するための「整理術」である。同じデータでも、適切な構造で保持するかどうかで、処理速度が 100 倍、1000 倍と変わることもある。
本記事では、配列とリンクリスト、スタックとキュー、ハッシュマップとツリーという 4 つの柱から、データ構造の基礎を体系的に解説する。
データ構造の基本——なぜ必要なのか
データ構造が効率に与える影響
【データ構造の違いによる検索時間の差】
100 万個のデータから 1 つを検索する場合:
配列(線形探索): 平均 500,000 回比較 → 0.5 秒
ソート済み配列(二分探索): 最大 20 回比較 → 0.00002 秒
ハッシュマップ: 平均 1-2 回比較 → 0.000001 秒
→ 適切なデータ構造で 50 万倍の差
計算量の表記(ビッグオー記法):
def analyze_time_complexity():
"""計算量の基本概念"""
complexities = {
'O(1)': '定数時間(ハッシュマップの検索)',
'O(log n)': '対数時間(二分探索)',
'O(n)': '線形時間(配列の線形探索)',
'O(n log n)': '準線形時間(マージソート)',
'O(n²)': '二次時間(バブルソート)',
'O(2ⁿ)': '指数時間(再帰的フィボナッチ)'
}
return complexities
# 使用例
complexities = analyze_time_complexity()
for notation, description in complexities.items():
print(f"{notation}: {description}")
出力:
O(1): 定数時間(ハッシュマップの検索)
O(log n): 対数時間(二分探索)
O(n): 線形時間(配列の線形探索)
O(n log n): 準線形時間(マージソート)
O(n²): 二次時間(バブルソート)
O(2ⁿ): 指数時間(再帰的フィボナッチ)
データ構造の選択基準
【データ構造選びの指針】
1. 検索が多い場合
- ハッシュマップ:O(1) 検索
- 二分探索木:O(log n) 検索
2. 挿入・削除が多い場合
- リンクリスト:O(1) 挿入・削除(先頭)
- 動的配列:O(n) 挿入・削除(中間)
3. 順序を維持したい場合
- 配列:インデックスで順序保持
- ツリー:ソート済み順序
4. FIFO/LIFO が必要な場合
- キュー:FIFO(先入れ先出し)
- スタック:LIFO(後入れ先出し)
配列とリンクリスト——連続と分散
配列(Array)——連続メモリ領域
class Array:
"""配列の基本概念(Python リスト)"""
def __init__(self):
self.data = []
def append(self, value):
"""末尾に追加:O(1)"""
self.data.append(value)
def insert(self, index, value):
"""途中に挿入:O(n)(要素シフトが必要)"""
self.data.insert(index, value)
def delete(self, index):
"""削除:O(n)(要素シフトが必要)"""
del self.data[index]
def search(self, value):
"""検索:O(n)(線形探索)"""
try:
return self.data.index(value)
except ValueError:
return -1
def get(self, index):
"""インデックスアクセス:O(1)"""
return self.data[index]
# 使用例
arr = Array()
arr.append(10) # [10]
arr.append(20) # [10, 20]
arr.append(30) # [10, 20, 30]
arr.insert(1, 15) # [10, 15, 20, 30]
print(f"配列:{arr.data}")
print(f"15 のインデックス:{arr.search(15)}")
print(f"インデックス 2 の要素:{arr.get(2)}")
配列の特徴:
【配列のメモリ構造】
┌─────┬─────┬─────┬─────┬─────┐
│ 10 │ 15 │ 20 │ 30 │ │
└─────┴─────┴─────┴─────┴─────┘
↑ ↑ ↑ ↑
0x100 0x104 0x108 0x10C ← 連続メモリ領域
メリット:
・インデックスアクセスが O(1) で高速
・キャッシュヒット率が高い(局所性)
デメリット:
・途中挿入・削除は要素シフトで O(n)
・サイズ変更にコスト(動的配列は別)
リンクリスト(Linked List)——ポインタ接続
class Node:
"""リンクリストのノード"""
def __init__(self, value):
self.value = value
self.next = None # 次のノードへのポインタ
class LinkedList:
"""単方向リンクリスト"""
def __init__(self):
self.head = None
self.size = 0
def prepend(self, value):
"""先頭に追加:O(1)"""
new_node = Node(value)
new_node.next = self.head
self.head = new_node
self.size += 1
def append(self, value):
"""末尾に追加:O(n)"""
new_node = Node(value)
if not self.head:
self.head = new_node
else:
current = self.head
while current.next: # 末尾まで移動:O(n)
current = current.next
current.next = new_node
self.size += 1
def insert_after(self, target_value, new_value):
"""指定値の後に挿入:O(n)"""
current = self.head
while current:
if current.value == target_value:
new_node = Node(new_value)
new_node.next = current.next
current.next = new_node
self.size += 1
return True
current = current.next
return False
def delete(self, value):
"""削除:O(n)"""
if not self.head:
return False
if self.head.value == value:
self.head = self.head.next
self.size -= 1
return True
current = self.head
while current.next:
if current.next.value == value:
current.next = current.next.next
self.size -= 1
return True
current = current.next
return False
def search(self, value):
"""検索:O(n)"""
current = self.head
index = 0
while current:
if current.value == value:
return index
current = current.next
index += 1
return -1
def to_list(self):
"""Python リストに変換"""
result = []
current = self.head
while current:
result.append(current.value)
current = current.next
return result
# 使用例
ll = LinkedList()
ll.prepend(30) # 30
ll.prepend(20) # 20 → 30
ll.prepend(10) # 10 → 20 → 30
ll.append(40) # 10 → 20 → 30 → 40
ll.insert_after(20, 25) # 10 → 20 → 25 → 30 → 40
print(f"リンクリスト:{ll.to_list()}")
print(f"サイズ:{ll.size}")
print(f"25 のインデックス:{ll.search(25)}")
ll.delete(20)
print(f"削除後:{ll.to_list()}")
リンクリストの特徴:
【リンクリストのメモリ構造】
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ 10 │ → │ 20 │ → │ 30 │ → │ 40 │
│ 0xA │ │ 0xB │ │ 0xC │ │NULL │
└─────┘ └─────┘ └─────┘ └─────┘
↑ ↑ ↑ ↑
0x100 0x200 0x300 0x400 ← 分散メモリ領域
メリット:
・先頭挿入・削除が O(1) で高速
・メモリ断片化に強い
デメリット:
・インデックスアクセスは O(n)
・キャッシュヒット率が低い
配列 vs リンクリスト——比較
def compare_array_vs_linked_list(n=10000):
"""配列とリンクリストのパフォーマンス比較"""
import time
# 配列テスト
arr = []
start = time.time()
for i in range(n):
arr.insert(0, i) # 先頭挿入
array_prepend = time.time() - start
arr = list(range(n))
start = time.time()
_ = arr[n // 2] # 中央アクセス
array_access = time.time() - start
# リンクリストテスト
ll = LinkedList()
start = time.time()
for i in range(n):
ll.prepend(i) # 先頭挿入
linked_prepend = time.time() - start
start = time.time()
_ = ll.search(n // 2) # 中央アクセス
linked_access = time.time() - start
return {
'先頭挿入(配列)': f"{array_prepend:.4f}秒",
'先頭挿入(リンクリスト)': f"{linked_prepend:.4f}秒",
'中央アクセス(配列)': f"{array_access:.6f}秒",
'中央アクセス(リンクリスト)': f"{linked_access:.4f}秒"
}
# 使用例(小規模でテスト)
results = compare_array_vs_linked_list(1000)
for operation, time_taken in results.items():
print(f"{operation}: {time_taken}")
比較表:
| 操作 | 配列 | リンクリスト |
|---|---|---|
| 先頭挿入 | O(n) | O(1) |
| 末尾挿入 | O(1)* | O(n) |
| 途中挿入 | O(n) | O(n) |
| 先頭削除 | O(n) | O(1) |
| 検索 | O(n) | O(n) |
| インデックスアクセス | O(1) | O(n) |
*動的配列の場合
スタックとキュー——操作制限付き構造
スタック(Stack)——LIFO
class Stack:
"""スタック(後入れ先出し:LIFO)"""
def __init__(self):
self.items = []
def push(self, value):
"""プッシュ:O(1)"""
self.items.append(value)
def pop(self):
"""ポップ:O(1)"""
if not self.is_empty():
return self.items.pop()
return None
def peek(self):
"""トップ要素確認:O(1)"""
if not self.is_empty():
return self.items[-1]
return None
def is_empty(self):
"""空判定:O(1)"""
return len(self.items) == 0
def __len__(self):
return len(self.items)
# 使用例:バックトラック(行き止まり検出)
def solve_maze_with_stack(maze, start, end):
"""スタックを使った迷路探索(深さ優先探索)"""
stack = Stack()
stack.push((start, [start])) # (現在位置,経路)
visited = set()
while len(stack) > 0:
(current, path) = stack.pop()
if current == end:
return path
if current in visited:
continue
visited.add(current)
# 隣接セルをスタックにプッシュ
for neighbor in get_neighbors(maze, current):
if neighbor not in visited:
stack.push((neighbor, path + [neighbor]))
return None # 経路なし
def get_neighbors(maze, pos):
"""迷路の隣接セル取得(簡易版)"""
x, y = pos
neighbors = []
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
nx, ny = x + dx, y + dy
if 0 <= nx < len(maze) and 0 <= ny < len(maze[0]):
if maze[nx][ny] == 0: # 0=通路
neighbors.append((nx, ny))
return neighbors
# 使用例
maze = [
[0, 1, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
[0, 0, 0, 0, 0]
]
path = solve_maze_with_stack(maze, (0, 0), (4, 4))
print(f"経路:{path}")
スタックの応用例:
【スタックのユースケース】
1. 関数呼び出し(コールスタック)
- 再帰呼び出しの管理
- ローカル変数の保存
2. 式の評価
- 逆ポーランド記法
- 括弧の整合性チェック
3. バックトラック
- 迷路探索
- パズルSolver
4. 履歴管理
- ブラウザの「戻る」ボタン
- テキストエディタの Undo
キュー(Queue)——FIFO
from collections import deque
class Queue:
"""キュー(先入れ先出し:FIFO)"""
def __init__(self):
self.items = deque() # 両端キュー:O(1) 操作
def enqueue(self, value):
"""エンキュー:O(1)"""
self.items.append(value)
def dequeue(self):
"""デキュー:O(1)"""
if not self.is_empty():
return self.items.popleft()
return None
def front(self):
"""先頭要素確認:O(1)"""
if not self.is_empty():
return self.items[0]
return None
def is_empty(self):
"""空判定:O(1)"""
return len(self.items) == 0
def __len__(self):
return len(self.items)
# 使用例:幅優先探索(BFS)
def shortest_path_bfs(maze, start, end):
"""キューを使った最短経路探索(幅優先探索)"""
queue = Queue()
queue.enqueue((start, [start]))
visited = set()
while len(queue) > 0:
(current, path) = queue.dequeue()
if current == end:
return path
if current in visited:
continue
visited.add(current)
# 隣接セルをキューにエンキュー
for neighbor in get_neighbors(maze, current):
if neighbor not in visited:
queue.enqueue((neighbor, path + [neighbor]))
return None # 経路なし
# 使用例
path = shortest_path_bfs(maze, (0, 0), (4, 4))
print(f"最短経路:{path}")
キューの応用例:
【キューのユースケース】
1. タスクスケジューリング
- OS のプロセス管理
- 印刷ジョブ管理
2. 幅優先探索(BFS)
- 最短経路探索
- ソーシャルネットワーク(友達の友達)
3. メッセージキュー
- 非同期処理
- ワークキュー
4. バッファリング
- ストリーミング再生
- 入出力バッファ
スタック vs キュー——比較
def compare_stack_vs_queue():
"""スタックとキューの動作比較"""
print("【スタック(LIFO)】")
stack = Stack()
for i in range(1, 4):
stack.push(i)
print(f"プッシュ:{i} → {stack.items}")
while len(stack) > 0:
print(f"ポップ:{stack.pop()} → {stack.items}")
print("\n【キュー(FIFO)】")
queue = Queue()
for i in range(1, 4):
queue.enqueue(i)
print(f"エンキュー:{i} → {list(queue.items)}")
while len(queue) > 0:
print(f"デキュー:{queue.dequeue()} → {list(queue.items)}")
compare_stack_vs_queue()
出力:
【スタック(LIFO)】
プッシュ:1 → [1]
プッシュ:2 → [1, 2]
プッシュ:3 → [1, 2, 3]
ポップ:3 → [1, 2]
ポップ:2 → [1]
ポップ:1 → []
【キュー(FIFO)】
エンキュー:1 → [1]
エンキュー:2 → [1, 2]
エンキュー:3 → [1, 2, 3]
デキュー:1 → [2, 3]
デキュー:2 → [3]
デキュー:3 → []
ハッシュマップとツリー——高速検索と階層構造
ハッシュマップ(Hash Map)——O(1) 検索
class HashMap:
"""ハッシュマップ(辞書)の基本概念"""
def __init__(self, capacity=16):
self.capacity = capacity
self.buckets = [[] for _ in range(capacity)] # 衝突はチェイン法
self.size = 0
def _hash(self, key):
"""ハッシュ関数"""
return hash(key) % self.capacity
def put(self, key, value):
"""挿入:平均 O(1)"""
index = self._hash(key)
bucket = self.buckets[index]
# 既存キー更新
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
# 新規追加
bucket.append((key, value))
self.size += 1
# リサイズ(簡易版)
if self.size > self.capacity * 0.75:
self._resize()
def get(self, key):
"""検索:平均 O(1)"""
index = self._hash(key)
bucket = self.buckets[index]
for k, v in bucket:
if k == key:
return v
raise KeyError(key)
def remove(self, key):
"""削除:平均 O(1)"""
index = self._hash(key)
bucket = self.buckets[index]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
self.size -= 1
return True
return False
def _resize(self):
"""リサイズ:O(n)"""
old_buckets = self.buckets
self.capacity *= 2
self.buckets = [[] for _ in range(self.capacity)]
self.size = 0
for bucket in old_buckets:
for key, value in bucket:
self.put(key, value)
def __len__(self):
return self.size
def __repr__(self):
items = []
for bucket in self.buckets:
for key, value in bucket:
items.append(f"{key}: {value}")
return "{" + ", ".join(items) + "}"
# 使用例
hm = HashMap()
hm.put("name", "Alice")
hm.put("age", 30)
hm.put("city", "Tokyo")
print(f"ハッシュマップ:{hm}")
print(f"name: {hm.get('name')}")
print(f"age: {hm.get('age')}")
hm.remove("city")
print(f"削除後:{hm}")
ハッシュマップの仕組み:
【ハッシュマップの構造】
キー「apple」→ ハッシュ関数 → インデックス 3
┌───────┬───────┬───────┬────────────┬───────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │
├───────┼───────┼───────┼────────────┼───────┤
│ │ │ │ (apple,1) │ │
│ │ │ │ (apply,2) │ │
│ │ │ │ │ │
└───────┴───────┴───────┴────────────┴───────┘
↑
衝突(チェイン法で解決)
メリット:
・検索・挿入・削除が平均 O(1) で最速
・実装が比較的単純
デメリット:
・順序が保持されない
・最悪計算量は O(n)(衝突多発時)
・メモリ効率(スパースな場合)
二分探索木(Binary Search Tree)——階層的探索
class TreeNode:
"""二分探索木のノード"""
def __init__(self, value):
self.value = value
self.left = None # 小さい値
self.right = None # 大きい値
class BinarySearchTree:
"""二分探索木"""
def __init__(self):
self.root = None
def insert(self, value):
"""挿入:平均 O(log n)、最悪 O(n)"""
self.root = self._insert_recursive(self.root, value)
def _insert_recursive(self, node, value):
"""再帰的挿入"""
if node is None:
return TreeNode(value)
if value < node.value:
node.left = self._insert_recursive(node.left, value)
elif value > node.value:
node.right = self._insert_recursive(node.right, value)
# 重複は挿入しない
return node
def search(self, value):
"""検索:平均 O(log n)、最悪 O(n)"""
return self._search_recursive(self.root, value)
def _search_recursive(self, node, value):
"""再帰的検索"""
if node is None:
return False
if node.value == value:
return True
elif value < node.value:
return self._search_recursive(node.left, value)
else:
return self._search_recursive(node.right, value)
def delete(self, value):
"""削除:平均 O(log n)、最悪 O(n)"""
self.root = self._delete_recursive(self.root, value)
def _delete_recursive(self, node, value):
"""再帰的削除"""
if node is None:
return None
if value < node.value:
node.left = self._delete_recursive(node.left, value)
elif value > node.value:
node.right = self._delete_recursive(node.right, value)
else:
# 削除対象発見
# 子なし
if node.left is None and node.right is None:
return None
# 右子のみ
if node.left is None:
return node.right
# 左子のみ
if node.right is None:
return node.left
# 両方あり:右部分木の最小値で置換
min_value = self._find_min(node.right)
node.value = min_value
node.right = self._delete_recursive(node.right, min_value)
return node
def _find_min(self, node):
"""最小値探索"""
while node.left:
node = node.left
return node.value
def inorder_traversal(self):
"""中間順走査:ソート済み順序"""
result = []
self._inorder_recursive(self.root, result)
return result
def _inorder_recursive(self, node, result):
"""再帰的中間順走査"""
if node:
self._inorder_recursive(node.left, result)
result.append(node.value)
self._inorder_recursive(node.right, result)
# 使用例
bst = BinarySearchTree()
for value in [50, 30, 70, 20, 40, 60, 80]:
bst.insert(value)
print(f"ソート済み:{bst.inorder_traversal()}")
print(f"40 を検索:{bst.search(40)}") # True
print(f"25 を検索:{bst.search(25)}") # False
bst.delete(30)
print(f"削除後:{bst.inorder_traversal()}")
二分探索木の構造:
【二分探索木の例】
50
/ \
30 70
/ \ / \
20 40 60 80
性質:
・左部分木 < 親 < 右部分木
・中間順走査でソート済み順序
計算量:
・平衡状態:O(log n)
・偏りの場合:O(n)(片寄った木)
平衡二分探索木——自己調整
def analyze_tree_balance():
"""木の平衡状態による性能差"""
# 平衡な木の場合(log₂100万 ≈ 20)
balanced_height = 20
print(f"平衡な木(100 万要素):最大{balanced_height}回の比較")
# 偏った木の場合(100 万)
skewed_height = 1_000_000
print(f"偏った木(100 万要素):最大{skewed_height}回の比較")
print(f"\n性能差:{skewed_height / balanced_height}倍")
analyze_tree_balance()
出力:
平衡な木(100 万要素):最大 20 回の比較
偏った木(100 万要素):最大 1000000 回の比較
性能差:50000.0倍
平衡木の種類:
【主要な平衡木】
1. AVL 木
- 部分木の高さ差 ≤ 1
- 厳密な平衡、検索重視
2. 赤黒木
- 緩和された平衡
- 挿入・削除頻繁な場合に有利
- Java TreeMap、C++ std::map
3. B 木
- 多分探索木
- ディスクアクセス最小化
- データベースインデックス
データ構造の選択——実践ガイド
使用場面別おすすめ
def choose_data_structure(requirements):
"""要件に基づいてデータ構造を選択"""
decision_tree = {
'key_value_pairs': {
'order_matters': 'OrderedDict(Python 3.7+ は dict)',
'order_not_matters': 'dict(ハッシュマップ)',
'sorted_keys': 'SortedDict または 二分探索木'
},
'ordered_collection': {
'frequent_access': 'list(配列)',
'frequent_insert_delete_start': 'collections.deque(リンクリスト)',
'frequent_insert_delete_middle': 'list(ただし O(n))'
},
'special_requirements': {
'fifo': 'queue.Queue(キュー)',
'lifo': 'list(スタック)',
'priority': 'heapq(ヒープ)',
'unique_elements': 'set(ハッシュセット)',
'fast_lookup': 'dict(ハッシュマップ)'
}
}
return decision_tree
# 使用例
ds = choose_data_structure({})
print("【キー・バリュー】")
for req, structure in ds['key_value_pairs'].items():
print(f"- {req}: {structure}")
print("\n【順序付きコレクション】")
for req, structure in ds['ordered_collection'].items():
print(f"- {req}: {structure}")
print("\n【特殊要件】")
for req, structure in ds['special_requirements'].items():
print(f"- {req}: {structure}")
パフォーマンス比較まとめ
def compare_all_data_structures():
"""全データ構造のパフォーマンス比較"""
comparison = """
【データ構造の計算量比較】
| 検索 | 挿入 | 削除 | アクセス
----------------|--------|--------|--------|--------
配列(list) | O(n) | O(n) | O(n) | O(1)
リンクリスト | O(n) | O(1)* | O(1)* | O(n)
スタック | - | O(1) | O(1) | O(1)**
キュー | - | O(1) | O(1) | O(1)**
ハッシュマップ | O(1) | O(1) | O(1) | -
二分探索木 | O(log n)| O(log n)| O(log n)| -
* 先頭の場合
** トップ/先頭のみ
【メモリ効率比較】
| メモリ効率 | キャッシュ効率
----------------|-----------|---------------
配列 | 高 | 高(連続)
リンクリスト | 中 | 低(分散)
ハッシュマップ | 中 | 中
ツリー | 中 | 低
"""
return comparison
print(compare_all_data_structures())
よくある誤解と落とし穴
1. 「ハッシュマップは常に O(1)」の誤解
def demonstrate_hash_collisions():
"""ハッシュ衝突による性能劣化"""
# 悪いハッシュ関数(常に同じ値を返す)
class BadHashMap:
def __init__(self):
self.bucket = [] # 1 つのバケットに全部
def put(self, key, value):
self.bucket.append((key, value)) # 全部追加
def get(self, key):
for k, v in self.bucket: # 線形探索:O(n)
if k == key:
return v
raise KeyError(key)
# 1000 件插入
bad_map = BadHashMap()
for i in range(1000):
bad_map.put(f"key_{i}", i)
# 検索は O(n) に劣化
import time
start = time.time()
for i in range(1000):
_ = bad_map.get(f"key_{i}")
elapsed = time.time() - start
print(f"悪いハッシュマップ:{elapsed:.4f}秒(O(n²))")
demonstrate_hash_collisions()
2. 「配列=固定長」の誤解
【配列の種類】
固定長配列(C 言語など):
・コンパイル時にサイズ決定
・メモリ効率良し
・サイズ変更不可
動的配列(Python list):
・実行時にサイズ変更
・内部で 2 倍戦略(amortized O(1))
・メモリ効率は中程度
3. 「ツリー=常に O(log n)」の誤解
def demonstrate_skewed_tree():
"""偏った二分探索木の例"""
bst = BinarySearchTree()
# ソート済みデータ插入→右に偏る
for i in range(1, 11):
bst.insert(i)
# 木の高さ:10(O(n))
print(f"要素数:10, 木の高さ:10")
print(f"検索計算量:O(n) に劣化")
# 対策:平衡木(AVL、赤黒木)を使用
demonstrate_skewed_tree()
まとめ
データ構造の核心:
- 配列とリンクリスト: 連続 vs 分散、アクセス vs 挿入
- スタックとキュー: LIFO vs FIFO、バックトラック vs 幅優先
- ハッシュマップ: O(1) 検索、キー・バリュー管理
- ツリー: 階層構造、ソート済み探索
適切なデータ構造の選択が、プログラムの性能を決定する。
重要なのは、各データ構造のトレードオフを理解することだ:
- 配列:高速アクセス vs 挿入削除コスト
- リンクリスト:高速挿入 vs 低速アクセス
- ハッシュマップ:高速検索 vs 順序保持不可
- ツリー:ソート済み探索 vs 平衡管理
「銀の弾丸」はない。要件に応じて最適な構造を選択する判断力が、優れたエンジニアの証である。
参考資料
- “Introduction to Algorithms” Thomas H. Cormen 著(通称 CLRS)
- “Algorithms” Robert Sedgewick 著
- “Cracking the Coding Interview” Gayle Laakmann McDowell 著
- Python 公式ドキュメント - データ構造:https://docs.python.org/ja/3/tutorial/datastructures.html
- Big-O Cheat Sheet: https://www.bigocheatsheet.com/
- VisuAlgo(データ構造可視化):https://visualgo.net/
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。