ネストされたループはなぜ悪い習慣と見なされるのですか?


33

私の講師は、ネストされたループを処理するときに参照できるように、Javaでループに「ラベル付け」することが可能であると今日述べました。それで、私はそれについて知らなかったので機能を調べました、そして、この機能が説明された多くの場所に警告が続き、ネストされたループを思いとどまらせました。

どうしてか分からないの?それはコードの可読性に影響するからですか?それとももっと「技術的」なものですか?


4
私のCS3コースを正しく覚えているのは、指数関数的な時間がかかることが多いためです。つまり、大きなデータセットを取得すると、アプリケーションが使用できなくなるからです。
トラビスペセット

21
CS講師について知っておくべきことの1つは、彼らが言うことすべてが現実世界で100%当てはまるわけではないということです。ループをいくつかの深さ以上にネストすることはお勧めしませんが、問題を解決するためにm x n個の要素を処理する必要がある場合は、それだけ多くの反復を行うことになります。
Blrfl

8
@TravisPessetto実際には、まだ多項式の複雑さです-O(n ^ k)、kは入れ子の数で、指数O(k ^ n)ではありません。ここで、kは定数です。
m3th0dman

1
@ m3th0dman修正してくれてありがとう。私の先生はこのテーマに関しては最高ではありませんでした。彼はO(n ^ 2)とO(k ^ n)を同じものとして扱いました。
トラビスペセット

2
一部の人によると、ネストされたループは循環的複雑度を高め(こちらを参照)、プログラムの保守性を低下させます。
マルコ

回答:


62

ネストされたループは、正しいアルゴリズムを記述している限り問題ありません。

ネストされたループにはパフォーマンスの考慮事項があります(@ Travis-Pesettoの回答を参照)が、マトリックスのすべての値にアクセスする必要がある場合など、正確なアルゴリズムである場合があります。

Javaのループにラベルを付けると、他の方法でこれが面倒な場合に、いくつかのネストされたループから途中で抜け出すことができます。たとえば、一部のゲームには次のようなコードが含まれている場合があります。

Player chosen_one = null;
...
outer: // this is a label
for (Player player : party.getPlayers()) {
  for (Cell cell : player.getVisibleMapCells()) {
    for (Item artefact : cell.getItemsOnTheFloor())
      if (artefact == HOLY_GRAIL) {
        chosen_one = player;
        break outer; // everyone stop looking, we found it
      }
  }
}

上記の例のようなコードは特定のアルゴリズムを表現する最適な方法である場合がありますが、通常、このコードを小さな関数に分割し、おそらくのreturn代わりに使用する方が適切ですbreak。だから、breakラベル付きはかすかなコードの匂いです; あなたがそれを見るとき、特別な注意を払います。


2
サイドノートグラフィックスがマトリックスのすべての部分にアクセスしなければならないアルゴリズムを使用するように。ただし、GPUはこれを時間効率の良い方法で処理するように特化されています。
トラビスペセット

はい、GPUはこれを超並列的に実行します。問題は実行の単一スレッドに関するものだったと思います。
9000

2
ラベルが疑われる傾向がある理由の1つは、多くの場合代替手段があるためです。この場合、代わりに戻ることができます。
jgmjgm

22

ネストされたループは、頻繁に(常にではありませんが)悪い習慣です。なぜなら、それらはあなたがやろうとしていることに対してしばしば(しかし常にではない)過剰すぎるからです。多くの場合、達成しようとしている目標を達成するためのはるかに高速で無駄の少ない方法があります。

たとえば、リストAに100個のアイテムがあり、リストBに100個のアイテムがあり、リストAの各アイテムに一致するリストBのアイテムが1つあることがわかっている場合(「match」の定義は意図的に曖昧にここで)、ペアのリストを作成したい場合、簡単な方法は次のとおりです:

for each item X in list A:
  for each item Y in list B:
    if X matches Y then
      add (X, Y) to results
      break

