水平走査法(Horizontal Scanning)による O(S) 実装(S = 全文字列の合計文字数)
一言で言うと:「複数の文字列を先頭から見比べて、全員に共通する最長の接頭辞を取り出す問題」
💡 この問題を一言で言うと:「複数の文字列を先頭から見比べて、全員に共通する最長の接頭辞を取り出す問題」です。
文字列の配列 strs が与えられたとき、すべての文字列に共通する最長の接頭辞(先頭部分)を返します。 共通する接頭辞が1文字も無ければ、空文字列 "" を返します。
⚠️ なぜ単純な方法では解けないのか
入出力例
例1
strs = [flower, flow, flight]
出力: fl
なぜ正解か:3つの文字列すべてが f と l で始まっており、3文字目でそれぞれ o / o / i と分かれるため、 共通しているのは先頭2文字の fl までです。
例2
strs = [dog, racecar, car]
出力: (空文字列)
なぜ正解か:dog, racecar, car はそもそも先頭の1文字目からすべて異なる(d, r, c)ため、 共通する接頭辞は1文字も存在しません。
代表例 strs = [flower, flow, flight] を使って、水平走査アルゴリズムがどのように動くかを1ステップずつ確認しましょう。 左のリストからステップを選ぶか、「再生」ボタンで自動的に進めることができます。
同じアルゴリズムでも、言語によって書き方や最適化の方向性が変わります。 タブを切り替えて、4言語それぞれの実装・動作トレース・言語特有のポイントを見比べてみましょう。
📖 Big-O 記法の読み方
| 言語 | 採用アルゴリズム | 時間計算量 | 空間計算量 | 候補の縮め方 |
|---|---|---|---|---|
| Python | 垂直走査(zip + set) | O(S) | O(1) | 一致した文字をリストに集め join で結合 |
| TypeScript | 水平走査(startsWith) | O(S) | O(1) | slice で新しい文字列に再代入 |
| Go | 水平走査(strings.HasPrefix) | O(S) | O(1) | スライス式でコピーせず範囲だけ変更 |
| Rust | 水平走査(starts_with) | O(S) | O(1) | usize の長さのみ更新し文字列は最後に1回だけ生成 |
🔍 なぜこの計算量になるのか
どの言語の実装も、最悪の場合で全文字列の全文字を最大1回ずつ調べる構造になっているため、時間計算量は O(S)(Sは全文字列の合計文字数)です。 空間計算量については、候補の文字列(または候補の長さを表す整数)以外に追加のデータ構造を使わないため、出力用の文字列を除けば O(1) になります。
同じアルゴリズムでも、言語ごとに「メモリの扱い方」や「エラーの表現方法」が異なります。 ここでは4言語の実装を横並びで比較し、それぞれの言語特有の設計判断を振り返ります。
| 観点 | Python | TypeScript | Go | Rust |
|---|---|---|---|---|
| メモリ確保の特徴 | join で最後に1回だけ結合 | slice のたびに新しい文字列 | スライス式はコピーなし | 整数更新のみ、最後に1回だけ確保 |
| エラー表現 | TypeError / ValueError の例外 | TypeError / RangeError の例外 | error 戻り値(提出版は省略) | Result型が定石だが戻り値固定のため空文字列で表現 |
| メモリ管理方式 | GC(ガベージコレクション) | GC(ガベージコレクション) | GC(ガベージコレクション) | 所有権システム(GC不要) |
| 型安全性の担保方法 | 型ヒント + pylance | readonly + 型ガード | 明示的な型宣言 + go vet | 所有権システム + コンパイラチェック |
相違点の背景
Python・TypeScript・GoはGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)付きの言語であるため、 文字列の縮小操作で新しいオブジェクトが生成されてもGCが後始末をしてくれます。 一方Rustにはガベージコレクタが無く、コンパイル時に所有権を追跡する仕組みがあるため、 「新しい文字列を作らず整数だけを更新する」という、より低レベルなメモリ効率を意識した設計が自然に選ばれます。
このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。
List[str]
のように書き、pylance(VSCodeの型チェッカー)が実行前に型の誤りを検出できるようにします。
&str
のように書くことで、Vec<String>
の所有権を保持したまま中身を安全に読み取れます。
s[:i]
や Rust の
&s[..n]
がこれにあたります。