正しいループを書く方法は?


65

ほとんどの場合、ループを書いている間、間違った境界条件(例:間違った結果)を書くか、ループ終了に関する私の仮定が間違っています(例:無限ループ)。試行錯誤の結果、仮定は正しいものになりましたが、頭の中に正しいコンピューティングモデルがないためにイライラしすぎました。

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

私が最初にした間違いは次のとおりです。

  1. j> = 0の代わりに、j> 0を維持しました。
  2. array [j + 1] = valueかarray [j] = valueかどうか混乱してしまいました。

そのような間違いを避けるためのツール/メンタルモデルとは何ですか?


6
どのような状況でそれj >= 0が間違いだと思いますか?私はあなたがアクセスしているという事実をより警戒するだろうarray[j]array[j + 1]最初にそれをチェックせずarray.length > (j + 1)
ベンコットレル

5
@LightnessRacesinOrbitが言ったことと同じように、すでに解決された問題を解決している可能性があります。一般的に、データ構造に対して行う必要のあるループは、コアモジュールまたはクラス(Array.prototypeJSの例)に既に存在します。これはmap、すべての配列で動作するようなものがあるため、エッジ条件に遭遇するのを防ぎます。ループを回避するために、スライスと連結を使用して上記の問題を解決できます。codepen.io / anon / pen / ZWovdg?editors= 0012ループを記述する最も正しい方法は、ループをまったく書き込まないことです。
ジェドシュナイダー

13
実際には、解決された問題を解決してください。それは練習と呼ばれます。わざわざ公開しないでください。つまり、ソリューションを改善する方法を見つけない限りです。とはいえ、車輪の再発明には車輪以上のものが含まれます。ホイール全体の品質管理システムと顧客サポートが含まれます。それでも、カスタムリムはいいです。
candied_orange

53
ここで間違った方向に向かっているのではないかと心配しています。CodeYogiの例はよく知られたアルゴリズムの一部であるため、CodeYogiを提供するのはかなり根拠がありません。彼は何か新しいものを発明したと主張したことはありません。彼は、ループを記述するときに、非常に一般的な境界エラーを回避する方法を尋ねています。図書館は長い道のりを歩んできましたが、ループの書き方を知っている人々にはまだ未来があります。
candied_orange

5
一般に、ループとインデックスを扱う場合、インデックスは要素間を指し、半開間隔に慣れていることを学習する必要があります(実際、これらは同じ概念の2つの側面です)。これらの事実を取得すると、ループ/インデックスの大部分が完全に消えます。
マッテオイタリア

回答:


208

テスト

いいえ、真剣に、テストします。

私は20年以上コーディングしてきましたが、最初はループを正しく書くことをまだ信用していません。動作を疑う前に動作することを証明するテストを作成して実行します。すべての境界条件の両側をテストします。たとえばrightIndex、0の場合はどうすればよいですか?-1はどうですか?

複雑にしないでおく

他の人が一目でそれが何をするかを見ることができないなら、あなたはそれをやりすぎています。理解しやすいものを書くことができる場合は、パフォーマンスを無視してください。本当に必要な場合にのみ、高速化してください。そして、それでもあなたはあなたが何があなたを遅くしているのかを正確に知っていると絶対に確信しているときだけです。Big Oの実際の改善を達成できる場合、このアクティビティは無意味ではないかもしれませんが、それでもコードをできるだけ読みやすくします。

1つオフ

指を数えることと指の間を数えることの違いを知ってください。時々、スペースは実際に重要なものです。指で気を散らさないでください。親指が指かどうかを確認します。小指と親指の隙間がスペースとしてカウントされるかどうかを確認します。

コメント

あなたがコードで迷子になる前に、あなたが英語で意味することを言ってみてください。期待を明確に述べてください。コードの仕組みを説明しないでください。なぜそれをするようにさせているのか説明してください。実装の詳細は外してください。コメントを変更せずにコードをリファクタリングできるようにする必要があります。

最良のコメントは良い名前です。

あなたが良い名前で言う必要があるすべてを言うことができるならば、コメントでそれを再び言わないでください。

抽象化

オブジェクト、関数、配列、および変数はすべて、与えられた名前と同じくらい良い抽象化です。人々が彼らの中を見るとき、彼らが見つけたものに驚かないことを保証する名前を彼らに与えてください。

短い名前

短命のものには短い名前を使用します。iは、意味を明確にする小さなスコープ内の素敵なタイトループ内のインデックスの良い名前です。i混同される可能性のある他のアイデアや名前とi次々に広まっていくのに十分な長さの生活を送っているならi、素敵な長い説明的な名前を付ける時が来ました。

長い名前

行の長さを考慮して、名前を短くしないでください。コードをレイアウトする別の方法を見つけてください。

空白

欠陥は、読み取り不能なコードで隠すのが大好きです。言語でインデントスタイルを選択できる場合、少なくとも一貫性が保たれます。コードをノイズの流れのように見せないでください。コードは、形成中に行進しているように見えるはずです。

ループ構造

あなたの言語のループ構造を学び、確認してください。 デバッガーでfor(;;)ループ強調表示することは非常に有益です。すべてのフォームをご覧ください。 whiledo whilewhile(true)for each。できるだけ簡単なものを使用してください。見上げてポンプをプライミング。あなたがそれらを持っている場合、何breakを学び、何をcontinueします。違いを知っているc++++c。閉じる必要があるものすべてを常に閉じている限り、早く戻ることを恐れないでください。最後にブロックするか、できればそれを開いたときに自動的に閉じるようにマークするもの:Using statement / Try with Resources

ループの代替

可能であれば、他の人にループをさせてください。目には簡単で、すでにデバッグされています。これらは、多くの形で来る:コレクションまたは許可ストリームmap()reduce()foreach()、および他のそのような方法ラムダを適用します。などの特殊機能を探しArrays.fill()ます。再帰もありますが、それは特別な場合に物事を簡単にすることだけを期待しています。一般に、代替がどのように見えるかがわかるまで、再帰を使用しないでください。

ああ、テスト。

テスト、テスト、テスト。

テストについて言及しましたか?

もう一つありました。思い出せない。はじめに...


36
良い答えですが、テストについて言及する必要があります。ユニットテストで無限ループをどのように扱いますか?そのようなループはテストを「中断」しませんか?
GameAlchemist

139
@GameAlchemistこれがピザのテストです。ピザを作るのにかかる時間内にコードの実行が停止しない場合、何かがおかしいと疑い始めます。確かに、アラン・チューリングの停止問題の治療法ではありませんが、少なくとも私はピザを取り引きします。
candied_orange

12
@CodeYogi-実際、非常に接近することができます。単一の値で動作するテストから始めます。ループなしでコードを実装します。次に、2つの値を操作するテストを作成します。ループを実装します。このようにすると、ループで境界条件が間違っている可能性はほとんどありません。ほとんどすべての状況で、このようなミスをするとこれら2つのテストのいずれかが失敗するからです。
ジュール

