配列の中央を計算するときに、なぜstart +(end-start)/ 2よりも(start + end)/ 2を優先するのですか?


160

プログラマーが式を使用するのを見てきました

mid = start + (end - start) / 2

より単純な式を使用する代わりに

mid = (start + end) / 2

配列またはリストの中央の要素を見つけるため。

なぜ彼らは前者のものを使うのですか?


51
ワイルドな推測:(start + end)オーバーフローする可能性がありますが、オーバーフロー(end - start)することはありません。
cadaniluk

30
後者はポインターstartendポインターの場合は機能しないためです。
ensc 2016


20
start + (end - start) / 2また、意味的な意味を持ちます:(end - start)長さですstart + half the length
njzk2

2
@LưuVĩnhPhúc:この質問には、最高の回答と最高の投票がありませんか?もしそうなら、他の質問はおそらくこれの重複として閉じられるべきです。投稿の年齢は関係ありません。
NisseEngström16年

回答:


218

理由は3つあります。

まず、オーバーフローしないstart + (end - start) / 2限り、ポインターを使用している場合でも機能します1end - start

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) / 2オーバーフローしていないかどうstartend大きな正の数です。符号付きオペランドでは、オーバーフローは未定義です。

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。)

または、符号なし演算では、オーバーフローが定義されていますが、間違った答えを出します。ただし、符号なしオペランドの場合、であるstart + (end - start) / 2限り、オーバーフローすることはありませんend >= start

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は、アドレス空間全体の少なくとも半分を使用して配列を割り当てる必要があります。


オーバーフロー(end - start)したsigned int場合の結果は未定義です。
ensc 2016

end-startオーバーフローしないことを証明できますか?私の知る限り、あなたがネガティブを取るならばstart、それをオーバーフローさせることが可能であるべきです。もちろん、平均を計算するほとんどの場合、値は>= 0...であることがわかります
Bakuriu

12
@Bakuriu:真実でないことを証明することは不可能です。
ディートリッヒエップ2016

4
ポインターの減算(標準による)は仕様により無効になっているため、これはCで特に重要です。end - startオブジェクトサイズは符号なしであるのに対し、ポインタの違いは符号付きであるため、実装では未定義の非常に大きな配列を作成できます。したがってend - start、以下の配列のサイズを何らかの方法で維持することを条件に、「ポインターを使用しても機能します」PTRDIFF_MAX。標準に公平に言えば、メモリマップの半分のサイズであるため、ほとんどのアーキテクチャではそれほど障害にはなりません。
スティーブジェソップ

3
@Bakuriu:ちなみに、投稿に「編集」ボタンがあり、私が何かを見逃した、または不明な場合は、変更を提案(または自分で変更)できます。私は人間です。この投稿は、2,000組を超える眼球で見られています。「明確にすべきです...」というコメントは、本当に私を間違った方向にこすります。
ディートリッヒエップ2016

18

この事実を示すために簡単な例をとることができます。ある大きな配列で、範囲の中点を見つけようとしているとしましょう[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) どのオーバーフローしないであろう


1
に1を追加するINT_MAXと、結果は負ではなく、未定義になります。
celtschk

@celtschk理論的にはそうです。実際には、からINT_MAXまでの多くの時間がラップアラウンドされ-INT_MAXます。それに頼ることは悪い習慣です。
マスト

17

他の人がすでに言ったことに追加するために、最初のものは数学的にあまり気にしていない人にその意味をより明確に説明します:

mid = start + (end - start) / 2

次のように読みます:

midはstartに長さの半分を加えたものに等しい。

一方:

mid = (start + end) / 2

次のように読みます:

中央は開始と終了の半分に等しい

少なくともそのように表現された場合、これは最初のものほど明確ではないようです。

コスが指摘したように、それはまた読むことができます:

midは開始と終了の平均に等しい

それはより明確ですが、少なくとも私の意見では、最初のものほど明確ではありません。


3
あなたの言いたいことはわかりますが、これは本当にストレッチです。「e-s」を見て「長さ」を考えると、ほぼ確実に「(s + e)/ 2」を見て「平均」または「中」だと思います。
djechlin

2
@djechlinプログラマーは数学が苦手です。彼らは仕事に忙しい。彼らは数学の授業に出席する時間がない。
リトルエイリアン

1

start +(end-start)/ 2は、オーバーフローの可能性を回避できます。たとえば、start = 2 ^ 20およびend = 2 ^ 30

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