各リストに100個のアイテムがある場合、これには平均100 * 100/2(5,000)matches操作がかかります。より多くのアイテムがある場合、または1:1の相関が保証されていない場合は、さらに高価になります。

一方、次のような操作を実行するはるかに高速な方法があります。

sort list A
sort list B (according to the same sort order)
I = 0
J = 0
repeat
  X = A[I]
  Y = B[J]
  if X matches Y then
    add (X, Y) to results
    increment I
    increment J
  else if X < Y then
    increment I
  else increment J
until either index reaches the end of its list

あなたは、このようにそれを行う場合は、代わりの数のmatches上で基準とする操作length(A) * length(B)、今に基づいているlength(A) + length(B)あなたのコードははるかに高速に実行することを意味しています。


10
O(n log n)Quicksortが使用されている場合、並べ替えのセットアップに2回の取るに足らない時間がかかることに注意してください。
ロバートハーベイ

@RobertHarvey:もちろんです。しかし、それはまだ遠い未満だO(n^2)N.の非小さな値のため
メイソンウィーラー

10
アルゴリズムの2番目のバージョンは一般に正しくありません。まず、XとYは<演算子を介して比較可能であると想定していますが、通常はmatches演算子から導出することはできません。第二に、XとYの両方が数値であっても、2番目のアルゴリズムが間違った結果を生成する場合X matches YがありX + Y == 100ます(例:is)。
パシャ

9
@ user958624:明らかに、これは一般的なアルゴリズムの非常に高レベルの概要です。「一致」演算子と同様に、「<」は比較されるデータのコンテキストで正しい方法で定義する必要があります。これが正しく行われると、結果は正しいものになります。
メイソンウィーラー

PHPは、私が思うにユニークな配列で、そして/またはその反対で、最後の1つを実行しました。代わりに、ハッシュベースのPHPの配列を使用するだけで、はるかに高速になります。
jgmjgm

11

ループのネストを回避する理由の1つは、ループであるかどうかに関係なく、ブロック構造を深くネストしすぎるのは悪い考えだからです。

各関数またはメソッドは、目的(名前がそれを表すもの)とメンテナー(内部構造を理解しやすい)の両方で、理解しやすいものでなければなりません。関数が複雑すぎて簡単に理解できない場合、通常、内部の一部を個別の関数に分解して、名前で(現在はより小さい)メイン関数で参照できるようにする必要があることを意味します。

入れ子になったループは比較的簡単に理解するのが難しくなる場合がありますが、いくつかのループの入れ子は問題ありません-他の人が指摘するように、それは極端に(そして不必要に)遅いアルゴリズムを使用してパフォーマンスの問題を引き起こしているという意味ではありません。

実際、パフォーマンスの限界を驚くほ​​ど遅くするために、ネストされたループは必要ありません。たとえば、各反復でキューから1つの項目を取得し、場合によってはいくつかの項目を戻す単一のループ(迷路の幅優先探索など)を考えてください。パフォーマンスは、ループのネストの深さ(1のみ)ではなく、最終的に使い果たされる前にキューに入れられるアイテムの数(使い果たされた場合)によって決定されます-到達可能な部分の大きさ迷路です。


1
多くの場合、ネストされたループを平坦化するだけで、同じ時間がかかります。0から幅、0から高さ、代わりに、幅に高さを掛けた値を0にすることができます。
jgmjgm

@jgmjgm-はい、ループ内でコードを複雑にするリスクがあります。場合によっては、それを単純化することもできますが、少なくとも実際に必要なインデックスを復元する複雑さを少なくとも追加することが多くなります。そのための秘trickの1つは、すべてのループを特別な複合インデックスをインクリメントするロジックにネストするインデックスタイプを使用することです。1つのループだけでそれを行うことはできませんが、同様の構造を持つ複数のループがあるか、より柔軟な汎用バージョンを作成できます。そのタイプ(存在する場合)を使用するためのオーバーヘッドは、明確にするために価値があります。
Steve314