15
@CodeYogi DudeすべてTDDの功績ですが、Testing >> TDDです。値を出力することはテストであり、コードに目を向けるのはテストです(これをコードレビューとして形式化できますが、多くの場合、誰かと5分間会話するだけです)。テストとは、失敗する意図を表現するチャンスです。地獄、あなたはあなたのお母さんと考えを通して話すことによってあなたのコードをテストすることができます。シャワーのタイルを見つめていると、コードにバグが見つかりました。TDDは、すべてのショップで見つかるわけではない、効果的な形式化された専門分野です。私は、人々がテストしなかった場所でコーディングしたことは一度もありません。
candied_orange

12
私はTDDを聞いたことが何年も前にコーディングとテストを行っていました。その年と、ズボンを着用せずにコーディングに費やした年数との相関関係に気づいたのは今だけです。
candied_orange

72

プログラミングするときは、次のことを考えると便利です。

未知の領域(インデックスとのジャグリングなど)を探索する場合、それらについて考えるだけでなく、実際にアサーションを使用してコード内で明示的にすることは非常に便利です。

元のコードを見てみましょう。

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

そして、私たちが持っているものを確認してください:

  • 前提条件:array[0..rightIndex]ソートされます
  • 事後条件:array[0..rightIndex+1]ソートされます
  • 不変:0 <= j <= rightIndexしかし、少し冗長に見えます。または、コメントに記載されている@Julesのように、「ラウンド」の最後にfor n in [j, rightIndex+1] => array[j] > value
  • 不変式:「ラウンド」の終わりに、array[0..rightIndex+1]ソートされます

したがって、最初にis_sorted関数とmin配列スライスで機能する関数を記述し、次にアサートすることができます。

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

