web-dev-qa-db-ja.com

理解しやすい効率的なアルゴリズムまたはコードを書く方が良いですか?

そこで、最近、大手金融会社からコーディングの割り当てを受けました。この問題を解決する2つの方法を考えました。 1つの方法には、1つの外部forループと1つの内部forループが含まれていました。この場合、コードは比較的理解しやすく、この方法をより「明白な」ソリューションと見なしました。

しかし、4つのforループを連続して使用する別の方法を考えました。私はBig Oの専門家ではありませんが、私の理解では、漸近的に言えば、4つの外側のforループはO(n)で、1つの外側と1つの内側のforループはO(n ^ 2 )ただし、4つの外側のforループの方法は、明らかに理解するのが少し難しく、あまり明確ではありませんでした。

私はコーディング割り当ての質問から疑問に思っていましたが、どちらの方法が優れていると思われますか?そして、より広い視点で、どちらが良いですか?私はまだプロのコーディングのアイデアにまだ慣れていないので(私は若い頃に多くのカジュアルコーディングを行いました)、読みやすさと効率のバランスのアイデアがあることを知っています。この場合のバランスはどうだろうと思っていました。

ありがとう!

3
Tyler M

これが課題ではなかった場合、どちらのソリューションが適しているかは比較的簡単に推測できます。

  • ほとんどの場合、それは最初のものです。

  • まれなケース(および金融セクターについて話しているという事実は、この可能性を無視してはならないということを意味します)、パフォーマンスが重要になります(コードの一部が数千回、毎秒数百回実行されるため)マシン、またはミリ秒を節約することは、トレーダーが競争上の優位性を持つことを意味します)。だが:

これは課題なので、両方を行います。

課題の目標は、特定の問題に直面したときにどのように反応するかを確認することです。してもいいです:

  • シンプルなソリューションを見つけて、最適化されたソリューションを見逃してください。

  • 最適化されたソリューションを見つけて、シンプルなソリューションを見逃してください。

  • 両方のソリューションを見つけて、それぞれの利点と欠点を理解します。たとえば、2番目の選択肢が最初の選択肢よりどれだけ速いか、なぜ理解するのが難しいのか、パフォーマンスを犠牲にすることなく理解しやすくするために何ができるのかなどです。潜在的な変化の欠点です。

推測することはできません(そうしないと、最初にこの質問をすることはありません)。したがって、安全のために、両方のソリューションが見つかり、パフォーマンスと保守性の観点から実際のニーズに関する詳細情報が得られたら、どちらか一方を提案する準備ができていることを示してください。

19

場合によります。

時々、企業はどちらか一方を明確に好みます。時々、開発チームは好みを持っています。

自由に決定できる場合は、この領域でのパフォーマンスの関連性について考えてください。

  • これがクリティカルの部分であり、パフォーマンスの問題がユーザーの満足度やアプリケーション全体のユーザビリティに影響を与える場合は、より高速で複雑なソリューションを検討してください(ただし、考えやアルゴリズムをコードに記述してください)。
  • これがnotが重要な部分である場合は、理解しやすい(そして保守しやすい)バージョンを使用します(気になる場合は、読みやすくするために、より速く実行できたはずのコメントを追加してください)。 。

どちらの方法でも、予測が本当であることを確認してください(=>測定!)。より遅い複雑なソリューションよりも悪いものはありません。

3
Aganju

教室の観点から、私は(より多くの回答/コメントで)理解しやすいほうがよいことに同意します。ほとんどの場合、それはおそらくその教室の質問に対する正しい答えになるでしょう。しかし、最終的に「専門的なコーディング」を行う(フレーズを引用する)ときに発生する多くの問題は、そのような簡単な解決策をまったく備えていない場合があります。それであなたはその後何をしますか?

結局のところ、あなたの理解しやすいものより効率的なものは、誤った二分法です。優れたインラインドキュメントは常にsine qua nonです。これにより、メンテナはこれまでに見たことのない数万行のコードを把握でき、そのコードの元の開発者は長いです。 -なくなって以来。

優れたインラインドキュメントの1つの側面は、各関数の前にある大きなヘッダーコメントブロックであり、その目的、呼び出しシーケンス、副作用、アルゴリズムなどを記述および説明します。たとえば、投影関数の小さなライブラリから、