良いことだとは言っていませんが、2つのループを1つに変えるのは驚くほど簡単ですが、時間の複雑さには影響しません。
jgmjgm

7

多くのネストされたループの場合、多項式時間になります。たとえば、次の擬似コードがあります。

set i equal to 1
while i is not equal to 100
  increment i
  set j equal to 1
  while j is not equal to i
    increment j
  end
 end

これはO(n ^ 2)時間と見なされ、次のようなグラフになります。 ここに画像の説明を入力してください

ここで、y軸はプログラムの終了にかかる時間であり、x軸はデータの量です。

データが多すぎる場合、プログラムは非常に遅くなり、誰もそれを待つことはありません。そして、私はそれがあまりにも長くかかると私が信じる約1,000のデータエントリほどではありません。


10
あなたはグラフを再選択したいかもしれません:それは二次曲線ではなく指数曲線であり、また再帰はO(n ^ 2)からO(n log n)を作りません
ラチェットフリーク

5
再帰については、「スタック」と「オーバーフロー」という2つの単語があります
マテウス

10
再帰によってO(n ^ 2)操作をO(n log n)に減らすことができるという記述は、ほとんど不正確です。再帰的に実装された同じアルゴリズムと反復的に実装された同じアルゴリズムは、まったく同じbig-O時間の複雑さを持つ必要があります。さらに、各再帰呼び出しは新しいスタックフレームの作成を必要とするのに対し、反復は分岐/比較のみを必要とするため、再帰はしばしば遅くなります(言語実装によって異なります)。通常、関数呼び出しは安価ですが、無料ではありません。
dckrooney

2
@TravisPessetto過去6か月間に、再帰または循環オブジェクト参照によるC#アプリケーションの開発中に3つのスタックオーバーフローが発生しました。何がおかしいのか、それがクラッシュし、何があなたを襲ったのかわからない。ネストされたループを見ると、何か悪いことが起こる可能性があり、何かの間違ったインデックスに関する例外が簡単に見えることがわかります。
マテウス

2
@Mateuszも、Javaのような言語を使用すると、スタックオーバーフローエラーをキャッチできます。スタックトレースを使用すると、何が起こったのかを確認できます。私はあまり経験がありませんが、スタックオーバーフローエラーを目にしたのは、無限再帰を引き起こすバグがあったPHPで、PHPが割り当てられた512 MBのメモリを使い果たしたときだけです。再帰には、無限ループには適さない最終的な終了値が必要です。CSのすべてのものと同様に、すべてのものに時間と場所があります。
トラビスペセット

0

小型乗用車の代わりに30トンのトラックを運転するのは悪い習慣です。20トンまたは30トンの荷物を輸送する必要がある場合を除きます。

ネストされたループを使用する場合、悪い習慣ではありません。それはまったく馬鹿げているか、まさに必要なものです。あなたが決める。

しかし、誰かがループのラベル付けについて不満を言いました。その答えは、質問する必要がある場合は、ラベルを使用しないでください。自分を決定するのに十分な知識がある場合は、自分で決定します。


自分自身を決定するのに十分な知識がある場合は、ラベル付きループを使用しないことを十分に知っています。:-)
user949300

いいえ。十分に教えられたら、ラベル付きの使用を使わないように教えられました。あなたが十分に知っているとき、あなたは教義を超えて、正しいことをします。
gnasher729

1
ラベルの問題は、それが必要になることはめったになく、多くの人がフロー制御の誤りを回避するために早い段階でそれを使用することです。人々がフロー制御を間違えたときに出口をあちこちに配置するようなものです。また、機能により大幅に冗長化されます。
jgmjgm

-1

ネストされたループについて本質的に悪いことや、必ずしも悪いことはありません。ただし、特定の考慮事項と落とし穴があります。

あなたが導かれた記事は、おそらく簡潔さの名において、または燃えているとして知られている心理的なプロセスのために、詳細をスキップしました。