また、ループ条件が少し複雑であるという事実もあります。あなたは物事を分割することで自分自身でそれを簡単にしたいかもしれません:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for (var j = rightIndex; j >= 0; j--) {
        if (array[j] <= value) { break; }

        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

現在、ループは単純です(jからrightIndexに進みます0)。

最後に、これをテストする必要があります。

  • 境界条件を考える(rightIndex == 0rightIndex == array.size - 2
  • valueより小さいarray[0]か大きいと思うarray[rightIndex]
  • valueに等しいarray[0]array[rightIndex]または何らかの中間インデックスと考える

また、ファジングを過小評価しないでください。ミスをキャッチするためのアサーションが用意されているため、ランダム配列を生成し、メソッドを使用してソートします。アサーションが起動すると、バグが見つかり、テストスイートを拡張できます。


8
@CodeYogi:With ...テスト。問題は、すべてをアサーションで表現することは非実用的である可能性があることです。アサーションが単にコードを繰り返すだけの場合、新しいものは何ももたらされません(繰り返しは品質に役立ちません)。これが、ここでループ内で、0 <= j <= rightIndexまたはarray[j] <= value、コードを繰り返すだけだと断言しなかった理由です。一方、is_sorted新しい保証をもたらすため、価値があります。その後、それがテストの目的です。insert([0, 1, 2], 2, 3)関数を呼び出しても出力がそうでない[0, 1, 2, 3]場合は、バグが見つかりました。
マチューM.

3
なつみ 単にコードを複製するからといってアサーションの価値を軽視しないでください。実際、コードを書き直すことにした場合、これらは非常に貴重なアサーションになる可能性があります。テストには重複するすべての権利があります。むしろ、アサーションが単一のコード実装に非常に結合されているため、書き換えによってアサーションが無効になる場合を考慮してください。それは時間を無駄にしているときです。ところでいい答え。
candied_orange

1
@CandiedOrange:コードを複製することにより、文字通り意味しarray[j+1] = array[j]; assert(array[j+1] == array[j]);ます。この場合、値は非常に低いようです(コピー/貼り付けです)。意味を複製するが、別の方法で表現される場合、それはより価値があります。
マチューM.

10
Hoareのルール:1969年以来、正しいループの記述を支援しています。
Joker_vD

1
なつみ 価値が非常に低いことに同意します。しかし、私はそれがコピー/ペーストであることによって引き起こされるとは考えていません。insert()古い配列から新しい配列にコピーすることで機能するようにリファクタリングしたいとします。それは、他assertのを壊すことなく行うことができます。しかし、これは最後ではありません。他assertのがどれだけうまく設計されたかを示しています。
candied_orange

29

単体テスト/ TDDを使用する

forループを介してシーケンスに本当にアクセスする必要がある場合は、ユニットテスト、特にテスト駆動開発によってミスを回避できます。

ゼロより優れた値を逆の順序でとるメソッドを実装する必要があると想像してください。どのようなテストケースが考えられますか?

  1. シーケンスには、ゼロよりも優れた1つの値が含まれます。

    実際:[5]。予想:[5]

    要件を満たす最も簡単な実装は、呼び出し元にソースシーケンスを返すだけです。

  2. シーケンスには2つの値が含まれており、どちらもゼロより上です。

    実際:[5, 7]。予想:[7, 5]

    これで、シーケンスを単に返すことはできませんが、逆にする必要があります。あなたが使用するfor (;;)ループを、別の言語構築物またはライブラリー法は重要ではありません。

  3. シーケンスには3つの値が含まれ、1つはゼロです。

    実際:[5, 0, 7]。予想:[7, 5]

    ここで、値をフィルタリングするためにコードを変更する必要があります。繰り返しになりますが、これは、ifステートメントまたはお気に入りのフレームワークメソッドの呼び出しによって表現できます。

  4. アルゴリズムに応じて(これはホワイトボックステストであるため、実装が重要です)、空のシーケンス[] → []ケースを具体的に処理する必要がある場合とそうでない場合があります。または、すべての値が負であるエッジケース[-4, 0, -5, 0] → []が正しく処理されるようにするか、境界の負の値が次のようになるようにし[6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]ます。ただし、多くの場合、上記のテストは3つしかありません。追加のテストを行ってもコードは変更されないため、無関係です。

より高い抽象化レベルで作業する

ただし、多くの場合、既存のライブラリ/フレームワークを使用して、より高い抽象化レベルで作業することにより、これらのエラーのほとんどを回避できます。これらのライブラリ/フレームワークにより、シーケンスを元に戻したり、ソートしたり、分割したり、結合したり、配列や二重リンクリストに値を挿入または削除したりすることができます。

通常、のforeach代わりに使用できfor、境界条件のチェックを無関係にします。言語がそれを行います。Pythonなどの一部の言語には、for (;;)コンストラクトさえありませんが、のみfor ... in ...です。

C#では、シーケンスを操作するときにLINQが特に便利です。

var result = source.Skip(5).TakeWhile(c => c > 0);

forバリアントと比較して、はるかに読みやすく、エラーが発生しにくい:

for (int i = 5; i < source.Length; i++)
{
    var value = source[i];
    if (value <= 0)
    {
        break;
    }

    yield return value;
}

3
さて、あなたの元の質問から、選択肢は一方でTDDを使用して正しい解決策を取得し、他方でテスト部分をスキップして境界条件を間違っているという印象があります。
Arseni Mourzenko

18
部屋にいる象に言及してくれてありがとう:ループをまったく使わないなぜ人々がまだ1985年のようにコードを書いているのか(そして私は寛大だ)、私を超えている。ボクタオ。
ジャレッドスミス

4
@JaredSmithコンピューターが実際にそのコードを実行したら、そこにジャンプ命令がないことをどれだけ賭けたいですか?LINQを使用することで、ループを抽象化していますが、それはまだあります。私はこれを画家シュレミエルの苦労について学ばなかった同僚に説明しました。ループがどこで発生するかを理解しないと、コード内でループが抽象化され、結果としてコードが大幅に読みやすくなる場合でも、ほとんどの場合、パフォーマンスの問題が発生します。
からCVn

6
@MichaelKjörling:LINQを使用すると、ループは存在しますfor(;;)コンストラクトはこのループをあまり説明しません。重要な点は、少なくとも元の質問の範囲内で、LINQ(およびPythonのリスト内包表記および他の言語の類似要素)が境界条件をほとんど無関係にすることです。ただし、特に遅延評価に関しては、LINQを使用する際に内部で何が起こるかを理解する必要性について、私はこれ以上同意できません。
Arseni Mourzenko

4
@MichaelKjörling必ずしもLINQについて話しているわけではありませんでしたが、私はあなたの主張を見逃しています。forEachmapLazyIterator、などの言語のコンパイラやランタイム環境で提供し、間違いなくされている以下の各反復でペンキのバケツに戻って歩いされる可能性が高いです。つまり、読みやすさとオフバイワンのエラーが、これらの機能が最新の言語に追加された2つの理由です。
ジャレッドスミス

15

私はあなたのコードをテストすると言う他の人々に同意します。しかし、そもそもそれを正しくするのもいいことです。多くの場合、境界条件が間違っている傾向があるため、このような問題を防ぐためのメンタルトリックを開発しました。

インデックスが0の配列では、通常の状態は次のようになります。

for (int i = 0; i < length; i++)

または

for (int i = length - 1; i >= 0; i--)

それらのパターンは第二の性質になるはずです。それらについて考える必要はまったくありません。

しかし、すべてがその正確なパターンに従うわけではありません。正しく書いたかどうかわからない場合は、次のステップに進んでください。

値をプラグインし、自分の脳のコードを評価します。できるだけ簡単に考えてください。関連する値が0の場合はどうなりますか?それらが1の場合はどうなりますか?

for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
    array[j + 1] = array[j];
}   
array[j + 1] = value;

あなたの例では、[j] = valueと[j + 1] = valueのどちらなのかがわかりません。手動で評価を開始する時間:

配列の長さが0の場合はどうなりますか?答えは明らかです。rightIndexは(length-1)== -1でなければならないため、jは-1から始まるため、インデックス0に挿入するには1を追加する必要があります。

したがって、最終条件が正しいことを証明しましたが、ループの内部は証明しませんでした。

1要素、10の配列があり、5を挿入しようとするとどうなりますか?単一の要素では、rightIndexは0から開始する必要があります。したがって、ループを最初に実行するときはj = 0なので、「0> = 0 && 10> 5」となります。インデックス0に5を挿入するため、10はインデックス1に移動する必要があるため、array [1] = array [0]になります。これはjが0のときに起こるため、array [j + 1] = array [j + 0]です。

大きな配列を想像して、任意の場所に挿入するとどうなるかを想像すると、脳が圧倒されるでしょう。ただし、単純な0/1/2サイズの例に固執する場合は、簡単にメンタルランを実行し、境界条件がどこで壊れるかを簡単に確認できるはずです。

フェンスポストの問題を聞いたことがないと想像してください。100本のフェンスポストが直線上にあり、それらの間にいくつのセグメントがあるか教えてください。頭の中に100のフェンスポストを想像しようとすると、ただ圧倒されてしまいます。それでは、有効なフェンスを作るために最も少ないフェンスの投稿は何ですか?フェンスを作るには2つ必要なので、2つの投稿を想像してください。投稿間の1つのセグメントの精神的なイメージが非常に明確になります。ポストやセグメントを数えてそこに座っている必要はありません。なぜなら、あなたは問題をあなたの脳にとって直感的に明らかなものにしたからです。

正しいと思ったら、テストを実行して、コンピューターが思ったとおりに動作することを確認するのは良いことですが、その時点では、それは形式的なものにすぎません。


4
私は本当に好きfor (int i = 0; i < length; i++)です。その習慣になったら、<=同じくらい頻繁に使用をやめ、ループが簡単になると感じました。しかしfor (int i = length - 1; i >= 0; i--)、以下と比較して、非常に複雑に思えますfor (int i=length; i--; )(おそらく、スコープ/ライフをより小さくwhileしようとしてiいない限り、ループとして書く方が賢明でしょう)。結果は、i == length-1(最初)からi == 0でループを実行しますが、機能の違いはwhile()、i =の代わりに、ループの後にバージョンがi == -1で終了することです(存在する場合) = 0。
TOOGAM

2
@TOOGAM(int i = length; i--;)は、0がfalseと評価されるためC / C ++で機能しますが、すべての言語がその同等性を持つわけではありません。i
ブライスワーグナー

当然、必要> 0な機能を取得するために「」を必要とする言語を使用する場合、そのような文字はその言語で必要であるため使用する必要があります。依然として、さらにこれらの場合に、単に「使用> 0」の2つのプロセス行うより簡単である次いでものを減算し、そしてまた、使用して「>= 0」。少しの経験でそれを知った後>= 0、ループテスト条件で等号(たとえば " ")を使用する頻度がはるかに少なくなる傾向があり、それ以降、結果のコードは一般に単純に感じられました。
TOOGAM

1
@BryceWagnerあなたがする必要があるならi-- > 0、古典的なジョークを試してみてくださいi --> 0
-porglezomp

3
@porglezompああ、そうですgoes to operatorです。C、C ++、Java、C#を含むほとんどのCライクな言語にはそれがあります。
CVn

11

頭の中に正しいコンピューティングモデルがないために、私はあまりにもイライラしました。

この質問の非常に興味深いポイントであり、このコメントを生成しました:-

唯一の方法があります。問題をよりよく理解することです。しかし、それはあなたの質問と同じくらい一般的です。– トーマスジャンク

...そしてトーマスは正しい。関数の明確な意図がないことは、赤旗であるはずです-すぐに停止し、鉛筆と紙をつかみ、IDEから離れ、問題を適切に分解する必要があることを明確に示します。または少なくともあなたがやったことの健全性チェック。

著者が問題を完全に定義する前に実装を定義しようとしたため、完全に混乱した非常に多くの関数とクラスを見てきました。そして、それはとても簡単に対処できます。

問題を完全に理解していない場合、最適なソリューションをコーディングすることも(効率または明確さの点で)ありそうになく、TDD方法論で本当に有用な単体テストを作成することもできません。

ここにあなたのコードを例として考えてください、それはあなたがまだ考慮していないいくつかの潜在的な欠陥を含んでいます:-

  • rightIndexが低すぎる場合はどうなりますか?(手がかり:それはされますデータの損失を伴います)
  • rightIndexが配列の境界外にある場合はどうなりますか?(例外が発生しますか、それともバッファオーバーフローが発生しましたか?)

コードのパフォーマンスと設計に関連する他の問題がいくつかあります...

  • このコードはスケーリングする必要がありますか?配列を並べ替えたままにしておくのが最良のオプションですか、それとも他のオプション(リンクリストなど)を確認する必要がありますか?
  • あなたの仮定を確信できますか?(配列がソートされることを保証できますか?そうでない場合はどうですか?)
  • あなたは車輪を再発明していますか?配列の並べ替えはよく知られている問題ですが、既存のソリューションを検討しましたか?あなたの言語(SortedList<t>C#など)で既に利用可能なソリューションはありますか?
  • 一度に1つの配列エントリを手動でコピーする必要がありますか?またはあなたの言語はJScriptのような一般的な機能を提供していますArray.Insert(...)か?このコードはより明確になりますか?

このコードを改善する方法はたくさんありますが、このコードに必要なことを適切に定義するまでは、コードを開発するのではなく、機能することを期待して一緒にハッキングするだけです。それに時間を費やすとあなたの人生は楽になります。


2
インデックスを既存の関数(Array.Copyなど)に渡す場合でも、バインドされた条件を正しく取得するための検討が必要になる場合があります。長さ0、長さ1、長さ2の状況で何が起こるかを想像することは、コピーが少なすぎたり多すぎたりしないようにするための最良の方法です。
ブライスワーグナー

@BryceWagner-確かに本当ですが、実際に解決している問題が何なのか明確な考えがなければ、「ヒットアンドホープ」戦略で暗闇の中でスラッシングすることに多くの時間を費やすことになりますこの時点で最大の問題。
ジェームス・スネル

2
@CodeYogi-あなたは持っているし、他の人が指摘したように、あなたは問題をかなり悪いサブ問題に分割しているので、それを回避する方法として問題を解決するあなたのアプローチに多くの答えが言及している理由です。それはあなたが個人的にとるべきものではなく、そこにいた私たちの経験からだけです。
ジェームス・スネル

2
@CodeYogi、このサイトをStack Overflowと混同していると思います。このサイトは、コンピューター端末ではなく、ホワイトボードでの Q&Aセッションに相当します。「コードを見せて」は、あなたが間違ったサイトにいることを示す非常に明確な兆候です。
ワイルドカード

2
@Wildcard +1:「コードを見せて」は、私にとって、この答えが正しい理由に関する優れた指標であり、多分、それができるのは人的要因/設計の問題であることをより良く実証する方法に取り組む必要があるということです人間のプロセスの変化によって対処される-コードの量はそれを教えることができません。
ジェームス・スネル

10

オフバイワンエラーは、最も一般的なプログラミングの間違いの1つです。経験豊富な開発者でさえ、これを時々間違っています。高レベルの言語には、通常、明示的なインデックス作成を完全に回避する、foreachまたはmap回避するような反復構造があります。ただし、例のように、明示的なインデックス付けが必要な場合があります。

課題は、配列セルの範囲をどのように考えるかです。明確なメンタルモデルがなければ、エンドポイントを含めるか除外するかがわかりにくくなります。

配列の範囲を記述する場合、下限は上限含め、上限は除外します。たとえば、範囲0..3はセル0、1、2です。この規則は、たとえば、0インデックス付きの言語で使用されてslice(start, end)JavaScriptでの方法は、インデックスから始まる部分配列を返すstartまでは含まないインデックスをend

範囲インデックスを配列セルのエッジを記述するものと考えると、より明確になります。以下の図は長さ9の配列であり、セルの下の数字は端に揃えられており、配列セグメントの説明に使用されています。たとえば、範囲2..5がセル2,3,4であるよりも、図から明らかです。

┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │   -- cell indexes, e.g array[3]
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
0   1   2   3   4   5   6   7   8   9   -- segment bounds, e.g. slice(2,5) 
        └───────────┘ 
          range 2..5

このモデルは、配列の長さが配列の上限になることと一致しています。長さ5の配列にはセル0..5があります。つまり、5つのセル0、1、2、3、4があります。これは、セグメントの長さが上限から下限を引いたものであることも意味します。つまり、セグメント2..5のセルは5-2 = 3です。

上下に反復するときにこのモデルを念頭に置くと、エンドポイントを含めるか除外するかがより明確になります。上方向に反復する場合、開始点を含める必要がありますが、終了点は除外する必要があります。下方向に反復する場合、開始点(上限)を除外し、終了点(下限)を含める必要があります。

コードを下向きに繰り返しているため、下限0を含める必要がありますj >= 0

これを考えると、rightIndex引数がサブ配列の最後のインデックスを表すように選択すると、規則に違反します。つまり、反復に両方のエンドポイント(0とrightIndex)を含める必要があります。また、空のセグメント(並べ替えを開始するときに必要なセグメント)を表すことが難しくなります。実際には、最初の値を挿入するときにrightIndexとして-1を使用する必要があります。これはかなり不自然に思えます。rightIndexセグメントの後にインデックスを指定する方が自然なように見えるため、0は空のセグメントを表します。

もちろん、ソートされたサブ配列を1つで拡張し、最初にソートされたサブ配列の直後のアイテムを上書きするため、コードは非常に混乱します。したがって、インデックスjから読み取りますが、値をj + 1に書き込みます。ここで、jが挿入前の初期サブ配列内の位置であることを明確にする必要があります。インデックス操作が面倒になると、グリッドペーパーに図を描くのに役立ちます。


4
@CodeYogi:小さなアレイを紙にグリッドとして描画し、鉛筆でループの反復を手動で実行します。これにより、実際に何が起こるか、たとえばセル範囲が右にシフトしたこと、新しい値が挿入された場所などを明確にすることができます。
ジャックB

3
「コンピュータサイエンスには、キャッシュの無効化、命名、およびオフバイワンエラーの2つの難しいことがあります。」
デジタル外傷

1
@CodeYogi:私が話していることを示す小さな図を追加しました。
ジャックB

1
特に最後の2つのパーを読むのは素晴らしい洞察です、混乱はforループの性質によるものです
CodeYogi

1
非常に。優れた。回答。また、この包括的/排他的インデックス規則は、myArray.Lengthor の値によっても動機付けられます。orの値myList.Countは、常にゼロベースの「右端」インデックスよりも1つ多くなります。...説明の長さは、これらの明示的なコーディングヒューリスティックの実用的で単純な応用に反しています。TL; DRの群衆は見逃されています。
レーダーボブ

5

あなたの質問の紹介は、あなたが適切にコーディングすることを学んでいないと思います。数週間以上命令型言語でプログラミングしている人は、実際に90%以上のケースで初めてループ境界を正しく取得する必要があります。おそらく、問題を十分に検討する前に、コーディングを開始しようと急いでいるのでしょう。

ループの書き方を(再)学習することでこの欠陥を修正することをお勧めします-紙と鉛筆でさまざまなループを数時間作業することをお勧めします。これを行うには、午後を休んでください。それから、あなたが本当にそれを得るまで、45分かそこらの時間をトピックに取り組んでいます。

それはすべて非常によくテストされていますが、通常ループ境界(およびコードの残りの部分)が適切に得られることを期待してテストする必要があります。


4
OPのスキルについてはあまり積極的ではありません。特に採用面接のようなストレスの多い状況では、境界の間違いを犯すのは簡単です。経験豊富な開発者もこれらの間違いをする可能性がありますが、当然のことながら、経験豊富な開発者は最初にテストを通じてそのような間違いを回避します。
Arseni Mourzenko

3
@MainMa-Markはもっと敏感だったかもしれませんが、彼は正しいと思います-インタビューのストレスがあり、問題を定義することを十分に考慮せずにコードをハッキングするだけです。方法の質問が非常に強く、後者にポイントを言葉で表現し、それが最高ではないIDEで離れてハッキングによって、あなたは強固な基盤を持っていることを確認することにより、長期的に解決することができるものです
ジェームズスネル

@JamesSnellあなたはあなた自身について自信を取り戻していると思います。コードを見て、その下に文書化されていると思うものを教えてください?あなたがはっきりと見るならば、私が問題を解決することができなかったと言及されたどこもありませんか?同じ間違いを繰り返さないようにする方法を知りたかっただけです。一度にすべてのプログラムを修正できると思います。
CodeYogi

4
@CodeYogi「試行錯誤」をする必要があり、コーディングに「イライラ」し、「同じ間違いを犯している」場合は、書き始める前に問題を十分に理解していない兆候です。あなたはそれを理解していないと言っている人はいませんが、あなたのコードはよりよく考えられていて、苦労している兆候であり、あなたはそれを拾い上げて学ぶかどうかを選択できます。
ジェームス・スネル

2
@CodeYogi ...そして、あなたが尋ねるので、私はコードを書く前に達成する必要があることを明確に理解することを重視しているので、ループと分岐が間違っていることはめったにありません。ソートされたような簡単なことを行うのは難しくありません配列クラス。プログラマーとして最も難しいことの1つは、自分が問題であることを認めることですが、それをするまでは、本当に良いコードを書き始めることはありません。
ジェームス・スネル

3

おそらく、私は私のコメントにいくつかの肉を置くべきです:

唯一の方法があります。問題をよりよく理解することです。しかし、それはあなたの質問と同じくらい一般的です

あなたのポイントは

試行錯誤の結果、仮定は正しいものになりましたが、頭の中に正しいコンピューティングモデルがないためにイライラしすぎました。

を読むtrial and errorと、アラームのベルが鳴り始めます。もちろん、私たちの多くは心の状態を知っています。小さな問題を修正したいとき、頭を他のものに巻きつけ、何らかの方法で推測し始めて、コードseemを実行させるために何をすべきかを考えます。いくつかのハック的な解決策はこれから出てきます-そしてそれらのいくつかは純粋な天才です。正直言って、それらのほとんどはそうではありません。私も含めて、この状態を知っています。

具体的な問題とは別に、改善方法について質問しました。

1)テスト

それは他の人によって言われました、そして、私は追加する価値がある何もありません

2)問題分析

