配列と文字列 — 二つのポインタ・尺取り法・累積和
この章の目次開く
Part 2はデータ構造編です。初回は最頻出の配列と文字列。paizaでもAtCoderのA〜C問題でも、出題の半分以上は配列の処理と言っても過言ではありません。
この章のゴールは、素朴に書くとO(N²)になる処理を、3つの道具でO(N)に落とせるようになることです。
- 二つのポインタ(two pointers)
- 尺取り法(スライディングウィンドウ)
- 累積和
前提: TypeScriptの配列操作おさらい
コーディングテストで多用する操作を、計算量つきで整理しておきます。
const arr = [3, 1, 4, 1, 5];
arr.push(9); // 末尾に追加 O(1)
arr.pop(); // 末尾を削除 O(1)
arr.sort((a, b) => a - b); // 昇順ソート O(N log N) ※比較関数を忘れない
arr.reduce((s, v) => s + v, 0); // 合計 O(N)
Math.max(...arr); // 最大値 O(N) ※要素数が多すぎるとスタックエラー注意
const s = "hello";
s.split(''); // 1文字ずつの配列に O(N)
s.charCodeAt(0); // 文字コード('a'は97)
[...s].reverse().join(''); // 反転sort() に比較関数を渡さないと辞書順([10, 2, 1] → [1, 10, 2])になるのは、TypeScript勢が最初に踏む罠です。数値のソートは必ず (a, b) => a - b を付けてください。
パターン1: 二つのポインタ
ソート済み配列の両端から、2つの添字を内側に詰めていくテクニックです。定番問題で見てみます。
問題: 昇順ソート済みの配列から、合計がちょうどXになる2要素のペアがあるか判定せよ。(N ≤ 10^5)
制約から2重ループは不可(第2章)。ソート済みという条件を活かします。
function hasPairSum(sorted: number[], x: number): boolean {
let left = 0;
let right = sorted.length - 1;
while (left < right) {
const sum = sorted[left] + sorted[right];
if (sum === x) return true;
if (sum < x) left++; // 合計が足りない → 小さい方を進めて増やす
else right--; // 合計が大きすぎる → 大きい方を戻して減らす
}
return false;
}
学習者これ、どうして候補を見逃さないの?leftを進めた時点で、そのleftと組むはずだった相手を飛ばしちゃう気がするんだけど。
いい疑問です。sum < x でleftを進めるとき、いまのleftは最大の相手(right)と組んでも足りなかったわけです。もっと小さい相手と組んだらさらに足りません。つまり「いまのleftは誰と組んでもダメ」と確定してから捨てている——だから見逃しがないんです。両ポインタは合計N回しか動かないのでO(N)です。
パターン2: 尺取り法(スライディングウィンドウ)
**連続する区間(部分配列)**に関する問題の主役です。窓の右端を伸ばし、条件を壊したら左端を縮める——尺取り虫のような動きをします。
問題: 正の整数の配列で、合計がK以下になる連続部分配列の最大の長さを求めよ。(N ≤ 10^5)
function maxLength(nums: number[], k: number): number {
let left = 0;
let sum = 0;
let best = 0;
for (let right = 0; right < nums.length; right++) {
sum += nums[right]; // 窓を右に広げる
while (sum > k) { // 条件が壊れたら
sum -= nums[left]; // 左から縮める
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}「区間の全列挙はO(N²)ある。でも各要素は窓に入るのが1回、出るのが1回だけだからO(N)」——これが尺取り法の計算量の理屈です。

適用条件は「窓を広げると条件が悪化し、縮めると改善する」という単調性があること。要素に負の数が混ざると単調性が崩れて使えないので、その場合は次の累積和の出番です。
パターン3: 累積和
「区間の合計」を何度も聞かれる問題の決定版です。
問題: 長さNの配列に対して、Q個のクエリ「l番目からr番目までの合計は?」に答えよ。(N, Q ≤ 10^5)
毎回ループで足すとO(NQ) = 10^10でTLE。そこで前処理をします。
// prefix[i] = 先頭からi個の合計(prefix[0] = 0)
const prefix = new Array(nums.length + 1).fill(0);
for (let i = 0; i < nums.length; i++) {
prefix[i + 1] = prefix[i] + nums[i];
}
// 区間 [l, r](0-indexed・両端含む)の合計はO(1)で出る
const rangeSum = (l: number, r: number) => prefix[r + 1] - prefix[l];前処理O(N)を1回やれば、各クエリはO(1)。合計O(N + Q)です。「区間の合計=2つの累積の差」という発想は、2次元(グリッドの矩形合計)にも拡張できる汎用テクニックです。
文字列の頻出処理
文字列問題も、実体は「文字の配列」として同じパターンで解けます。加えて覚えておくと速いのが出現回数カウントです。
// 各文字の出現回数を数える(アナグラム判定などの基本部品)
function countChars(s: string): Map<string, number> {
const count = new Map<string, number>();
for (const ch of s) {
count.set(ch, (count.get(ch) ?? 0) + 1);
}
return count;
}「アナグラム判定」「最頻出文字」「k種類以下の文字からなる最長区間(尺取り法との合わせ技)」など、カウント+Mapは文字列問題の基本部品です。Mapの詳細は次章で扱います。
この章のパターンの見分け方
| 問題文のシグナル | 使うパターン |
|---|---|
| 「ソート済み配列で、条件を満たすペア」 | 二つのポインタ |
| 「条件を満たす連続部分配列の最長/最短/個数」 | 尺取り法 |
| 「区間の合計を何度も」「部分配列の和」 | 累積和 |
| 「アナグラム」「文字の種類・回数」 | カウント + Map |
メンター「連続する」という単語が出たら尺取り法か累積和、と反応できるようになるのが第一目標です。この「単語→パターン」の反射は第12章で総仕上げしますが、各章の最後のこの表を毎回意識しておくと定着が早いですよ。
まとめ
- 数値ソートは
(a, b) => a - bを忘れない。shiftやincludesのループ内使用は隠れO(N²)(第2章) - 二つのポインタ: ソート済み配列の両端から詰める。「もうダメと確定した側を捨てる」からO(N)
- 尺取り法: 連続区間の問題で、右で伸ばし左で縮める。単調性が適用条件
- 累積和: 区間合計の繰り返しクエリを「前処理O(N) + クエリO(1)」に変える
- 問題文の「連続する」「区間」「ペア」がパターン選択のシグナル
次章は、第2章の解法3でも登場した「探索をO(1)にする」道具——ハッシュマップとSetを本格的に使いこなします。