焦げているとは、あなたが何かを否定的に経験し、それを避けているという意味です。たとえば、野菜を鋭いナイフで切り、自分で切ります。その場合、鋭利なナイフは悪いと言うかもしれません。野菜を切るためにそれらを使用して、その悪い経験が二度と起こらないようにしようとしないでください。それは明らかに非常に非現実的です。実際には、注意する必要があります。他の人に野菜を切るように言っているなら、あなたはこれをさらに強く感じます。野菜を切るように子供たちに指示していた場合、特にそれらを厳密に監督できない場合は、鋭いナイフを使用しないように彼らに伝えることを非常に強く感じます。

プログラミングの問題は、常に最初に安全性を優先する場合、ピーク効率に達しないことです。この場合、子供は柔らかい野菜のみを切ることができます。他の何かに直面し、彼らは鈍いナイフを使用してそれを台無しにします。ネストされたループを含むループの適切な使用法を学ぶことが重要であり、それらが悪いとみなされて、決してそれらを使用しようとしないなら、あなたはそれをすることができません。

ここで多くの回答が指摘しているように、ネストされたforループは、プログラムのパフォーマンス特性を示しており、ネストごとに指数関数的に悪化する可能性があります。つまり、O(n)、O(n ^ 2)、O(n ^ 3)などは、O(n ^ depth)で構成されます。ここで、depthは、ネストしたループの数を表します。ネストが大きくなると、必要な時間が指数関数的に長くなります。問題はこれがあなたの時間または空間の複雑さがそれであるという確実性ではないことです(非常にしばしばa * b * cですが、すべてのネストループが常に実行されるわけではありません)。それであってもパフォーマンスの問題があります。

多くの人々、特に率直に言う学生、作家、講師にとっては、生活のために、または日常的にループのためにプログラムされることはめったにないことも、慣れていないものであり、早期の出会いにあまりにも多くの認知的負荷を引き起こします。学習曲線は常に存在し、それを避けることは学生をプログラマーに変えるのに効果的ではないため、これは問題のある側面です。

ネストされたループはワイルドになる可能性があります。つまり、非常に深くネストされる可能性があります。私が各大陸を通り、次に各国を通り、次に各都市を通り、次に各店を通り、次に各棚を通り、それから各製品を通り抜け、各豆に豆の缶があれば、そのサイズを測定して平均を求めます非常に深くネストすることがわかります。ピラミッドがあり、左マージンから多くの無駄なスペースがあります。場合によっては、ページを離れることもあります。

これは、画面が小さく、解像度が低い場合、歴史的に重要な問題です。これらの場合、いくつかのレベルのネストでさえ、本当に多くのスペースを占有する可能性があります。これは、しきい値が高い今日ではそれほど懸念事項ではありませんが、十分な入れ子がある場合でも問題が発生する可能性があります。

関連するのは美学の議論です。多くの人は、より一貫性のある配置のレイアウトとは対照的に、ネストされたforループが見た目が良いとは感じません。ただし、自己強化される傾向があり、コードのブロックを分割し、関数などの抽象化の背後にループをカプセル化すると、コードの実行フローへのマッピングを分割するリスクがあるため、最終的にコードを読みにくくする可能性があるという点で問題があります。

人々が慣れているものへの自然な傾向があります。ネストを必要としない確率が最も単純な方法でプログラミングしている場合、あるレベルが必要になる確率は1桁低下し、別のレベルが発生する確率は再び低下します。頻度が低下し、本質的に意味するのは、ネストが深いほど、訓練されていない人間の感覚はそれを予測することです。

これに関連するのは、ネストされたループと見なすことができる複雑な構造では、ループをあまり必要としないソリューションを逃す可能性があるため、できるだけ簡単なソリューションであるということです。皮肉なことに、ネストされたソリューションは、多くの場合、最小限の労力、複雑さ、および認知負荷で機能するものを作成する最も簡単な方法です。多くの場合、forループをネストします。たとえば、上記の回答の1つを検討すると、ネストされたforループよりもはるかに高速な方法もはるかに複雑で、かなり多くのコードで構成されています。