それにアドバイスをするのは難しい。私があなたに与えることができる2つのヒントのみがあり、おそらくあなたはそのトピックに関するあなたのスキルを向上させるのに役立ちます

  • 明白かつ最も些細なものは、長期的には最も効果的です:多くの問題を解決します。練習して繰り返しながら、将来のタスクに役立つ考え方を開発します。プログラミングは、一生懸命練習することで改善される他のアクティビティのようなものです

コードカタスは方法であり、少し役立つかもしれません。

どのようにして素晴らしいミュージシャンになりますか?理論を知り、楽器の仕組みを理解するのに役立ちます。才能があると助かります。しかし、最終的に、偉大さは練習から生まれます。フィードバックを使用して毎回良くなるために、理論を繰り返し適用します。

コードカタ

私がとても気に入っているサイト:Code Wars

チャレンジを通じて習得を達成する他の人と実際のコードチャレンジについてトレーニングすることでスキルを向上させる

これらは比較的小さな問題であり、プログラミングスキルを磨くのに役立ちます。そして、私がCode Warsで一番気に入っているのは、あなたのソリューションを他のソリューションと比較できることです。

または、コミュニティからフィードバックを得るExercism.ioをご覧ください。

  • 他のアドバイスは、ささいなことです:問題を分解する ことを学ぶ問題を本当に小さな問題に分解するために、自分で訓練する必要があります。ループの記述に問題があると言うと、間違いを犯し、ループ全体を構築物として認識し、それを断片に分解しないということです。ステップごとに物事を分解することを学ぶなら、あなたはそのような間違いを避けることを学びます。

