เหตุใดจึงต้องการเริ่มต้น + (สิ้นสุด - เริ่ม) / 2 เหนือ (เริ่ม + จบ) / 2 เมื่อคำนวณตรงกลางของอาร์เรย์?


160

ฉันเคยเห็นโปรแกรมเมอร์ใช้สูตร

mid = start + (end - start) / 2

แทนที่จะใช้สูตรที่ง่ายกว่า

mid = (start + end) / 2

สำหรับการค้นหาองค์ประกอบกลางในอาร์เรย์หรือรายการ

ทำไมพวกเขาถึงใช้อดีต?


51
Wild เดา: (start + end)อาจล้นในขณะที่(end - start)ไม่สามารถ
cadaniluk

30
เพราะหลังไม่ทำงานเมื่อstartและendเป็นตัวชี้
ensc


20
start + (end - start) / 2ยังแบกความหมาย: คือความยาวดังนั้นนี้พูดว่า:(end - start) start + half the length
njzk2

2
@ LưuVĩnhPhúc: คำถามนี้ไม่มีคำตอบที่ดีที่สุดและได้รับคะแนนโหวตมากที่สุดหรือไม่? ถ้าเป็นเช่นนั้นคำถามอื่น ๆ ควรจะถูกปิดเป็นซ้ำของคำถามนี้ อายุของโพสต์ไม่เกี่ยวข้อง
Nisse Engström

คำตอบ:


218

มีสามเหตุผล

ครั้งแรกของทั้งหมดstart + (end - start) / 2ทำงานแม้ว่าคุณจะใช้ตัวชี้ตราบใดที่end - startไม่ได้ล้น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) / 2จะไม่ล้นถ้าstartและendเป็นจำนวนบวกมาก ด้วยตัวถูกดำเนินการลงนามล้นไม่ได้กำหนด:

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 หากผลลัพธ์ของการลบตัวชี้ไม่สามารถแทนได้ว่าเป็น a ptrdiff_tดังนั้นพฤติกรรมจะไม่ได้กำหนด อย่างไรก็ตามในทางปฏิบัติสิ่งนี้ต้องการการจัดสรรcharอาร์เรย์โดยใช้พื้นที่ที่อยู่ทั้งหมดอย่างน้อยครึ่งหนึ่ง


ผลลัพธ์ของ(end - start)ในsigned intกรณีที่ไม่ได้กำหนดเมื่อมันล้น
ensc

คุณพิสูจน์ได้ไหมว่าend-startไม่ล้น AFAIK หากคุณลบstartมันควรจะเป็นไปได้ที่จะทำให้มันล้น แน่นอนว่าส่วนใหญ่เวลาที่คุณคำนวณค่าเฉลี่ยคุณรู้ว่าค่าต่าง ๆ นั้นคือ>= 0...
Bakuriu

12
@Bakuriu: มันเป็นไปไม่ได้ที่จะพิสูจน์สิ่งที่ไม่เป็นความจริง
Dietrich Epp

4
มันเป็นเรื่องที่น่าสนใจเป็นพิเศษใน C เนื่องจากการลบตัวชี้ (ต่อมาตรฐาน) ถูกทำลายโดยการออกแบบ การใช้งานได้รับอนุญาตให้สร้างอาร์เรย์ที่มีขนาดใหญ่ซึ่งend - startไม่ได้กำหนดเนื่องจากขนาดวัตถุไม่ได้ลงนามในขณะที่ความแตกต่างของตัวชี้ถูกลงนาม ดังนั้นend - start"การทำงานแม้จะใช้ตัวชี้" PTRDIFF_MAXหากว่าคุณยังอย่างใดให้ขนาดของอาร์เรย์ที่อยู่ด้านล่าง เพื่อความเป็นธรรมกับมาตรฐานนั่นไม่ใช่อุปสรรคสำหรับสถาปัตยกรรมส่วนใหญ่เนื่องจากมีขนาดครึ่งหนึ่งของแผนที่หน่วยความจำ
Steve Jessop

3
@Bakuriu: โดยวิธีการมีปุ่ม "แก้ไข" ในโพสต์ที่คุณสามารถใช้เพื่อแนะนำการเปลี่ยนแปลง (หรือทำให้พวกเขาเอง) ถ้าคุณคิดว่าฉันพลาดบางสิ่งบางอย่างหรือบางสิ่งไม่ชัดเจน ฉันเป็นเพียงมนุษย์และมีการโพสต์ดวงตามากกว่าสองพันคู่ ชนิดของความคิดเห็น "คุณควรชี้แจง ... " จริง ๆ แล้ว rubs ฉันผิดทาง
Dietrich Epp

18

เราสามารถยกตัวอย่างง่ายๆเพื่อแสดงข้อเท็จจริงนี้ สมมติว่าในบางขนาดใหญ่[1000, INT_MAX]อาร์เรย์ที่เรากำลังพยายามที่จะหาจุดกึ่งกลางของช่วง ตอนนี้INT_MAXเป็นค่าที่ใหญ่ที่สุดintที่ชนิดข้อมูลสามารถจัดเก็บได้ แม้ว่า1จะถูกเพิ่มเข้าไปในสิ่งนี้ค่าสุดท้ายจะกลายเป็นค่าลบ

นอกจากนี้และstart = 1000end = 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 เท่ากับเริ่มต้นบวกครึ่งหนึ่งของความยาว

ขณะที่:

mid = (start + end) / 2

อ่านเป็น:

mid เท่ากับครึ่งหนึ่งของการเริ่มต้นและจุดสิ้นสุด

ซึ่งดูไม่ชัดเจนเท่าครั้งแรกอย่างน้อยเมื่อแสดงออกเช่นนั้น

ตามที่คอสชี้ให้เห็นว่ามันยังสามารถอ่านได้:

mid เท่ากับค่าเฉลี่ยของการเริ่มต้นและสิ้นสุด

ซึ่งมีความชัดเจน แต่ก็ยังไม่อย่างน้อยก็ในความคิดของฉันอย่างชัดเจนเหมือนครั้งแรก


3
ฉันเห็นประเด็นของคุณ แต่นี่เป็นการยืดตัวจริง ๆ ถ้าคุณเห็น "e - s" และคิดว่า "ยาว" คุณก็เกือบจะเห็น "(s + e) ​​/ 2" และคิดว่า "ปานกลาง" หรือ "กลาง"
djechlin

2
@djechlin โปรแกรมเมอร์ยากจนคณิตศาสตร์ พวกเขากำลังยุ่งอยู่กับการทำงาน พวกเขาไม่มีเวลาไปเรียนวิชาคณิตศาสตร์
Little Alien

1

start + (end-start) / 2 สามารถหลีกเลี่ยงการโอเวอร์โฟลว์ที่เป็นไปได้เช่น start = 2 ^ 20 และ end = 2 ^ 30

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.