フリーキーズ | 独学プログラミング

Big-O記法とアルゴリズム計算量入門!プログラミング初心者でもわかる基礎知識

リンドくん

リンドくん

先生、「Big-O記法」とか「計算量」っていう言葉をよく聞くんですけど、これって何なんですか?難しそうで...

たなべ

たなべ

Big-O記法や計算量は「プログラムがどれくらい速いか」を表す物差しのようなものなんだよ。
料理で例えると、「何人分の料理を作るのにどれくらい時間がかかるか」を予測する方法みたいなものかな。

リンドくん

リンドくん

なるほど!でも、なんでそんなものが必要なんですか?

たなべ

たなべ

それはね、同じ結果を出すプログラムでも、書き方によって速度が全然違うからなんだ。
10件のデータなら気にならなくても、100万件になったら何時間もかかる...なんてことも珍しくないんだよ。今日はそのあたりをしっかり理解していこう!

プログラミングを学んでいくと、「動くコードを書く」だけでなく「効率的なコードを書く」ことの重要性に気づく瞬間が訪れます。
その効率性を測る基準となるのがアルゴリズムの計算量であり、それを表現する方法がBig-O記法です。

本記事では、プログラミング初心者の方でも理解できるよう、Big-O記法とアルゴリズム計算量の基礎を丁寧に解説していきます。
この知識は、単なる理論ではなく、実際のプログラミングにおいて「なぜこのコードは遅いのか」「どう改善すれば速くなるのか」を判断する力につながります。

オンラインコミュニティ運営しています

HackATAは、IT技術を習得する人のために広く開かれたオンラインコミュニティです。 現在、無料コミュニティとして開放していますので、ご気軽に参加してください。

✓ 再立ち上げ

✓ コミュニティの方向性について意見募集中

HackATA公式Webサイト

そもそもアルゴリズムの計算量とは何か

リンドくん

リンドくん

「計算量」って、具体的に何を計算しているんですか?

たなべ

たなべ

それはね、データの量が増えたときに、処理時間や使うメモリがどれくらい増えるかを表しているんだ。
例えば、100人の名簿から特定の人を探すのと、10万人の名簿から探すのでは、時間のかかり方が全然違うよね?

計算量の基本的な考え方

アルゴリズムの計算量とは、プログラムが問題を解くために必要なリソース(時間やメモリ)の量を表す指標です。主に以下の2種類があります。

  • 時間計算量 → プログラムの実行にかかる時間
  • 空間計算量 → プログラムが使用するメモリの量

一般的に「計算量」といえば時間計算量を指すことが多く、本記事でも主に時間計算量について解説していきます。

なぜ計算量を学ぶ必要があるのか

実際のソフトウェア開発では、以下のような場面で計算量の理解が重要になります。

  • 大量のデータを扱う場合 → 数百万件のユーザーデータを検索する
  • リアルタイム処理が必要な場合 → ゲームやチャットアプリなど
  • サーバーコストの削減 → 効率的なコードはサーバー負荷を下げる
  • ユーザー体験の向上 → レスポンスの速いアプリは評価が高い

例えば、10件のデータを扱うプログラムでは、どんな書き方をしても一瞬で処理が終わるため違いが分かりません。しかし、100万件のデータになると、効率の悪いアルゴリズムでは数時間かかることもあるのです。

計算量を測る基準

計算量を測る際、私たちは入力サイズ(データの量)が増えたときに、処理時間がどう変化するかに注目します。

具体的には以下のような視点で考えます。

  • データが2倍になったら処理時間も2倍になる
  • データが2倍になったら処理時間が4倍になる
  • データが何倍になっても処理時間は変わらない

この「データ量と処理時間の関係性」を数学的に表現したものがBig-O記法なのです。

Big-O記法の基本

リンドくん

リンドくん

Big-O記法って、O(n)とかO(1)とか見るんですけど、あれって何を表しているんですか?

たなべ

たなべ

あぁ、それね!Oは「Order(オーダー)」の頭文字で、カッコの中の文字が「データ量が増えたときの処理時間の増え方」を表しているんだ。nはデータの個数を表す変数だよ。

Big-O記法とは

Big-O記法(Big O Notation)は、アルゴリズムの効率を表現するための数学的な記法です。
最悪の場合の計算量を表現することで、そのアルゴリズムのパフォーマンスを客観的に評価できます。

書き方は以下のような形式です。

O(関数)

この「関数」の部分に、入力サイズnに対する処理量の増加率を表す式が入ります。

代表的なBig-O記法

よく使われる計算量を、速い順に並べると以下のようになります。

  1. O(1) - 定数時間
  2. O(log n) - 対数時間
  3. O(n) - 線形時間
  4. O(n log n) - 線形対数時間
  5. O(n²) - 二乗時間
  6. O(2ⁿ) - 指数時間
  7. O(n!) - 階乗時間