上で言ったように、時々あなたはそのような状態にあることを知っています-「単純な」ものをより「死んだ単純な」タスクに分割するのは難しいことです。しかし、それは大いに役立ちます。

専門的なプログラミングを初めて学んだとき、コードのデバッグに大きな問題があったことを覚えています。なにが問題だったの?HYBRIS -私はので、エラーは、コードのようにしてこのような領域にすることはできません知っている、それはできないこと。そして結果として?コードを分析する代わりに、コードをざっと目を通しました。 コードを命令ごとに分解するのが面倒だったとしても、学ぶ必要がありました。

3)Toolbeltを開発する

あなたの言語とツールを知っていることに加えて、これらは開発者が最初に考える光沢のあるものであることを知っています- アルゴリズム(リーディング)を学びます。

開始する2冊の本は次のとおりです。

これは、料理を始めるためのいくつかのレシピを学ぶようなものです。最初は何をすべきかわからないので、前のシェフがあなたのためにどのような料理をしたかを見てください。同じことがアルゴリズムについても言えます。アルゴリズムは、一般的な食事の調理レシピ(データ構造、並べ替え、ハッシュなど)に似ています。それらを暗に知っている(少なくとも試している)場合は、良い出発点があります。

3a)プログラミング構造を知っている

この点は派生物です-つまり あなたの言語を知ってください-そして、より良い:あなたの言語でどんな構成が可能であるかを知ってください。

悪いまたは非効率的なコードの共通点は、プログラマーが異なる種類のループ(for-while-およびdo-loops)の違いを知らないことです。それらは何らかの形ですべて互換的に使用可能です。しかし、状況によっては、別のループ構造を選択すると、よりエレガントなコードになります。

そして、ダフのデバイスがあり ......

PS:

そうでなければ、あなたのコメントはドナルドトランプよりも良くありません。

