JavaScriptで不要な配列を作ってしまう初心者の罠:中間配列アンチパターンを克服した話

Paiza

こんにちは、いーさんです。

今回は、paizaで同じ間違いを2回も繰り返してしまった経験から学んだ「中間配列アンチパターン」について書きます。

プログラミング初心者が陥りやすい罠で、私もまさにハマってしまいました。特にPython経験者がJavaScriptを学ぶときに起こりやすいパターンなので、同じような背景を持つ方の参考になれば嬉しいです。

何度も繰り返した同じ間違い

paizaの「文字列の最初の出現位置検索」という問題を解いていたとき、テストケースの一部は通るのに、大量データになるとエラーが出る現象に遭遇しました。

そして、Python の正解コードと自分のJavaScriptコードを見比べて、やっと問題に気づきました。

それが「不要な中間配列」でした。

間違ったコード

まず、私が書いてしまった問題のあるコードがこちらです。

reader.on('close', () => {
  const [N, Q] = lines[0].split(" ").map(Number);
  let s = {};
  let t = [];  // ← 問題:不要な中間配列
  
  // S配列をオブジェクトに格納
  for(let i = 0; i < N; i++){
      let S = lines[i+1];
      if (!(S in s)){
          s[S] = i + 1;
      }
  }
  
  // ❌ 一旦配列に格納
  for(let j = 0; j < Q; j++){
      t.push(lines[N + 1 + j]);
  }
  
  // ❌ 別のループで処理
  for(let x of t){
      if(x in s){
          console.log(s[x]);
      } else {
          console.log(-1);
      }
  }
});

一見問題なさそうですが、このコードには重大な無駄があります。

何が問題だったのか

問題点1:不要な中間配列 t

let t = [];  // これ、本当に必要?
for(let j = 0; j < Q; j++){
    t.push(lines[N + 1 + j]);
}

lines 配列にすでにデータが全部入っているのに、わざわざ別の配列 t に移し替えています。

これが完全に無駄でした。

問題点2:ループが2回に分かれている

// 1回目のループ:配列に格納
for(let j = 0; j < Q; j++){
    t.push(lines[N + 1 + j]);
}

// 2回目のループ:処理
for(let x of t){
    // 処理
}

ループを2回回す必要がありません。1回のループで処理できます。

問題点3:余分なメモリ使用

中間配列 t を作ることで:

  • 時間計算量: O(Q) + O(Q) = O(Q)(変わらない)
  • 空間計算量: O(Q)(余分なメモリ使用)

小さいデータなら問題になりませんが、大量データになるとメモリを圧迫します。

正しいコード

修正後のコードがこちらです。

reader.on('close', () => {
  const [N, Q] = lines[0].split(" ").map(Number);
  const s = {};
  
  // S配列をオブジェクトに格納
  for(let i = 0; i < N; i++){
      const S = lines[i + 1];
      if (!(S in s)){
          s[S] = i + 1;
      }
  }
  
  // ✅ クエリを直接処理
  for(let j = 0; j < Q; j++){
      const t = lines[N + 1 + j];
      if(t in s){
          console.log(s[t]);
      } else {
          console.log(-1);
      }
  }
});

シンプルで分かりやすくなりました。

なぜこの間違いをしてしまったのか

振り返ってみると、3つの心理的要因がありました。

1. 「一旦まとめて取得したい」衝動

データを処理する前に、「まずデータを全部集めよう」と考えてしまいました。

これは一見論理的に思えますが、実は不要なステップです。

2. Pythonの input() との違いに引きずられた

Pythonでは input() で1行ずつ読み込みますが、JavaScriptの paiza環境では lines 配列に全データが最初から入っています。

この違いを理解していなかったため、「データを取り出して別の場所に保存する」という発想になってしまいました。

3. 処理を段階的に分けたい気持ち

「データ取得」と「データ処理」を分けた方が分かりやすい気がしていました。

しかし実際は、分ける必要がない場面で分けることで、かえってコードが複雑になっていました。

Python vs JavaScript の処理の違い

Python の正解コードと比較してみます。

Python(参考)

N, Q = map(int, input().split())

S = {}
for i in range(N):
    s = input()
    if s not in S:
        S[s] = i + 1