/* ==========================================================================
 * Function:    rotatetoz ( l, r, axis, nlines )
 * Purpose:     xlate, rotate l, returning r as though
 *              axis were along the z-axis with axis.pt1 at Origin
 * --------------------------------------------------------------------------
 * Arguments:   l (I)           input LINE * to 3d-lines to be xlated, rotated
 *                              along with axis
 *              r (O)           output LINE * to xlated, rotated 3d-lines
 *              axis (I)        LINE specifying rotation axis,
 *                              with pt1 the tail, and pt2 the head,
 *                              such that pt1 ends up at (0,0,0), and
 *                              pt2 at (0,0,z=r) with r the length of axis
 *              nlines (I)      int containing #lines to be rotated
 * Returns:     (double)        r, the length of axis, or -1.0 for any error
 * --------------------------------------------------------------------------
 * Notes:     o axis is (obviously) not changed, with r xlated, rotated as
 *              though axis pt1 was at the Origin, and pt2 was at (0,0,z=r)
 *            o to accomplish this, first xlate pt1 of axis to (0,0,0),
 *              and then...
 *                    z.             As per usual spherical polar coords,
 *                    |  .                 x = r*cos(phi)*cos(theta)
 *                    |    .               y = r*cos(phi)*sin(theta)
 *                    |      .             z = r*sin(phi), where
 *                    |        o(x,y,z)    r^2 = x^2 + y^2 + z^2
 *                    |90-phi. |for r-axis
 *                    |    .   |     Then, to rotate axis to positive z-axis,
 *                    |  .     |           o rotate about z-axis by -theta
 *                    |.       |             to bring axis to x,z-plane
 *                    +--------|---y       o rotate about y-axis by -(90-phi)
 *                   /   .     |  /          to bring axis coincident with z
 *                  /theta .   | /   Where we calculate
 *                 /         . |/          o sin(phi)   = z/r
 *                x------------+           o sin(theta) = y/(r*cos(phi)) or
 *                                           cos(theta) = x/(r*cos(phi))
 *
 * ======================================================================= */
/* --- entry point --- */
double  rotatetoz ( LINE *l, LINE *r, LINE axis, int nlines ) {
  /* ---
   * lots of code;
   * ---------------- */
  } /* --- end-of-function rotatetoz() --- */

そして、コード自体は広くコメントされており、頻繁に上記のアルゴリズムの概要/説明を参照しています。うまくいけば、将来のメンテナが、やや密度の高いコードの機能ブロックへの分解をより簡単に理解できるようになります。しかし、「読み取りが容易であるか、より効率的な」設計の選択肢は決してありませんでした。そして、それはしばしばありません。

3
John Forkosh

コンセンサスは、最適化されたコードよりも可読性の高いコードを使用することです。これは、ほとんどのコードで、コードの読み取りに実際に実行される時間よりも多くの時間を費やすためです。

最適化する場合は、まずそれが必要かどうかを確認します。プロファイラーを使用して必要な領域を見つけ、それを必要とするルーチンのみを最適化します。

ただし、大きなOの複雑さを適切に理解することは、どのプログラマーにとっても当然のことです。正しいアルゴリズムを選択することで、将来の多くの時間を節約できるからです。この素晴らしい本「アルゴリズム」があり、私の意見では、すべてのソフトウェア開発者はそれを読むべきです。これは、独学のプログラマーと学業のプログラマーとの大きな違いです。この基本的なアルゴリズムの知識は、独学のプログラマーにはしばしば欠けています。

だからここであなたに私の質問:どのアルゴリズムを使用していますか?

1
Pieter B

ドナルド・クヌースは次のようなことを述べました

時期尚早の最適化は、すべての悪の根源です。

物事が機能するようになるまで、最も簡単で自明のテクニックを使います。

次に、ボトルネックをテストして最適化します。

0
dkretz

Developing everything as an APIの設計アプローチを採用する場合、私が推奨するアプローチはこれです(C++/Qtで記述)。

  1. アルゴリズムから生成する必要がある戻り値を特定します。私のやや恣意的な例では、文字列の先頭にあるハッシュの数が必要です。

  2. その戻り値のメンバー関数を作成します。次のようになります。

    // A descriptive function name is preferred over comments
    int MyClass::countFrontRecurringHashes() 
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         ... 
    }
    
  3. 後続のすべてのコードは、この関数の戻り値のみに依存する必要があります。このコード内ではメンバー変数を変更しないでください。そのため、他のコードとのやり取りを気にすることなく、アルゴリズムを変更できます。

  4. 最初のパスでは、時間を節約できる場合は、読みやすいようにコードを記述します。明らかな場合は、改善のためにマークを付けます。

    int MyClass::countFrontRecurringHashes()
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         /// Todo: Performance
         return QRegularExpression("^[#]*").match(m_MyString).capturedLength();
    }
    
  5. コードが稼働中の場合、パフォーマンスの向上が他の場所でのさらなる開発よりも優先されるかどうかを特定できます。そうであれば、次に行って次のような変更を加えることができます。

    int MyClass::countFrontRecurringHashes()
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         int i, l = m_MyString.length();
         for(i = 0; i < l && m_MyString[i] == '#'; ++i);
         return i;
         // This code is more complex, abstract, and difficult to read, 
         // However, it is about 100 times more efficient.
    }
    
  6. また、チームで作業している場合、これは新しいメンバーを追加するのにも良い場所です。コード内でtodofixmeを検索するように指示しますが、多くの場合、FixmeやToDoは最終的に他のコードが依存する可能性のある機能になります。そのため、適切なカプセル化とAPI開発戦略が重要です。

0
Akiva