はい、コーディングを再び素晴らしいものにする必要があります!

Stackoverflowの新しいモットー。


ああ、非常に真剣に一つのことを話させてください。私はあなたが言及したことすべてを行ってきました、そして、それらのサイトであなたに私のリンクさえ与えることができます。しかし、私にとってイライラしているのは、質問に対する答えを得る代わりに、プログラミングに関するあらゆる可能なアドバイスを得ていることです。たった一人の人がこれpre-postまで条件について言及してきたが、私はそれを感謝している。
CodeYogi

あなたが言ったことから、あなたの問題がどこにあるか想像することは困難です。おそらく比metaが役立ちます:私にとっては、「どうやって見ることができる」と言うようなものです-私にとって明白な答えは「あなたの目を使う」ことです。あなたの質問にも同じことが言えます。
トーマスジャンク

「試行錯誤」に関する警告ベルに完全に同意します。問題解決の考え方を完全に学ぶ最良の方法は、紙と鉛筆でアルゴリズムとコードを実行することだと思います。
ワイルドカード

ええと...なぜプログラミングに関するあなたの答えの途中で、文脈なしに引用された政治的候補者に、文法的に貧弱なジャブがありますか?
ワイルドカード

2

私が問題を正しく理解していれば、あなたの質問は、ループが正しいことを確認する方法ではなく、最初の試行からループを取得する方法を考えることです(他の回答で説明されているように答えはテストされます)。

良いアプローチだと思うのは、ループなしで最初の反復を書くことです。これを行った後、反復間で何を変更する必要があるかに気付くはずです。

0や1のような数字ですか?それから、ほとんどの場合、forとbingoが必要です。次に、同じことを何回実行したいかを考えます。そうすれば、終了条件も得られます。

実行回数が正確にわからない場合は、forは必要ありませんが、whileまたはdo whileは必要ありません。

技術的には、どのループも他のループに変換できますが、正しいループを使用するとコードが読みやすくなります。ここにいくつかのヒントを示します。

  1. for()の中にif(){...; break;}を書いていることに気付いたら、しばらく時間が必要です。

  2. 「While」は、おそらくどの言語でも最もよく使用されるループですが、imoであってはなりません。bool ok = Trueと書いていることに気づいたら、while(check){何かをし、うまくいけばある時点で[ok]に変更する}; しばらくは必要ありませんが、最初の反復を実行するために必要なものがすべて揃っているため、しばらくは必要です。

ちょっとした文脈...最初にプログラミングを学んだとき(Pascal)、私は英語を話せませんでした。私にとって、「for」と「while」はあまり意味がありませんでしたが、「repeat」(Cでdo while)キーワードは母国語でほぼ同じなので、すべてに使用します。私の意見では、繰り返し(do while)が最も自然なループです。なぜなら、ほとんどの場合、何かを実行してから、目標に到達するまで何度も繰り返し実行するからです。"For"は、イテレータを提供し、コードの先頭に条件を奇妙に配置するだけのショートカットですが、ほとんどの場合、何かが起こるまで何かを実行したいと考えています。また、whileはif(){do while()}の単なるショートカットです。ショートカットは後で便利ですが、


2

事前/事後条件と不変条件を使用して正しいループを作成する方法のより詳細な例を示します。このようなアサーションをまとめて、仕様または契約と呼びます。

ループごとにこれを行うことをお勧めするわけではありません。しかし、関係する思考プロセスを見ることが役立つと思います。

そのために、メソッドをMicrosoft Dafnyというツールに変換します。MicrosoftDafnyは、このような仕様の正確性を証明するように設計されています。また、各ループの終了もチェックします。Dafnyにはforループがないため、while代わりにループを使用する必要があることに注意してください。

最後に、このような仕様を使用して、ほぼ間違いなく簡単なバージョンのループを設計する方法を示します。この単純なループバージョンには、実際にはループ条件j > 0と割り当てarray[j] = valueがあります-最初の直感と同じです。

Dafnyは、これらのループの両方が正しいことを証明し、同じことを行います。

次に、経験に基づいて、正しい逆方向ループを記述する方法について一般的な主張を行います。これは、将来このような状況に直面した場合に役立つでしょう。

パート1-メソッドの仕様を記述する

私たちが直面する最初の課題は、メソッドが実際に行うべきことを決定することです。このため、メソッドの動作を指定する事前条件と事後条件を設計しました。仕様をより正確にするために、value挿入されたインデックスを返すようにメソッドを拡張しました。

method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  // the method will modify the array
  modifies arr
  // the array will not be null
  requires arr != null
  // the right index is within the bounds of the array
  // but not the last item
  requires 0 <= rightIndex < arr.Length - 1
  // value will be inserted into the array at index
  ensures arr[index] == value 
  // index is within the bounds of the array
  ensures 0 <= index <= rightIndex + 1
  // the array to the left of index is not modified
  ensures arr[..index] == old(arr[..index])
  // the array to the right of index, up to right index is
  // shifted to the right by one place
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  // the array to the right of rightIndex+1 is not modified
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])

この仕様は、メソッドの動作を完全にキャプチャします。この仕様に関する私の主な観察は、手順が値rightIndex+1でなく渡されるならそれが簡素化されることですrightIndex。しかし、このメソッドがどこから呼び出されたのかわからないため、その変更がプログラムの残りの部分にどのような影響を与えるかはわかりません。

パート2-ループ不変量の決定

メソッドの動作の仕様ができたので、ループの実行の仕様を追加して、ループの実行が終了し、の最終状態になることをDafnyに確信させる必要がありarrayます。

以下は、ループ不変条件が追加されたDafny構文に変換された元のループです。また、値が挿入されたインデックスを返すように変更しました。

{
    // take a copy of the initial array, so we can refer to it later
    // ghost variables do not affect program execution, they are just
    // for specification
    ghost var initialArr := arr[..];


    var j := rightIndex;
    while(j >= 0 && arr[j] > value)
       // the loop always decreases j, so it will terminate
       decreases j
       // j remains within the loop index off-by-one
       invariant -1 <= j < arr.Length
       // the right side of the array is not modified
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       // the part of the array looked at by the loop so far is
       // shifted by one place to the right
       invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
       // the part of the array not looked at yet is not modified
       invariant arr[..j+1] == initialArr[..j+1] 
    {
        arr[j + 1] := arr[j];
        j := j-1;
    }   
    arr[j + 1] := value;
    return j+1; // return the position of the insert
}

これはDafnyで検証します。このリンクをたどると、自分で確認できます。そのため、ループは、パート1で書いたメソッド仕様を正しく実装しています。このメソッド仕様が本当に望んだ動作であるかどうかを判断する必要があります。

ここでDafnyが正確さの証拠を作成していることに注意してください。これは、テストで得られる可能性よりもはるかに強力な正確性の保証です。

パート3-より単純なループ

これで、ループの動作をキャプチャするメソッド仕様ができました。ループの動作を変更していないという自信を保ちながら、ループの実装を安全に変更できます。