# ✅ 直接処理
for _ in range(Q):
    t = input()
    if t in S:
        print(S[t])
    else:
        print(-1)

Pythonでは input() でその都度読み込んで、すぐに処理しています。

JavaScript(間違いパターン)

// ❌ 中間配列を作ってしまう
for(let j = 0; j < Q; j++){
    t.push(lines[N + 1 + j]);  // 一旦格納
}
for(let x of t){  // 別ループで処理
    console.log(...);
}

JavaScript(正解パターン)

// ✅ Pythonと同じように直接処理
for(let j = 0; j < Q; j++){
    const t = lines[N + 1 + j];
    console.log(...);
}

学び: JavaScriptでも、Pythonと同じように「取得→即処理」のパターンで書くべきでした

中間配列アンチパターンとは

この失敗から、「中間配列アンチパターン」という概念を学びました。

❌ アンチパターン(避けるべき)

// パターン1: 不要な中間配列
const temp = [];
for(let i = 0; i < N; i++){
    temp.push(lines[i]);
}
for(let x of temp){
    process(x);
}

// パターン2: 配列に格納してから検索
const list = [];
for(let i = 0; i < N; i++){
    list.push(lines[i]);
}
for(let item of list){
    console.log(search(item));
}

✅ 推奨パターン

// パターン1: 直接処理
for(let i = 0; i < N; i++){
    const x = lines[i];
    process(x);
}

// パターン2: その場で検索
for(let i = 0; i < N; i++){
    const item = lines[i];
    console.log(search(item));
}

いつ中間配列が必要なのか

ただし、中間配列が本当に必要な場面もあります。

中間配列が必要なケース

1. ソートが必要な場合

const data = [];
for(let i = 0; i < N; i++){
    data.push(lines[i]);
}
data.sort((a, b) => a - b);  // ソート必須

2. フィルタリングが必要な場合

const filtered = [];
for(let i = 0; i < N; i++){
    if(条件){
        filtered.push(lines[i]);
    }
}

3. データを変換してから使う場合

const transformed = lines.map(Number);  // 文字列→数値変換

中間配列が不要なケース

1. 単純な順次処理

// ❌ 不要
const temp = [];
for(let i = 0; i < N; i++){
    temp.push(lines[i]);
}
for(let x of temp){
    console.log(x);
}

// ✅ 直接処理
for(let i = 0; i < N; i++){
    console.log(lines[i]);
}

2. 検索・照合処理

// ❌ 不要
const queries = [];
for(let i = 0; i &lt; Q; i++){
    queries.push(lines[i]);
}
for(let q of queries){
    console.log(map[q]);
}

// ✅ 直接処理
for(let i = 0; i &lt; Q; i++){
    const q = lines[i];
    console.log(map[q]);
}

チェックリスト:中間配列を作る前に確認

let array = [];
for(...){
    array.push(...);
}
for(... of array){  // ← これが見つかったら要注意
    ...
}

学んだこと

1. 「一旦配列に格納」は要注意フレーズ

「一旦」「とりあえず」という言葉が出てきたら、本当に必要か立ち止まって考える。

2. 同じ間違いを繰り返すことでパターン認識

1回目:指摘されて直した
2回目:また同じ間違い
→ これでやっとパターンとして認識できた

3. Python経験が逆に足を引っ張ることもある

異なる言語の特性を理解せず、思考パターンをそのまま移植すると失敗する。

4. コードレビューの重要性

Python解答と比較することで、初めて違いに気づけた。 他の人のコードを見ることは学びになる。

まとめ

今回学んだポイント:

中間配列アンチパターンを認識
「一旦配列に格納」は要注意
Python と JavaScript の違いを理解
本当に必要な配列かを考える癖をつける

同じ間違いを2回繰り返したのは悔しいですが、これでパターンとして完全に理解できました。

次に同じ状況になったら、絶対に中間配列は作りません!

プログラミング学習では、こうした失敗から学ぶことが一番身につくと実感しています。


今回のアンチパターン:

// ❌ 避ける
let temp = [];
for(...){ temp.push(...); }
for(...){ process(temp); }

// ✅ 推奨
for(...){
    const x = data[i];
    process(x);
}

次回もpaizaでの学びをアウトプットしていきます!

関連記事:


この記事は paizaラーニング の学習記録です。

コメント

タイトルとURLをコピーしました