下に行くほど、データ量が増えたときに処理時間が急激に増加します。実用的なプログラムでは、できるだけ上の方(速い計算量)を目指すことが重要です。

Big-O記法の読み方

それぞれの記法が実際に何を意味するのか、簡単に説明します。

  • O(1)「オーワン」 → データが何個あっても処理時間は一定
  • O(n)「オーエヌ」 → データがn個あれば、処理もn回必要
  • O(n²)「オーエヌ二乗」 → データがn個あれば、処理はn×n回必要

この記法を理解することで、「このアルゴリズムはデータ量が増えるとどうなるか」を予測できるようになります。

O(1) 定数時間 - 最も速い処理

リンドくん

リンドくん

O(1)って、データが増えても処理時間が変わらないってことですか?すごいですね!

たなべ

たなべ

そうなんだ!例えば、配列の最初の要素を取り出すとか、変数に値を代入するといった処理がこれに当たるよ。何回実行しても同じ時間で終わるんだ。

O(1)の特徴

O(1)は定数時間と呼ばれ、入力サイズに関係なく常に一定の時間で処理が完了します。これは最も効率的な計算量です。

具体的には以下のような処理がO(1)になります。

  • 配列の特定位置の要素へのアクセス
  • 変数への値の代入
  • 数値の計算(足し算、掛け算など)
  • ハッシュテーブルでの検索(理想的な場合)

O(1)のコード例

# 配列の最初の要素を取得(O(1))
def get_first_element(array):
    return array[0]

# 使用例
numbers = [1, 2, 3, 4, 5]
first = get_first_element(numbers)  # 配列が何個あっても同じ速度

このコードは、配列に10個の要素があっても、100万個の要素があっても、同じ速度で最初の要素を取得できます。なぜなら、配列の先頭位置はメモリ上で決まっているため、すぐにアクセスできるからです。

O(1)が実現できる理由

配列のようなインデックスでアクセスできるデータ構造では、コンピュータは直接その位置に飛んでいけます。これは本の目次から特定のページを開くのと似ています。何ページあっても、ページ番号さえわかれば一瞬で開けますよね。

O(n) 線形時間 - 最も基本的な処理

リンドくん

リンドくん

O(n)は、データがn個あればn回処理するってことですか?

たなべ

たなべ

その通り!例えば、配列のすべての要素を順番に見ていくような処理がこれだね。データが2倍になれば処理時間も2倍になるんだ。

O(n)の特徴

O(n)は線形時間と呼ばれ、入力サイズに比例して処理時間が増加します。プログラミングで最もよく見かける計算量の一つです。

以下のような処理がO(n)になります。

  • 配列のすべての要素をループで処理する
  • リストから特定の値を探す(線形探索)
  • 配列の合計値を計算する
  • 配列の最大値・最小値を見つける

O(n)のコード例

# 配列の合計を計算(O(n))
def calculate_sum(array):
    total = 0
    for number in array:  # n個の要素をすべて見る
        total += number
    return total

# 使用例
numbers = [1, 2, 3, 4, 5]
result = calculate_sum(numbers)  # 5個の要素 → 5回のループ

このコードでは、配列の要素数が増えれば増えるほど、ループの回数も比例して増加します。10個なら10回、100万個なら100万回のループが必要です。

O(n)の実用例

線形探索は、データ構造の中でも基本的な操作です。

# 配列から特定の値を探す(O(n))
def find_value(array, target):
    for i in range(len(array)):  # 最悪の場合、全要素を見る
        if array[i] == target:
            return i
    return -1  # 見つからなかった

# 使用例
numbers = [10, 20, 30, 40, 50]
index = find_value(numbers, 40)  # 最悪の場合5回のチェック

この線形探索では、最悪の場合(探している値が配列の最後にある、または存在しない場合)、すべての要素をチェックする必要があります。

O(n²) 二乗時間 - ネストしたループの危険性

リンドくん

リンドくん

O(n²)って、すごく遅そうですね...

たなべ

たなべ

そうなんだ。ループの中にループがあると、この計算量になることが多いんだよ。
10個のデータなら100回、100個なら10,000回の処理になっちゃうからね。大量のデータには向かないんだ。

O(n²)の特徴

O(n²)は二乗時間と呼ばれ、入力サイズの二乗に比例して処理時間が増加します。これは初心者が書きがちな、効率の悪いコードの典型例です。

以下のような処理がO(n²)になります。

  • 二重ループで配列のすべてのペアをチェックする
  • バブルソートや選択ソートなどの単純なソートアルゴリズム
  • 配列の中で重複する要素を探す(非効率的な方法)

O(n²)のコード例