ループの条件と最終値に関する元の直感に一致するようにループを変更しましたj。このループは、質問で説明したループよりも簡単だと思います。より頻繁に使用することができますjのではなくj+1

  1. でjを開始 rightIndex+1

  2. ループ条件を変更します j > 0 && arr[j-1] > value

  3. 割り当てを変更します arr[j] := value

  4. ループの始まりではなく、ループの終わりでループカウンターをデクリメントします

これがコードです。ループの不変式は今ややや書きやすいことに注意してください。

method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 0 <= rightIndex < arr.Length - 1
  ensures 0 <= index <= rightIndex + 1
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex+1;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

パート4-後方ループに関するアドバイス

かなりの数年にわたって多くのループを作成し、正しいことを証明した後、逆方向のループに関する次の一般的なアドバイスを受け取ります。

ほとんどの場合、ループの最後ではなく最初にデクリメントが実行される場合、逆方向(デクリメント)ループを考えて記述する方が簡単です。

残念ながら、for多くの言語のループ構造はこれを難しくしています。

この複雑さが、ループがどうあるべきか、実際に必要なものについての直感に違いを生じさせたのではないかと思う(しかし証明できない)。あなたは順方向(増分)ループについて考えることに慣れています。逆方向(デクリメント)ループを作成する場合、順方向(インクリメント)ループで発生する順序を逆にしようとしてループを作成しようとします。しかし、forコンストラクトの動作方法のため、割り当てとループ変数の更新の順序を逆にすることを怠っています。これは、逆方向ループと順方向ループの間で操作の順序を真に逆にするために必要です。

パート5-ボーナス

完全を期すために、rightIndex+1ではなくメソッドに渡す場合に取得するコードを以下に示しrightIndexます。この変更により+2、ループの正確性について考えるために必要なすべてのオフセットが排除されます。

method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 1 <= rightIndex < arr.Length 
  ensures 0 <= index <= rightIndex
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
  ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
       invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

2
あなたが投票した場合、コメントを本当に感謝します
flamingpenguin

2

この権利を取得する簡単かつ流暢にすることは経験の問題です。言語では直接表現できない場合や、単純な組み込みのものが処理できるよりも複雑なケースを使用している場合でも、「敬eachな順序で各要素を1回訪問する」などの高レベルだと思います経験豊富なコーダーは、それを何度も行っているため、すぐに適切な詳細に変換します。

その場合でも、より複雑なケースでは、間違いを犯しやすいです。なぜなら、あなたが書いているものは、通常、缶詰の一般的なものではないからです。より近代的な言語とライブラリでは、簡単なことを書くことはできません。それは、定型化されたコンストラクトまたはそのための呼び出しがあるからです。C ++では、現代のマントラは「コードを書くのではなくアルゴリズムを使用する」ことです。

したがって、特にこの種のことに関して、それが正しいことを確認する方法は、境界条件を調べることです。物事が変わる場所の端にあるいくつかのケースについて、頭の中のコードをたどってください。の場合index == array-max、どうなりますか?どうmax-1?コードが間違った方向に進んだ場合、これらの境界のいずれかになります。一部のループでは、最初または最後の要素、およびループ構造が境界を正しく取得することを心配する必要があります。たとえば、参照した場合、最小値は何にa[I]なりますか?a[I-1]I

また、(正しい)反復の数が極端な場合を見てください。境界が満たされていて、反復が0になる場合、それは特別なケースなしで機能しますか?そして、下限が同時に上限である、1回の反復についてはどうでしょうか。

エッジケース(各エッジの両側)を調べることは、ループを記述するときに行うべきことであり、コードレビューで行うべきことです。


1

私は、すでに述べたトピックについては、すでに明確にしておくつもりです。

そのような間違いを避けるためのツール/メンタルモデルとは何ですか?

道具

私にとっては、より良い書き込むための最大のツールforwhileループはいずれも書くことではないforか、while全くループします。

現代のほとんどの言語は、何らかの方法でこの問題をターゲットにしています。たとえば、Java Iteratorは最初から使用していましたが、使用するのが少し不格好でしたが、ショートカット構文を導入して、それらをより簡単にリリースできるようにしました。C#も同様です。

私の現在好まれた言語、Rubyは、(機能的なアプローチに取った.each.mapなど)フルフロントを。これは非常に強力です。私が取り組んでいるRubyのコードベースで簡単に数えたところです。約10000行のコードには、ゼロforと約5がありwhileます。

新しい言語の選択を余儀なくされた場合、そのような機能/データベースのループを探すことは優先順位リストで非常に高くなります。

メンタルモデル

それwhileがあなたが得ることができる抽象の最小の最小値であることに留意してくださいgoto。私の意見でforは、ループの3つの部分すべてをしっかりとまとめているため、改善するのではなく、さらに悪化させます。

だから、私forが使用されている環境にいる場合、3つの部分すべてが完全にシンプルで常に同じであることを気にします。これは、私が書くことを意味します

