web-dev-qa-db-ja.com

正しいループを書くには?

ほとんどの場合、ループを記述している間、私は通常、間違った境界条件(たとえば、間違った結果)を書くか、ループの終了に関する私の仮定が間違っています(たとえば、無限に実行するループ)。いくつかの試行錯誤の後で自分の仮定は正しかったが、頭の中に正しいコンピューティングモデルが欠けていたので、私は苛立ちすぎた。

/**
 * 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であるか、混乱しました。

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

64
CodeYogi

テスト

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

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

複雑にしないでおく

他の人が一目でそれが何をするのかを見ることができない場合、あなたはそれをあまりにも難しくしています。わかりやすいものを書けるのであれば、無視してもかまいません。本当に必要な万が一の場合にのみ、速度を上げてください。そしてそれでも、何があなたを遅くしているのかを正確に知っていることが確実になったときだけです。実際の Big O 改善を達成できる場合、このアクティビティは無意味ではないかもしれませんが、それでも、コードをできるだけ読みやすくします。

1つずつオフ

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

コメント

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

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

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

抽象化

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

略称

寿命の短いものには短い名前を使用します。 iは、小さなスコープ内のニースタイトループ内のインデックスの明確な名前であり、意味を明確にします。 iiと混同される可能性のある他のアイデアや名前と次々に広まるのに十分な長さの場合、iに素敵な長い説明的な名前を付けます。 。

長い名前

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

空白

欠陥は判読不可能なコードに隠れるのが大好きです。あなたの言語があなたにあなたのインデントスタイルを選択させるのであれば、少なくとも一貫性があります。コードをノイズのストリームのように見せないでください。コードはフォーメーションで行進しているように見えるはずです。

ループ構造

あなたの言語のループ構造を学び、復習してください。 デバッガーがfor(;;)ループを強調表示するのを見る は非常に有益です。すべてのフォームをご覧ください。 while、_do while_、while(true)、_for each_。あなたが逃げることができる最も簡単なものを使ってください。ルックアップ ポンプのプライミングbreakcontinueがある場合はどうするかを学びます。 _c++_と_++c_の違いを理解する。閉鎖する必要があるすべてのものを常に閉鎖する限り、早く戻ることを恐れないでください。 最後にブロックする または、できればそれを開いたときに自動的に閉じるようにマークするもの: ステートメントの使用 / リソースで試す

代替ループ

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

ああ、テストして。

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

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

もう1つありました。思い出せない。 Tで始まった...

206
candied_orange

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

そして、(インデックスを使ったジャグリングなど)未知の領域を探索する場合、veryを考えるだけでなく、assertions

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

/**
 * 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]));
};

現在、ループは単純です(jrightIndexから0に移動します)。

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

  • 境界条件を考える(rightIndex == 0rightIndex == array.size - 2
  • valuearray[0]よりも小さい、またはarray[rightIndex]よりも大きいと考えてください
  • valuearray[0]array[rightIndex]またはいくつかの中間インデックスと等しいと考えてください

また、fuzzingを過小評価しないでください。間違いを見つけるためのアサーションがあるので、ランダム配列を生成し、メソッドを使用してそれをソートします。アサーションが発生した場合は、バグを発見したため、テストスイートを拡張できます。

73
Matthieu M.

単体テスト/ 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] → []_であるEdgeケースが正しく処理されるようにするか、境界の負の値が_[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;
}
_
28

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

インデックスが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] =値なのか[j + 1] =値なのかわからない。手動で評価を開始する時間:

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

したがって、ループの内側ではなく、最終的な条件が正しいことを証明しました。

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

大きな配列を想像してみて、任意の場所に挿入するとどうなるかを想像すると、おそらく頭がおかしくなります。しかし、単純な0/1/2サイズの例に固執する場合は、簡単なメンタルランスルーを実行して、境界条件がどこで崩れるかを確認するのは簡単です。

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

正しいと思ったら、それをテストで実行し、コンピューターが期待どおりに動作することを確認することをお勧めしますが、その時点では形式にすぎないはずです。

15
Bryce Wagner

頭の中に正しいコンピューティングモデルがないために、私は苛立ちすぎました。

この質問に対する非常に興味深い点であり、このコメントが生成されました:-

方法は1つだけです。問題をよりよく理解します。しかし、それはあなたの質問と同じくらい一般的です。 – Thomas Junk

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

作者が問題を完全に定義する前に実装を定義しようとしたため、完全に混乱している関数やクラスがたくさんあります。そして、それは対処するのがとても簡単です。

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

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

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

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

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

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

11
James Snell

1つずれたエラー は、最も一般的なプログラミングの誤りの1つです。経験豊富な開発者でさえ、これを間違えることがあります。高水準言語には通常、明示的なインデックス付けを完全に回避するforeachまたはmapのような反復構造があります。しかし、例のように、明示的なインデックス付けが必要な場合もあります。

問題は、配列セルのrangesをどのように考えるかです。明確なメンタルモデルがないと、エンドポイントを含めたり除外したりするタイミングがわかりにくくなります。

配列の範囲を説明するときの規則は、下限を含め、上限を除外することです。たとえば、範囲0..3はセル0、1、2です。この規則は、0インデックス付き言語全体で使用されます。たとえば、JavaScriptのslice(start, end)メソッドは、インデックスstartで始まるサブ配列を返します含まないインデックスend

範囲のインデックスをエッジの記述と考えると、より明確になりますbetween配列セル。以下の図は長さ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は挿入前の初期サブ配列内の位置であることを明確にする必要があります。インデックスの操作が難しくなりすぎると、グリッドペーパーに図を描くのに役立ちます。

10
JacquesB

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

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

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

多分私は私のコメントにいくつかの肉を置くべきです:

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

あなたのポイントは

いくつかの試行錯誤の後で自分の仮定は正しかったが、頭の中に正しいコンピューティングモデルが欠けていたので、私は苛立ちすぎた。

trial and error、私の警報ベルが鳴り始めます。もちろん、私たちの多くは心の状態を知っています。小さな問題を修正したいと思い、他のことに頭を抱えて、コードをseemにするために、何らかの方法で推測を始めたとき、何をすべきか。いくつかのハックな解決策はこれから生まれます-そしてそれらのいくつかは純粋ですgenius;しかし正直に言うと:それらのほとんどはそうではありません。この状態を知っている私が含まれています。

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

1)テスト

それは他の人から言われました、そして私は追加する価値があるものは何もないでしょう

2)問題分析

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

  • 明白で最も簡単なものは、長期的には最も効果的です。多くの問題を解決します。練習し、繰り返しながら、将来のタスクのために役立つ考え方を開発します。プログラミングは、ハードワークの練習によって改善される他のアクティビティと同様です

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

どのようにして素晴らしいミュージシャンになれるのですか?理論を理解し、楽器の力学を理解するのに役立ちます。それは才能を持つのに役立ちます。しかし、究極的には、偉大さは実践から生まれます。理論を何度も繰り返し適用し、フィードバックを使用して毎回改善します。

コードカタ

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

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

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

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

  • 問題を分解することを学ぶあなたは自分自身を訓練し、問題を本当に小さな問題に分解する必要があります。 ループの作成に問題があると言うと、ループ全体を構成体として見て、ループをバラバラに分解しないという誤りを犯します。あなたが物事を分解することを学ぶならステップバイステップ、あなたはそのような間違いを避けることを学ぶ。

私は知っています-先に述べたように、あなたは時々あなたはそのような状態にあります-「単純な」ものをより「完全に単純な」タスクに分解するのは難しいことです。しかし、それは非常に役立ちます。

私が初めて専門的にプログラミングを学習したとき、コードのデバッグで巨大な問題がありました。なにが問題だったの? Hybris-エラーがコードのそのような領域にあることはあり得ません。なぜなら、私はできないことを知っているからです。そして結果として? コードを分析するのではなく流用しました私は学ぶ必要がありました-コードを分解するのが面倒だったとしても命令

3)Toolbeltを開発する

あなたの言語とあなたのツールを知ることに加えて-私はこれらが開発者が最初に考える光沢のあるものであることを知っています-learn Algorithms(別名リーディング)。

まず、2冊の本があります。

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

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

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

不良または非効率的なコードの共通点は、プログラマーが異なるタイプのループ(for-while-およびdo- loops)。それらは何とかしてすべて互換的に使用できます。ただし、状況によっては、別のループ構造を選択すると、コードがより洗練されたものになります。

そして ダフのデバイス ...

PS:

それ以外の場合、あなたのコメントはドナルトランプと同じです。

はい、コーディングをもう一度素晴らしいものにすべきです!

Stackoverflowの新しいモットー。

3
Thomas Junk

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

すべてのループでこれを実行することをお勧めしません。しかし、私はあなたが関係する思考プロセスを見ることが有用であることを見つけてくれることを望みます。

そうするために、私はあなたの方法を Microsoft Dafnyと呼ばれるツール に翻訳します。これは、そのような仕様の正確さを証明するように設計されています。また、各ループの終了をチェックします。 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ではなくrightIndex+1の値が渡された場合は簡略化されることです。しかし、このメソッドがどこから呼び出されたかがわからないので、その変更がプログラムの残りの部分にどのような影響を与えるかはわかりません。

パート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で記述したメソッド仕様を正しく実装します。このメソッド仕様が本当に望んだ動作かどうかを判断する必要があります。

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

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

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

ループを変更して、ループ条件とjの最終的な値に関する元の直感と一致するようにしました。このループは、質問で説明したループよりも単純であると私は主張します。多くの場合、j+1ではなくjを使用できます。

  1. rightIndex+1でjを開始

  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ではなくrightIndex+1をメソッドに渡した場合に取得するコードを次に示します。この変更により、ループの正確さを考慮するために必要な+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

これを正しく簡単におよびスムーズにすることは経験の問題です。言語が直接それを表現することを許可していないか、または単純な組み込みのものが処理できるよりも複雑なケースを使用している場合でも、あなたが考える「各要素を1つの順序で一度に訪問する」のようなより高いレベルであり、経験豊富なコーダーは何度も行ったので、それを即座に正しい詳細に変換します。

それでも、あなたが書いているのは通常ではない缶詰の一般的なものではないため、より複雑なケースでは間違いが起こりやすくなります。より現代的な言語とライブラリーでは、缶詰の構成または呼び出しが存在するため、簡単なことを記述しないことができます。 C++では、現代のマントラは「コードを書くのではなくアルゴリズムを使用する」です。

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

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

Edgeケース(各Edgeの両側)を調べることは、ループを記述するときに行うべきこと、およびコードレビューで行うべきことです。

2
JDługosz

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

私が良いアプローチと考えるのは、ループなしで最初の反復を書くことです。これを実行した後、反復の間に何を変更する必要があるかを確認する必要があります。

0や1のような数字ですか?次に、おそらく、およびビンゴが必要になります。次に、同じことを何回実行したいかを考えてください。そうすれば、終了条件も得られます。

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

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

  1. もしfor()の中にif(){...; break;}を書いていることに気づいたら、しばらく時間がかかり、すでに条件を持っている

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

さて、ちょっとした文脈...私が最初にプログラミングを学んだとき(パスカル)、私は英語を話しませんでした。私にとっては、「for」と「while」はあまり意味がありませんでしたが、「repeat」(Cで行う)キーワードは母国語でほとんど同じなので、すべてに使用します。私の意見では、繰り返し(do while)は最も自然なループです。なぜなら、ほとんどの場合、何かが実行され、次に目標が達成されるまで繰り返し実行されるためです。 "For"はイテレータを提供するショートカットにすぎず、奇妙に条件をコードの先頭に配置しますが、ほとんどの場合、何かが発生するまで何かを実行したいと考えています。また、whileはif(){do while()}のショートカットにすぎません。ショートカットは後でいいですが、最初の試行からループを正しく作成する最も簡単な方法は、脳が考えているのと同じ方法でループを書くことです。停止します」というメッセージが表示され、必要に応じて適切なショートカットを使用します。

2
Andrei

特に、リバースループは、一般的なforループ構文とゼロベースのハーフオープンインターバルの両方の使用により、多くのプログラミング言語がフォワードイテレーションに偏っているので、推論が難しい場合があります。言語を選択するのが間違っていると言っているのではありません。私はこれらの選択が逆ループについて考えることを複雑にすることを言っているだけです。

一般に、forループは、whileループを中心に構築された構文上の砂糖であることを覚えておいてください。

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

以下と同等です。

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

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

これは多くの種類のループでは問題ありませんが、後ろに歩いているときは、最後のステップに来るのは面倒です。逆方向に作業するときは、次のように、ループインデックスを値afterで開始し、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. 逆方向に作業するので、ループインデックスを「現在の」配列スロットの後のエントリにします。ほとんどのプログラミング言語で範囲を表現するための半分開いた範囲が自然な方法であり、方法を提供するため、最後の要素のインデックスではなく整数のsizeをとるように関数を設計します-1のような魔法の値に頼らずに空の配列を表すため。

    function insert(array, size, value) {
      var j = size;
    
  2. 新しい値はprevious要素よりも小さい間、シフトを続けます。もちろん、前の要素はis前の要素が存在する場合にのみチェックできるため、最初に、最初でないことを確認する必要があります。

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

      array[j] = value; 
    };
    

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

1
Adrian McCarthy

私はすでに言及された豊富なトピックを避けようとします。

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

ツール

私にとって、forループとwhileループをより良く書くための最大のツールは、forまたはwhileループをまったく書かないことです。

最近のほとんどの言語は、何らかの方法でこの問題をターゲットにしています。たとえば、Javaは最初からIteratorを使用していましたが、以前は少し扱いに​​くいものでしたが、より短いリリースでより簡単に使用できるようにショートカット構文を導入しました。 C#にもあります。

私が現在好んで使用している言語であるRubyは、機能的なアプローチ(_.each_、_.map_など)を全面的に採用しています。これは非常に強力です。私が取り組んでいるいくつかのRuby code-base:約10.000行のコードで、ゼロforと約5 while

私が新しい言語を選択せざるを得なかった場合、そのような機能/データベースのループを探すことは、優先度リストで非常に高くなります。

メンタルモデル

whileは、gotoの1つ上のステップで、取得できる最小の抽象化であることを覚えておいてください。私の意見では、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
AnoE

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

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

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

initialization
while(condition)
{
    body
    increment
}

その他の提案は次のとおりです。

  • Foreachループのような別の言語構造を使用できますか?これにより、条件と増分ステップが処理されます。
  • マップまたはフィルター機能を使用できますか?一部の言語には、コレクションを内部でループするこれらの名前の関数があります。コレクションとボディを提供するだけです。
  • forループに慣れるために、より多くの時間を費やす必要があります。いつでも使用します。デバッガでforループをステップ実行して、それがどのように実行されるかを確認することをお勧めします。

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

0
user2023861

追加の洞察の試み

非自明なアルゴリズムでループを使用する場合、次の方法を試すことができます。

  1. 固定配列を作成 4つの位置で、いくつかの値を入力しますシミュレーションするには問題;
  2. アルゴリズムを与えられた問題を解決するループなしおよびハードコードされたインデックス付け;に記述します。
  3. その後、いくつかの変数iまたはjを使用して、コード内のハードコードされたインデックスを代入し、必要に応じてこれらの変数をインクリメント/デクリメントします(ただし、ループはありません)。 ;
  4. コードを書き直して、繰り返しブロックをループ内に入れます、事前条件と事後条件を満たします。
  5. [optional]ループを目的の形式(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:コードは入力が有効であり、その配列に繰り返しが含まれていないことを前提としています。

0
Emerson Cardoso