# 配列内の重複を見つける(O(n²))
def find_duplicates(array):
    duplicates = []
    for i in range(len(array)):  # 外側のループ: n回
        for j in range(i + 1, len(array)):  # 内側のループ: 最大n回
            if array[i] == array[j]:
                duplicates.append(array[i])
    return duplicates

# 使用例
numbers = [1, 2, 3, 2, 4, 3, 5]
result = find_duplicates(numbers)  # 7個の要素 → 最大49回の比較

このコードでは、外側のループがn回、その内側でさらにn回ループするため、合計でn×n回の処理が発生します。

O(n²)の問題点

データ量が増えると、処理時間が急激に増加します。

  • 10個のデータ → 100回の処理
  • 100個のデータ → 10,000回の処理
  • 1,000個のデータ → 1,000,000回の処理
  • 10,000個のデータ → 100,000,000回の処理

このように、少しデータが増えるだけで処理時間が爆発的に増えてしまうのが、O(n²)の大きな問題点です。実際のアプリケーション開発では、できるだけO(n²)のアルゴリズムを避けることが重要になります。

より効率的な代替案

先ほどの重複検出は、集合(Set)を使うことでO(n)に改善できます。

# より効率的な重複検出(O(n))
def find_duplicates_efficient(array):
    seen = set()
    duplicates = set()
    for number in array:  # n回のループだけ
        if number in seen:  # SetのinはO(1)
            duplicates.add(number)
        seen.add(number)
    return list(duplicates)

# 使用例
numbers = [1, 2, 3, 2, 4, 3, 5]
result = find_duplicates_efficient(numbers)  # 7回の処理で完了

このように、適切なデータ構造を選ぶことで、計算量を大幅に改善できることがあります。

O(log n) 対数時間 - 分割統治

リンドくん

リンドくん

O(log n)って、どういう意味ですか?logって数学で習ったような...

たなべ

たなべ

そう、対数だね!簡単に言うと、データを半分ずつに分けていくような処理がこれに当たるんだ。1,000個のデータでも10回程度の処理で終わる、とても効率的な計算量なんだよ。

O(log n)の特徴

O(log n)は対数時間と呼ばれ、データ量が増えても処理時間がゆっくりとしか増えない、非常に効率的な計算量です。

代表的な例は二分探索(バイナリサーチ)です。これは、ソート済みの配列から値を探す際に、毎回対象範囲を半分に絞っていく手法です。

O(log n)のイメージ

辞書で単語を調べるときのことを思い出してください。

  1. 辞書を真ん中あたりで開く
  2. 探している単語がそれより前か後かを判断
  3. 該当する半分だけを残して、また真ん中で開く
  4. これを繰り返す

こうすることで、数万語ある辞書でも数回のチェックで目的の単語を見つけられます。これがO(log n)の考え方です。

O(log n)のコード例

# 二分探索(O(log n))
def binary_search(sorted_array, target):
    left = 0
    right = len(sorted_array) - 1
    
    while left <= right:
        mid = (left + right) // 2  # 中央の位置を計算
        
        if sorted_array[mid] == target:
            return mid  # 見つかった
        elif sorted_array[mid] < target:
            left = mid + 1  # 右半分を探索
        else:
            right = mid - 1  # 左半分を探索
    
    return -1  # 見つからなかった

# 使用例
numbers = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
index = binary_search(numbers, 13)  # 最大4回のチェックで見つかる

このコードでは、10個の要素がある配列でも、最大で4回程度のチェックで目的の値を見つけられます(log₂(10) ≈ 3.3)。

O(log n)の威力

データ量と必要な処理回数の関係を見てみましょう。

  • 10個のデータ → 約3-4回の処理
  • 100個のデータ → 約6-7回の処理
  • 1,000個のデータ → 約9-10回の処理
  • 1,000,000個のデータ → 約19-20回の処理

100万個のデータでも20回程度の処理で済むというのは驚異的な効率です。これがO(log n)の大きな強みです。

計算量の比較と選択基準

リンドくん

リンドくん

結局、どの計算量を目指せばいいんでしょうか?

たなべ

たなべ

それは扱うデータの量と、求められる速度によって変わってくるんだ。
少量のデータならO(n²)でも問題ないけど、大量のデータを扱うならO(n)やO(log n)を目指すべきだね。

各計算量の実用的な評価

データ量による処理回数の違いを比較してみましょう。

データ量O(1)O(log n)O(n)O(n log n)O(n²)
10131033100
1001710066410,000
1,0001101,0009,9661,000,000
10,00011310,000132,877100,000,000

この表を見ると、データ量が増えるにつれて、計算量の違いが処理時間に大きく影響することがわかります。

アルゴリズム選択の実践的なガイドライン

データ量が少ない場合(100件以下)
  • どの計算量でもほとんど差が出ない
  • コードの読みやすさを優先してもOK
データ量が中程度の場合(100~10,000件)
  • O(n²)は避けるべき
  • O(n)やO(n log n)を目指す