limit = ...;
for (idx = 0; idx < limit; idx++) { 

しかし、それほど複雑なものはありません。たぶん、時々カウントダウンがありますが、それを避けるために最善を尽くします。

を使用する場合while、ループ状態に関係する複雑な内側のシェナンニガンは明らかになりません。内部のテストwhile(...)は可能な限り単純になり、できる限り回避breakします。また、ループは短く(コードの行を数える)、大量のコードは除外されます。

特に、実際のwhile条件が複雑な場合は、非常に見つけやすい「条件変数」を使用し、条件をwhileステートメント自体に配置しません。

repeat = true;
while (repeat) {
   repeat = false; 
   ...
   if (complex stuff...) {
      repeat = true;
      ... other complex stuff ...
   }
}

(または、そのようなものは、もちろん正しい尺度で。)

これにより、「この変数は0から10まで単調に実行されている」または「この変数がfalse / trueになるまでこのループが実行される」という非常に簡単なメンタルモデルが得られます。ほとんどの脳は、このレベルの抽象化をうまく処理できるようです。

お役に立てば幸いです。


1

特に、逆ループは、一般的なforループ構文とゼロベースのハーフオープン間隔の使用の両方で、プログラミング言語の多くが順方向反復に偏っているため、推論するのが難しい場合があります。言語がそれらの選択をしたのは間違っていると言っているのではありません。これらの選択は、逆ループについて考えることを複雑にしていると言っています。

一般的に、forループはwhileループの周りに構築された単なる構文上のシュガーであることに注意してください。

// pseudo-code!
for (init; cond; step) { body; }

以下と同等です:

// pseudo-code!
init;
while (cond) {
  body;
  step;
}

(おそらく、initステップで宣言された変数をループのローカルに保持するために、追加のスコープ層を使用します)。

これは多くの種類のループに適していますが、後方に歩いているときに最後のステップをとるのは厄介です。逆方向に作業する場合、次のように、必要な値のの値でループインデックスを開始し、そのstep部分をループの先頭に移動する方が簡単です。

auto i = v.size();  // init
while (i > 0) {  // simpler condition because i is one after
    --i;  // step before the body
    body;  // in body, i means what you'd expect
}

または、forループとして:

for (i = v.size(); i > 0; ) {
    --i;  // step
    body;
}

ステップ式はヘッダーではなく本文にあるため、これは不安に思われるかもしれません。これは、forループ構文の固有の順方向バイアスの不幸な副作用です。このため、代わりにこれを行うと主張する人もいます。

for (i = v.size() - 1; i >= 0; --i) {
    body;
}

ただし、インデックス変数が符号なしの型である場合(CまたはC ++の場合のように)、これは災害です。

これを念頭に置いて、挿入関数を作成しましょう。

  1. 後方に作業するので、ループインデックスを「現在の」配列スロットの後のエントリにします。ほとんどのプログラミング言語では半開きの範囲が範囲を表す自然な方法であり、再配列せずに空の配列を表す方法を提供するため、最後の要素へのインデックスではなく整数のサイズを取る関数を設計します-1のような魔法の値に。

    function insert(array, size, value) {
      var j = size;
    
  2. 新しい値は前の要素よりも小さいですが、シフトし続けます。そこ場合はもちろん、前の要素のみを確認することができている前の要素は、私たちはまず、我々は非常に最初にじゃないことを確認する必要があります。

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. これはj、新しい値が必要な場所にそのまま残ります。

      array[j] = value; 
    };
    

Jon BentleyによるプログラミングPearlsは、挿入ソート(およびその他のアルゴリズム)の非常に明確な説明を提供します。これは、これらの種類の問題に対するメンタルモデルの構築に役立ちます。


0

forループが実際に何をするのか、それがどのように機能するのかについて単純に混乱していますか?

for(initialization; condition; increment*)
{
    body
}
  1. 最初に、初期化が実行されます
  2. その後、条件がチェックされます
  3. 条件が真の場合、ボディは1回実行されます。#6に移動しない場合
  4. 増分コードが実行されます
  5. 後藤#2
  6. ループの終わり

別の構成を使用してこのコードを書き換えると便利な場合があります。whileループを使用した場合も同じです。

initialization
while(condition)
{
    body
    increment
}

その他の提案を次に示します。

  • foreachループのような別の言語構造を使用できますか?これにより、条件が処理され、ステップが増分されます。
  • マップまたはフィルター機能を使用できますか?一部の言語には、これらの名前を持つ関数があり、これらは内部的にコレクションをループします。コレクションと本文を指定するだけです。
  • forループに慣れるにはもっと時間をかける必要があります。常に使用します。デバッガーでforループをステップ実行して、実行方法を確認することをお勧めします。

*「インクリメント」という用語を使用しているが、本文の後で、条件チェックの前にあるのは単なるコードであることに注意してください。デクリメントでも、まったくない場合もあります。


1
どうして下票?
user2023861

0

追加の洞察の試み

以下のために非自明なアルゴリズムのループと、次の方法を試みることができます:

  1. 4つの位置を持つ固定配列作成し、いくつかの値を入れて問題をシミュレートします。
  2. ためにあなたのアルゴリズム書く与えられた問題を解決するため任意のループなしハードコーディングされたindexingsとします
  3. その後、コード内のハードコードされたインデックスを変数iまたはで置き換えj、必要に応じてこれらの変数をインクリメント/デクリメントします(ただし、ループはありません)。
  4. コードを書き直し、繰り返しブロックをループ内に入れて、事前条件と事後条件を満たします。
  5. [ オプション ]ループを希望の形式(for / while / do while)に書き換えます。
  6. 最も重要なのは、アルゴリズムを正しく記述することです。その後、必要に応じてコード/ループをリファクタリングおよび最適化します(ただし、これにより、読者にとってコードが非自明になる可能性があります)

あなたの問題

//TODO: Insert the given value in proper position in the sorted subarray
function insert(array, rightIndex, value) { ... };

ループの本体を手動で複数回記述します

4つの位置を持つ固定配列を使用して、ループなしで手動でアルゴリズムを記述してみましょう。

           //0 1 2 3
var array = [2,5,9,1]; //array sorted from index 0 to 2
var leftIndex = 0;
var rightIndex = 2;
var value = array[3]; //placing the last value within the array in the proper position

//starting here as 2 == rightIndex

if (array[2] > value) {
    array[3] = array[2];
} else {
    array[3] = value;
    return; //found proper position, no need to proceed;
}

if (array[1] > value) {
    array[2] = array[1];
} else {
    array[2] = value;
    return; //found proper position, no need to proceed;
}

if (array[0] > value) {
    array[1] = array[0];
} else {
    array[1] = value;
    return; //found proper position, no need to proceed;
}

array[0] = value; //achieved beginning of the array

//stopping here because there 0 == leftIndex

書き換え、ハードコードされた値の削除

//consider going from 2 to 0, going from "rightIndex" to "leftIndex"

var i = rightIndex //starting here as 2 == rightIndex

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

//stopping here because there 0 == leftIndex

ループに変換する

while

var i = rightIndex; //starting in rightIndex

while (true) {
    if (array[i] > value) { //refactor: this can go in the while clause
        array[i+1] = array[i];
    } else {
        array[i+1] = value;
        break; //found proper position, no need to proceed;
    }

    i--;
    if (i < leftIndex) { //refactor: this can go (inverted) in the while clause
        array[i+1] = value; //achieved beginning of the array
        break;
    }
}

ループを希望する方法でリファクタリング/リライト/最適化します。

while

var i = rightIndex; //starting in rightIndex

while ((array[i] > value) && (i >= leftIndex)) {
    array[i+1] = array[i];
    i--;
}

array[i+1] = value; //found proper position, or achieved beginning of the array

for

for (var i = rightIndex; (array[i] > value) && (i >= leftIndex); i--) {
    array[i+1] = array[i];
}

array[i+1] = value; //found proper position, or achieved beginning of the array

PS:コードは入力が有効であり、その配列に繰り返しが含まれていないことを前提としています。


-1

これが、可能な限り、より抽象化された操作を優先して、生のインデックスで動作するループの作成を避ける理由です。

この場合、次のようなことを(擬似コードで)行います。

array = array[:(rightIndex - 1)] + value + array[rightIndex:]

-3

あなたの例では、ループの本体は非常に明白です。そして、最後にいくつかの要素を変更する必要があることは非常に明白です。したがって、コードを記述しますが、ループの開始、終了条件、および最終割り当ては行いません。

次に、コードから離れて、実行する必要がある最初の動きを見つけます。最初の動きが正しいように、ループの開始を変更します。再びコードから離れて、実行する必要がある最後の動きを見つけます。最後の動きが正しくなるように、終了条件を変更します。最後に、コードから離れて、最終的な割り当てが何であるかを把握し、それに応じてコードを修正します。


1
例を挙げていただけますか?
CodeYogi

OPには例がありました。
gnasher729

2
どういう意味ですか?私はOPです。
CodeYogi
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.