Skip to content

Commit 783c538

Browse files
committed
leetcode 28. Find the Index of the First Occurrence in a String KMP(Knuth-Morris-Pratt)法
1 parent 4c6d241 commit 783c538

7 files changed

Lines changed: 1133 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// 以下に、`Node.js 18.16.1` 環境で実行可能な `.indexOf()` を使ったシンプルで効率的な **JavaScript(関数形式)** の実装を提示します。
2+
3+
// ---
4+
5+
// ## ✅ 実装コード(JavaScript + `.indexOf()`)
6+
7+
// ```javascript
8+
/**
9+
* haystack から needle を検索し、最初に一致するインデックスを返す。
10+
* 一致しない場合は -1 を返す。
11+
*
12+
* @param {string} haystack - 探索対象の文字列(最大長 10^4)
13+
* @param {string} needle - 探索する部分文字列(最大長 10^4)
14+
* @returns {number} - needle の最初の出現インデックス、存在しない場合は -1
15+
*
16+
* 時間計算量: O(n * m) 最悪ケース(平均は高速)
17+
* 空間計算量: O(1)
18+
*/
19+
function strStrJs(haystack, needle) {
20+
return haystack.indexOf(needle);
21+
}
22+
23+
// ## 🧪 テストケース例
24+
25+
// ```javascript
26+
// console.log(strStr("sadbutsad", "sad")); // => 0
27+
// console.log(strStr("leetcode", "leeto")); // => -1
28+
// console.log(strStr("mississippi", "issip")); // => 4
29+
// console.log(strStr("abc", "")); // => 0
30+
// ```
31+
32+
// ---
33+
34+
// ## 🔍 計算量と実行パフォーマンスの評価
35+
36+
// | 項目 | 値 |
37+
// | --------------- | --------------------------- |
38+
// | **時間計算量** | 平均: `O(n)` / 最悪: `O(n * m)` |
39+
// | **空間計算量** | `O(1)`(追加メモリなし) |
40+
// | **Node.js実行環境** | `v18.16.1`(ECMAScript仕様準拠) |
41+
// | **最大長制約** | 10⁴(LeetCode制限内) |
42+
43+
// ---
44+
45+
// ## ✅ なぜ `.indexOf()` は便利か?
46+
47+
// * ネイティブ実装のため **高速な最適化** がされている。
48+
// * 内部的には **Boyer-Moore** や **KMPのような手法**を使うこともあり、**平均ケースで高速**。
49+
// * コードが非常に短く保守性が高い。
50+
51+
// ---
52+
53+
// ## ❗️注意点
54+
55+
// * 最悪ケースでは `O(n * m)` の比較が発生することがあります。
56+
57+
// * 例: `haystack = "aaaaaaaab"`, `needle = "aaaab"` のようなケース
58+
59+
// ---
60+
61+
// ご希望があれば、`.indexOf()` の内部アルゴリズムや他手法(Rabin-Karp など)との速度比較も可能です。どうぞお申し付けください。
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# 以下に、`Python (CPython 3.11.4)` を使用し、LeetCode形式に準拠した `class Solution` を用いた解答を示します。
2+
3+
# 処理効率を重視し、**KMP(Knuth-Morris-Pratt)法**を用いて、`needle`の最初の出現インデックスを効率よく探索します。
4+
5+
# ---
6+
7+
# ### ✅ 実装コード(KMP法使用)
8+
9+
# ```python
10+
from typing import List
11+
12+
class Solution:
13+
def strStr(self, haystack: str, needle: str) -> int:
14+
"""
15+
haystack: str - 探索対象の文字列
16+
needle: str - 探索する部分文字列
17+
return: int - needleの最初の出現インデックス。見つからない場合は-1。
18+
時間計算量: O(n + m), 空間計算量: O(m)
19+
(n = haystackの長さ, m = needleの長さ)
20+
"""
21+
22+
def build_lps(pattern: str) -> List[int]:
23+
"""部分一致テーブル(LPS配列)を構築する"""
24+
lps = [0] * len(pattern)
25+
length = 0 # 前のLPS長
26+
i = 1
27+
28+
while i < len(pattern):
29+
if pattern[i] == pattern[length]:
30+
length += 1
31+
lps[i] = length
32+
i += 1
33+
else:
34+
if length != 0:
35+
length = lps[length - 1]
36+
else:
37+
lps[i] = 0
38+
i += 1
39+
return lps
40+
41+
if not needle:
42+
return 0 # 空文字列は0番目に見つかったとみなす
43+
44+
lps = build_lps(needle)
45+
i = j = 0 # iはhaystack用, jはneedle用
46+
47+
while i < len(haystack):
48+
if haystack[i] == needle[j]:
49+
i += 1
50+
j += 1
51+
if j == len(needle):
52+
return i - j
53+
else:
54+
if j != 0:
55+
j = lps[j - 1]
56+
else:
57+
i += 1
58+
59+
return -1 # 見つからなかった場合
60+
# ```
61+
62+
# ---
63+
64+
# ### 🔍 処理時間・メモリ計算量
65+
66+
# | 項目 | 計算量 |
67+
# | ------ | ------------------------------ |
68+
# | 時間計算量 | `O(n + m)` |
69+
# | 空間計算量 | `O(m)`(LPS配列の構築) |
70+
# | 最悪ケース例 | haystack: 10⁴文字, needle: 10⁴文字 |
71+
72+
# ---
73+
74+
# ### ✅ テストケース例
75+
76+
# ```python
77+
# s = Solution()
78+
# print(s.strStr("sadbutsad", "sad")) # 出力: 0
79+
# print(s.strStr("leetcode", "leeto")) # 出力: -1
80+
# print(s.strStr("aaaabaaabaaac", "abaaac"))# 出力: 7
81+
# print(s.strStr("abc", "")) # 出力: 0(空needle)
82+
# ```
83+
84+
# ---
85+
86+
# ### ✅ 備考
87+
88+
# * Pythonの組み込み関数 `haystack.find(needle)` を使えばワンライナーでも同様の結果が得られますが、KMP法はアルゴリズム的理解と応用力の訓練に適しています。
89+
# * LPS(Longest Prefix which is also Suffix)は、部分一致の再利用を可能にし、不要な比較を省きます。
90+
91+
# ---
92+
93+
# 必要があれば **図解付き**でLPS配列やKMPステップの説明も可能です。希望があればお知らせください。
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// 以下に、\*\*TypeScript 5.1(Node.js 18.16.1)\*\*で `needle` を `haystack` の中から最初に探す関数を、**KMP(Knuth-Morris-Pratt)法**を用いて効率的に実装したものを提示します。
2+
3+
// クラスは使用せず、**関数ベース**で構成されています。
4+
5+
// ---
6+
7+
// ## ✅ 実装(TypeScript・関数形式)
8+
9+
// ```ts
10+
/**
11+
* haystack から needle を検索し、最初に一致するインデックスを返す。
12+
* 一致しない場合は -1 を返す。
13+
*
14+
* @param haystack - 探索対象の文字列(長さ最大10^4)
15+
* @param needle - 探索する部分文字列(長さ最大10^4)
16+
* @returns 最初に一致した位置のインデックス、または -1
17+
*
18+
* 時間計算量: O(n + m)
19+
* 空間計算量: O(m)
20+
*/
21+
function strStr(haystack: string, needle: string): number {
22+
if (needle.length === 0) return 0;
23+
24+
// KMP: LPS(Longest Prefix Suffix)配列の構築
25+
function buildLPS(pattern: string): number[] {
26+
const lps: number[] = new Array(pattern.length).fill(0);
27+
let length = 0;
28+
let i = 1;
29+
30+
while (i < pattern.length) {
31+
if (pattern[i] === pattern[length]) {
32+
length++;
33+
lps[i] = length;
34+
i++;
35+
} else {
36+
if (length !== 0) {
37+
length = lps[length - 1]; // 戻って再試行
38+
} else {
39+
lps[i] = 0;
40+
i++;
41+
}
42+
}
43+
}
44+
return lps;
45+
}
46+
47+
const lps = buildLPS(needle);
48+
let i = 0; // haystack index
49+
let j = 0; // needle index
50+
51+
while (i < haystack.length) {
52+
if (haystack[i] === needle[j]) {
53+
i++;
54+
j++;
55+
if (j === needle.length) {
56+
return i - j; // 完全一致発見
57+
}
58+
} else {
59+
if (j !== 0) {
60+
j = lps[j - 1]; // needle をスライド
61+
} else {
62+
i++;
63+
}
64+
}
65+
}
66+
67+
return -1; // 一致せず
68+
}
69+
// ```
70+
71+
// ---
72+
73+
// ## 📦 テストケース例
74+
75+
// ```ts
76+
// console.log(strStr("sadbutsad", "sad")); // => 0
77+
// console.log(strStr("leetcode", "leeto")); // => -1
78+
// console.log(strStr("mississippi", "issip")); // => 4
79+
// console.log(strStr("abc", "")); // => 0
80+
// ```
81+
82+
// ---
83+
84+
// ## 🧠 処理時間・メモリ消費量の考察
85+
86+
// | 項目 | 値 |
87+
// | ----- | -------------------------------- |
88+
// | 時間計算量 | `O(n + m)` (KMP法) |
89+
// | 空間計算量 | `O(m)`(LPS配列) |
90+
// | 実行環境 | TypeScript 5.1 + Node.js 18.16.1 |
91+
// | 最大文字数 | 10⁴(問題制約) |
92+
93+
// この方法は、最悪ケースでも `haystack` と `needle` の長さの合計だけで済むため、**TLE(Time Limit Exceeded)を回避しやすい**です。
94+
95+
// ---
96+
97+
// ## ✅ 補足
98+
99+
// * TypeScriptでは `.indexOf()` を使えば O(n \* m) になる可能性があるため、KMP法がパフォーマンス的に優れています。
100+
// * クラスベースでの設計が不要な場合、競技プログラミングやLeetCodeの制約にも適しています。
101+
102+
// ---
103+
104+
// 他のアルゴリズム(Rabin-Karp など)や `.indexOf` ベースもご希望であれば比較提示可能です。必要であればお知らせください。
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
では、先ほどの `strStr` 関数(KMP法による文字列探索)の**各処理ステップを図解とともに具体的に解析・説明**します。
2+
3+
---
4+
5+
## 🔍 **問題概要**
6+
7+
* `haystack` から `needle`**最初に現れる位置(インデックス)** を返す。
8+
* 見つからなければ `-1` を返す。
9+
10+
例:
11+
12+
```
13+
haystack = "sadbutsad"
14+
needle = "sad"
15+
出力: 0
16+
```
17+
18+
---
19+
20+
## **KMP法 全体の流れ**
21+
22+
### ステップ1: `needle` から **LPS配列(部分一致テーブル)** を作る
23+
24+
### ステップ2: `haystack` を走査し、`needle` とマッチングする
25+
26+
---
27+
28+
## 📘 ステップ1: LPS 配列の構築
29+
30+
### ✅ 目的:
31+
32+
* パターンの途中で不一致が起きたとき、比較を**最初からやり直さずに済む**ようにする
33+
* LPS = `Longest Prefix which is also Suffix`
34+
35+
---
36+
37+
### 🎯 例:
38+
39+
```text
40+
needle = "ababc"
41+
```
42+
43+
#### LPS 配列の構築ステップ
44+
45+
| i | needle\[0\:i] | LPS\[i] | 説明 |
46+
| - | ------------- | ------- | ----------------------------------------------- |
47+
| 0 | "a" | 0 | 最初の文字なので LPS=0 |
48+
| 1 | "ab" | 0 | "a" ≠ "b" なので 0 |
49+
| 2 | "aba" | 1 | "a" で prefix=suffix -> 長さ1 |
50+
| 3 | "abab" | 2 | "ab" で prefix=suffix -> 長さ2 |
51+
| 4 | "ababc" | 0 | "c" ≠ "a", "c" ≠ "b" -> 一致するprefix/suffix無し → 0 |
52+
53+
```
54+
最終LPS = [0, 0, 1, 2, 0]
55+
```
56+
57+
📌 **この配列は失敗時にどこまで戻ればよいかを示すテーブル**です。
58+
59+
---
60+
61+
## 📘 ステップ2: `haystack` の走査
62+
63+
### ✅ 目的:
64+
65+
* LPS配列を使って `haystack` を効率よく走査
66+
* 一致しない時に `needle` のどこから再比較すればよいかを判断する
67+
68+
---
69+
70+
### 🎯 例:
71+
72+
```text
73+
haystack = "abxabcabcaby"
74+
needle = "abcaby"
75+
```
76+
77+
#### LPS(事前計算済み):
78+
79+
```
80+
needle = "a b c a b y"
81+
LPS = [0 0 0 1 2 0]
82+
```
83+
84+
---
85+
86+
### 🔄 マッチング処理ステップ(図で追跡):
87+
88+
```text
89+
haystack: a b x a b c a b c a b y
90+
needle : a b c a b y
91+
92+
```
93+
94+
#### 比較の様子:
95+
96+
| i (haystack) | j (needle) | 一致? | 説明 |
97+
| ------------ | ---------- | --- | ----------------------------- |
98+
| 0 | 0 || 'a'=='a' → i++, j++ |
99+
| 1 | 1 || 'b'=='b' → i++, j++ |
100+
| 2 | 2 || 'x' ≠ 'c' → j = LPS\[j-1] = 0 |
101+
| 2 | 0 || 'x' ≠ 'a' → i++, j stays at 0 |
102+
| ... | ... | | 同様の比較とジャンプが続く |
103+
| 6 | 0\~5 || すべて一致 → i=12, j=6(完了) |
104+
| 出力: | | | i - j = 12 - 6 = 6 が解 |
105+
106+
---
107+
108+
## 🎯 ビジュアル図(イメージ)
109+
110+
```
111+
haystack: a b x a b c a b c a b y
112+
↑↑↑↑↑↑ ← ここでneedle完全一致(6文字)
113+
114+
needle : a b c a b y
115+
```
116+
117+
---
118+
119+
## 🧠 KMP法が効率的な理由
120+
121+
* **部分一致までの情報をLPSに持っている**ので、無駄に戻らない
122+
* 文字列長が最大1万でも `O(n + m)` で探索できる(`find`より効率よい場合あり)
123+
124+
---
125+
126+
## ✅ 補足まとめ
127+
128+
| ステップ | 内容 | 時間計算量 | 空間計算量 |
129+
| ---------- | --------------- | ------------ | ----- |
130+
| LPS構築 | 部分一致テーブルの準備 | O(m) | O(m) |
131+
| haystack走査 | LPSを用いて文字列マッチング | O(n) | O(1) |
132+
| 合計 | | **O(n + m)** | O(m) |
133+
134+
---
135+
136+
ご希望であれば「比較の様子を表に拡張」や「より長い文字列の可視化」も可能です。お気軽にどうぞ!

0 commit comments

Comments
 (0)