ループを抽象化または平坦化することはしばしば可能であるため、多くの注意が必要です。最終結果は、特に努力から測定可能な大幅なパフォーマンスの向上を受けていない場合、最終的には病気よりも悪い治療法になります。

コンピュータにアクションを何度も繰り返すように指示するループに関連して、パフォーマンスの問題が頻繁に発生することは非常に一般的であり、本質的にパフォーマンスのボトルネックに関係します。残念ながら、これに対する応答は非常に表面的です。ループが見られ、パフォーマンスの問題が発生し、何も存在しない場合にループが見えなくなり、実際の効果が得られなくなることがよくあります。このコードは高速に「見え」ますが、道路に置いて、点火キーを押し、アクセルを床に置き、速度計を見てください。ジマーフレームを歩いている老婦人と同じくらい速いことがわかります。

この種の隠蔽は、ルートに10人の強盗がいる場合に似ています。あなたが行きたい場所にまっすぐなルートを持っている代わりに、隅々に強盗がいるように手配すると、強盗がないという旅を始めるときに幻想を与えます。頭の外に見えない。あなたはまだ10回強盗されるつもりですが、今ではそれが来るのを見ることはありません。

あなたの質問に対する答えは、両方であるということですが、どちらの懸念も絶対的なものではありません。それらは完全に主観的であるか、文脈的にのみ客観的です。残念なことに、完全に主観的な意見やむしろ意見が先行して支配することがあります。

経験則として、ネストされたループが必要な場合、またはそれが次の明白なステップのように思える場合は、意図せずに単純に実行するのが最善です。ただし、疑問が残る場合は、後で確認する必要があります。

もう1つの経験則は、カーディナリティを常に確認し、このループが問題になるかどうかを自問することです。前の例では、都市を通りました。テストのために、私は10の都市を通過するだけかもしれませんが、現実世界の使用で期待される都市の合理的な最大数は何ですか?それから、大陸で同じことを掛けます。ループで常に考慮することは、経験則です。特に、動的な(可変の)量を反復することは、それが最終的に変換される可能性があります。

とにかく、常に最初に動作することを実行してください。最適化の機会を見つけた場合、最適化されたソリューションを最も簡単に機能するソリューションと比較し、期待される利点が得られたことを確認できます。また、測定が行われる前に早すぎる最適化を行うと、YAGNIが発生したり、多くの時間を無駄にしたり、期限を逃したりする可能性があります。


鋭い<->鈍いナイフの例は、鈍いナイフは一般的に切断するのにより危険であるため、素晴らしくありません。そして、O(n)-> O(n ^ 2)-> O(n ^ 3)は指数関数ではなくn幾何学的または多項式です。
カレス

鈍いナイフが悪いということは、ちょっとした神話です。実際には、それは変化し、通常、鈍いナイフが特に不適切であり、通常、多くの力を必要とし、多くの滑りを伴う場合に特有です。ただし、隠されているが固有の制限があり、柔らかい野菜のみをカットできます。私はn ^ depth指数関数を検討しますが、あなたはそれらの例はそうではありません。
jgmjgm

@jgmjgm:n ^ depthは多項式、depth ^ nは指数関数です。それは本当に解釈の問題ではありません。
Roel Schroeven

x ^ yはy ^ xと異なりますか?あなたが犯した間違いは、あなたが答えを決して読まないということです。指数関数をgrepし、それ自体が指数関数ではない方程式をgrepしました。読めば、ネストの各層で指数関数的に増加し、それを自分でテストすると、for(a = 0; a <n; a ++); for(b = 0; b <n ; b ++); for(c = 0; c <n; c ++); ループを追加または削除すると、実際に指数関数的であることがわかります。n ^ 1で1つのループが実行され、2つがn ^ 2で実行され、3つがn ^ 3で実行されます。ネストされた:Dを理解できません。Gemoetricサブセット指数。
jgmjgm

これは、ネストされたコンストラクトに本当に苦労しているという点を証明していると思います。
jgmjgm
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.