データ量が多い場合(10,000件以上)
  • O(n log n)以下を強く推奨
  • 可能な限りO(log n)やO(1)を目指す

よくある改善パターン

効率の悪いコードを改善する典型的なパターンは以下です。

パターン① ループをなくす

  • Before: O(n)のループで毎回計算
  • After: O(1)の配列アクセスに変更

パターン② 適切なデータ構造を使う

  • Before: リストでの検索O(n)
  • After: ハッシュテーブル(辞書/Set)での検索O(1)

パターン③ ソート済みデータを活用

  • Before: 線形探索O(n)
  • After: 二分探索O(log n)

パターン④ 二重ループを避ける

  • Before: O(n²)の二重ループ
  • After: ハッシュテーブルを使ってO(n)に改善

計算量を意識したコーディングの習慣

リンドくん

リンドくん

普段からどんなことに気をつければ、効率的なコードが書けるようになりますか?

たなべ

たなべ

いくつかポイントがあるんだけど、まずは「ループが何回回るか」を常に意識することかな。
あとは、適切なデータ構造を選ぶことも大事だよ。慣れてくると自然に効率的なコードが書けるようになるよ!

チェックポイント① ループの回数を数える

コードを書くとき、または読むときに、以下を意識しましょう。

  • 単一のループ → おそらくO(n)
  • 二重ループ → おそらくO(n²)、要注意
  • 半分ずつ絞り込むループ → おそらくO(log n)、効率的
# このコードの計算量は?
for i in range(n):  # O(n)
    for j in range(n):  # O(n)
        print(i, j)
# 答え: O(n²)

チェックポイント② 適切なデータ構造を選ぶ

用途に応じて最適なデータ構造は異なります。

  • リスト(配列) → 順序が重要、インデックスアクセスが多い
  • Set(集合) → 重複を許さない、高速な検索が必要
  • 辞書(ハッシュマップ) → キーと値のペア、高速な検索が必要
  • ソート済みリスト → 二分探索を使いたい
# 検索が多い場合はSetを使う
if target in my_set:  # O(1)
    print("Found!")

# リストだと遅い
if target in my_list:  # O(n)
    print("Found!")

チェックポイント③ 事前計算を活用する

同じ計算を何度も繰り返すのは無駄です。

# 悪い例: 毎回合計を計算(O(n²))
for i in range(len(array)):
    total = sum(array)  # O(n)の計算を毎回実行
    print(total)

# 良い例: 一度だけ計算(O(n))
total = sum(array)  # 一度だけO(n)
for i in range(len(array)):
    print(total)

チェックポイント④ 早期リターンを活用する

不要な処理は避けましょう。

# 良い例: 見つかったらすぐ返す
def find_item(array, target):
    for item in array:
        if item == target:
            return True  # 見つかったらすぐ終了
    return False

# 悪い例: 最後まで処理を続ける
def find_item_bad(array, target):
    found = False
    for item in array:
        if item == target:
            found = True  # 見つかっても続ける
    return found

まとめ

リンドくん

リンドくん

計算量のことがだいぶわかってきました!これからは効率的なコードを意識して書いてみます!

たなべ

たなべ

その意気だね!最初から完璧を目指す必要はないよ。
まずは動くコードを書いて、それから改善していくというアプローチで大丈夫。計算量を意識することで、確実にエンジニアとしてのレベルが上がっていくはずだよ!

Big-O記法とアルゴリズムの計算量について、基本的な内容を解説してきました。この知識は、プログラミング初心者からプロのエンジニアへと成長する過程で、必ず身につけるべき重要なスキルです。

今回学んだ重要なポイントを振り返りましょう。

  • 計算量はプログラムの効率性を測る指標 → データ量が増えたときの処理時間の増え方を表す
  • 主な計算量の種類 → O(1)、O(log n)、O(n)、O(n²)など、速い順に理解しよう
  • データ構造の選択が重要 → リスト、Set、辞書など、用途に応じた最適な選択を
  • 二重ループには要注意 → O(n²)になりやすく、大量データには不向き
  • 実践的な改善方法 → ループを減らす、適切なデータ構造を使う、事前計算を活用する

計算量の理解は、単なる理論知識ではありません。
実際のアプリケーション開発では、ユーザー体験を左右する重要な要素です。レスポンスの速いアプリケーションは、ユーザーからの評価も高くなります。

計算量を意識することは、最初は難しく感じるかもしれません。
しかし、コードを書くたびに「このループは何回回るかな?」「もっと効率的な方法はないかな?」と考える習慣をつけることで、自然と効率的なコードが書けるようになります。

プログラミングでは、「動くコード」から「良いコード」へ、そして「効率的なコード」へと成長していくことが大切です。

この記事をシェア

関連するコンテンツ