擬似言語の配列への挿入と削除がわからない人へ|要素をずらす処理の読み方を解説
配列の途中に値を入れる処理で、どこをどうずらすのか分からなくなる。
for の中の添字が i なのか i + 1 なのかで、迷子になったことはありませんか?
削除のときは前から回すのに、挿入のときは後ろから回す。理由を知らないまま覚えようとすると、本番で必ず取り違えます。
でも大丈夫です。理由さえ分かれば、どちらの向きに回すかは自分で導き出せます。
私はエンジニアとして10年以上、Webシステムの開発や運用に携わってきました。実務でも、一覧の途中に行を差し込む処理で一つ分ずれてしまい、隣のデータが上書きされる不具合に付き合ったことがあります。
配列の要素をずらす処理は、地味ですが事故が起きやすい場所です。だからこそ、科目Bでも繰り返し問われます。
この記事では、擬似言語の配列に要素を挿入する処理と削除する処理を、動きを追いながら組み立てていきます。
配列は長さが変わらない入れ物¶
はじめに、なぜずらす作業が必要になるのかを押さえましょう。配列は、あらかじめ決めた数の箱が横一列に並んだものです。
この箱の数は、途中で増えたり減ったりしません。ここが出発点になります。
だから配列の途中に新しい値を入れたいとき、箱を一つ挿し込むことはできません。すでにある値を後ろへずらして、空いた箱に書き込むしかないのです。
削除も同じ発想です。真ん中の値を消しても箱は残るので、後ろの値を前へ引き寄せて隙間を埋めます。
詰めないと何が困るのか¶
消した場所をそのままにしてはいけないのでしょうか。空いたままだと、その配列を後で使う処理が困ります。
たとえば合計を求めるループは、先頭から件数分だけ順に足していきます。途中に無効な箱が混ざっていると、その値まで足してしまうでしょう。
集計の基本形があいまいなままだと、この影響が見えにくくなります。心当たりがあれば、こちらで土台を固めておいてください。
【関連記事】擬似言語の合計・最大値・件数の求め方|科目Bで頻出の集計パターンを解説
有効な値が先頭から隙間なく並んでいる。この状態を保つために、わざわざずらす処理を書いているわけです。
有効な件数を別の変数で持つ¶
もう一つ、大事な考え方があります。配列の箱の数と、実際に入っている有効なデータの件数は別物だという点です。
箱が10個あっても、有効なデータは5件かもしれません。そこで、件数を入れておく変数を別に用意します。
削除したら件数を1減らし、挿入したら1増やす。この更新を忘れると、消したはずの値が最後にもう一つ残って見えます。
科目Bの問題では、この件数を表す変数が空欄になっていることがよくあります。配列本体だけでなく、件数の動きにも目を配ってください。
削除は前から後ろへ回して詰める¶
ここから実際のコードを見ていきます。まずは削除からです。
data という配列に5件のデータが入っているとします。2番目の値を消して、後ろを前に詰める処理です。
○整数型: i, 件数, 位置
○整数型の配列: data ← {10, 20, 30, 40, 50}
件数 ← 5
位置 ← 2 /* data[2] を削除する */
for (i を 位置 から 件数 - 1 まで 1 ずつ増やす)
data[i] ← data[i + 1]
endfor
件数 ← 件数 - 1
ループの中身は一行だけです。data[i] に、その一つ後ろの data[i + 1] を持ってきています。
これで、後ろの値が一つずつ前へ移ってきます。位置から始めているので、消したい場所が最初に上書きされる形です。
一行ずつ値を追ってみる¶
言葉だけでは動きがつかみにくいところでしょう。i がどう進み、配列がどう変わるかを表にします。
| i の値 | 実行する代入 | 実行後の data |
|---|---|---|
| 開始前 | ― | 10, 20, 30, 40, 50 |
| 2 | data[2] ← data[3] | 10, 30, 30, 40, 50 |
| 3 | data[3] ← data[4] | 10, 30, 40, 40, 50 |
| 4 | data[4] ← data[5] | 10, 30, 40, 50, 50 |
ループが終わった時点で、先頭から4件が 10, 30, 40, 50 になりました。20 は消え、後ろが前へ詰まっています。
最後の 50 が二つ並んで残っている点に注目してください。ここは件数を1減らすことで、有効範囲の外に追い出します。
配列の中身そのものを消しているわけではありません。どこまでが有効かという線引きを動かしている、と考えると腑に落ちるはずです。
こうして一行ずつ値を書き出す作業が、科目Bでいちばん効きます。書き方の手順に不安があれば、こちらの記事が助けになります。
【関連記事】擬似言語のトレース表の書き方|科目Bで変数の値を追う手順を解説
なぜ前から回すのか¶
向きを逆にしたらどうなるでしょうか。後ろから前へ回して data[i] ← data[i + 1] を実行すると考えてみます。
i が4のとき data[4] に data[5] が入り、次に i が3で data[3] に data[4] が入ります。すると、さっき書き換えたばかりの値をもう一度コピーしてしまいます。
結果は 10, 50, 50, 50, 50 のような形です。持ってきたい値が、自分の手で上書きされてしまいました。
削除で前から回すのは、まだ触っていない後ろの値を読みに行くためです。読む場所が書く場所より後ろにあるとき、進む向きは前から後ろになります。
挿入は後ろから前へ回して押し出す¶
次は挿入です。今度は逆向きに回すことになります。
4件のデータが入った配列の、2番目の位置に 15 を割り込ませる処理を見てみましょう。
○整数型: i, 件数, 位置
○整数型の配列: data ← {10, 20, 30, 40, 0}
件数 ← 4
位置 ← 2 /* data[2] に 15 を挿入する */
for (i を 件数 から 位置 まで 1 ずつ減らす)
data[i + 1] ← data[i]
endfor
data[位置] ← 15
件数 ← 件数 + 1
ループでは、data[i] の値を一つ後ろの data[i + 1] へ送っています。i は件数から位置まで、1ずつ減っていきます。
この繰返しが終わると、位置の箱が空いた状態になります。そこへ 15 を書き込み、最後に件数を1増やして完了です。
繰返しの進む向きは、条件と更新の書き方で決まります。for と while の止まり方があいまいな人は、こちらで確認しておくと読み違いが減ります。
【関連記事】擬似言語のwhileとforってどう違う?繰返し処理で迷子にならない方法を詳しく解説
なぜ後ろから回すのか¶
理由は削除のときと同じ考え方です。書き込む場所が、読む場所より後ろにあるからです。
前から回して data[i + 1] ← data[i] を実行すると、i が2のとき data[3] に 20 が入ります。ところが data[3] には、これから運ぶはずの 30 が入っていました。
その 30 は上書きされて消えてしまいます。以降はずっと 20 がコピーされ続け、配列が同じ値で埋まる結果になるでしょう。
後ろから回せば、上書きする箱はすでに運び終わった場所か、まだ使っていない空きだけです。だから値が失われません。
覚えるべきは向きそのものではなく、書き込み先を先に空けてから運ぶという原則です。ここさえ握れば、問題文の for が増やすのか減らすのかを自分で判断できます。
挿入と削除を並べて比べる¶
二つの処理は、鏡写しの関係にあります。違いがはっきり見えるように、一覧にしてみましょう。
| 項目 | 削除 | 挿入 |
|---|---|---|
| ずらす向き | 後ろの値を前へ | 前の値を後ろへ |
| ループの進み方 | 位置から後ろへ増やす | 件数から位置へ減らす |
| ループの中身 | data[i] ← data[i + 1] | data[i + 1] ← data[i] |
| ループ後の処理 | 件数を1減らす | 位置に値を代入し件数を1増やす |
| 空く場所 | 末尾側に重複が残る | 位置の箱が空く |
表の3行目を見比べてください。矢印の左右が入れ替わっているだけで、扱う添字は同じ組み合わせです。
この対応を頭に入れておくと、問題文のコードがどちらの処理なのかを一目で判断できます。中身の代入式が、そのまま目印になるわけです。
移動する回数も問われる¶
処理の速さを聞かれることもあります。挿入も削除も、位置によって動かす件数が変わるのが特徴です。
先頭に入れる場合は、ほぼ全件をずらすことになります。末尾に足す場合は、ずらす必要がありません。
つまり最悪の場合は件数に比例した回数がかかり、オーダーで書けば O(n) です。配列の途中をいじる処理が重いと言われるのは、このためです。
一方で、添字を指定して値を取り出すだけなら一瞬で終わります。配列は読むのが速く、途中を書き換えるのが遅い入れ物だと整理しておきましょう。
なお、末尾に足すだけの処理ならループそのものが要りません。件数の次の箱に値を入れて、件数を1増やせば終わりです。
問題文が末尾への追加を求めているのか、指定した位置への挿入を求めているのか。ここを読み分けるだけで、選択肢がかなり絞れます。
連結リストとの違いにつながる¶
ここまで読んで、もっと楽な方法はないのかと感じた人もいるでしょう。その答えが連結リストです。
連結リストは、次のノードを指す参照をつなぎ替えるだけで挿入や削除ができます。要素をずらす作業が要らないわけです。
そのかわり、n 番目の要素を取り出すには先頭からたどる必要があります。配列とは得意な作業が逆になっている、という関係です。
参照のたどり方に不安が残っているなら、こちらの記事で動きを確認しておくと比較が理解しやすくなります。
【関連記事】擬似言語のリスト(連結リスト)がわからない人へ|ノードと参照のたどり方を解説
つまずきやすい場所を三つ¶
最後に、私が相談を受ける中で目立った間違いを挙げておきます。どれも一つずれるだけの小さな差です。
一つ目は、ループの終わりの値です。削除で 件数 まで回してしまうと、存在しない data[件数 + 1] を読みに行きます。
範囲の外を触る誤りは、答えが合わないだけでなく実務では障害の原因にもなります。終わりの値は必ず紙に書いて確かめてください。
二つ目は、件数の更新漏れです。配列の中身を正しくずらしても、件数がそのままなら結果は一件多いままになります。
件数を先に減らしてからループを回してしまう書き方も、よく見かける取り違えです。順番が入れ替わると、最後の一件が運ばれずに残ります。
三つ目は、添字の起点の取り違えです。擬似言語では1から始める例が多いですが、問題によっては0から始まることもあります。
起点が変われば、ループの範囲も式も一つずつずれます。要素数と添字の関係を整理しておきたい人は、こちらから読み直すと早いでしょう。
【関連記事】擬似言語の配列と添字がわからない人へ|要素数・二次元配列の読み方を解説
この三つは、どれも小さな確認で防げます。焦っているときほど飛ばしがちなので、手順として決めておくのがおすすめです。
Giji Academy の擬似言語シミュレーターなら、ループを一行ずつ進めながら配列の中身が変わる様子を目で追えます。向きを逆にすると値が壊れることも、自分の手で試すと一度で納得できるはずです。
まとめ¶
ここまでの内容を振り返ります。
配列は長さが変わらない入れ物なので、途中を出し入れするには要素をずらすしかありません。有効な件数は、別の変数で管理します。
削除は位置から後ろへ向かって回し、data[i] ← data[i + 1] で後ろの値を前へ引き寄せます。終わったら件数を1減らします。
挿入は件数から位置へ向かって減らしながら回し、data[i + 1] ← data[i] で前の値を後ろへ送ります。空いた箱に値を入れ、件数を1増やします。
向きが逆になるのは、書き込む先を先に空けておく必要があるからでした。丸暗記ではなく、上書きが起きるかどうかで判断してください。
つまずきやすいのは、ループの終わりの値、件数の更新、添字の起点の三つです。どれも一つ分のずれなので、確認の手順を決めておくと防げます。
添字のずれは、見直しの型を持っているかどうかで結果が変わります。同じような取りこぼしが続いている人は、こちらもあわせて読んでみてください。
【関連記事】擬似言語でケアレスミスが多い人へ|よくある間違いと確認方法
ずらす処理は、慣れれば得点源になる分野です。今日の一問から、書く場所と読む場所のどちらが後ろかを口に出して確かめてみてください。