プログラマーが式を使用するのを見てきました
mid = start + (end - start) / 2
より単純な式を使用する代わりに
mid = (start + end) / 2
配列またはリストの中央の要素を見つけるため。
なぜ前者を使用するのですか?
3つの理由があります。
まず、end - start
がオーバーフローしない限り、ポインターを使用していてもstart + (end - start) / 2
は機能します1。
int *start = ..., *end = ...;
int *mid = start + (end - start) / 2; // works as expected
int *mid = (start + end) / 2; // type error, won't compile
第二に、start
とend
が大きな正数の場合、start + (end - start) / 2
はオーバーフローしません。符号付きオペランドでは、オーバーフローは未定義です。
int start = 0x7ffffffe, end = 0x7fffffff;
int mid = start + (end - start) / 2; // works as expected
int mid = (start + end) / 2; // overflow... undefined
(end - start
はオーバーフローする可能性がありますが、start < 0
またはend < 0
の場合のみです。)
または、符号なし算術では、オーバーフローが定義されていますが、間違った答えが返されます。ただし、符号なしオペランドの場合、end >= start
である限り、start + (end - start) / 2
はオーバーフローしません。
unsigned start = 0xfffffffeu, end = 0xffffffffu;
unsigned mid = start + (end - start) / 2; // works as expected
unsigned mid = (start + end) / 2; // mid = 0x7ffffffe
最後に、多くの場合、start
要素に向かって丸めます。
int start = -3, end = 0;
int mid = start + (end - start) / 2; // -2, closer to start
int mid = (start + end) / 2; // -1, surprise!
1 C標準によれば、ポインター減算の結果がptrdiff_t
として表現できない場合、動作は未定義です。ただし、実際には、これには、アドレス空間全体の少なくとも半分を使用してchar
配列を割り当てる必要があります。
この事実を示すために簡単な例を挙げることができます。特定のlarge配列で、[1000, INT_MAX]
の範囲の中点を見つけようとしているとします。現在、INT_MAX
はint
データ型が格納できる最大値です。これに1
を追加しても、最終値は負になります。
また、start = 1000
およびend = INT_MAX
。
式を使用して:(start + end)/2
、
中間点は
(1000 + INT_MAX)/2
=-(INT_MAX+999)/2
、これは負およびセグメンテーション違反が発生する可能性がありますこの値を使用してインデックス付けを試みる場合です。
しかし、式(start + (end-start)/2)
を使用すると、次のようになります。
(1000 + (INT_MAX-1000)/2)
=(1000 + INT_MAX/2 - 500)
=(INT_MAX/2 + 500)
オーバーフローしない。
他の人がすでに言ったことに追加するために、最初のものはその意味を数学的にそれほど気にしない人に明確に説明します:
mid = start + (end - start) / 2
次のように読み取ります:
midはstartに長さの半分を加えたものに等しい。
一方:
mid = (start + end) / 2
次のように読み取ります:
midは、開始と終了の半分に等しい
少なくともそのように表現されたとき、それは最初のものほど明確に見えません。
コスが指摘したように、それはまた読むことができます:
midは開始と終了の平均に等しい
少なくとも私の意見では、それは最初のものほど明確ですが、まだ明確ではありません。
start +(end-start)/ 2は、可能なオーバーフローを回避できます。たとえば、start = 2 ^ 20およびend = 2 ^ 30