Mengapa memilih awal + (akhir - awal) / 2 daripada (mulai + akhir) / 2 saat menghitung tengah array?

160

Saya telah melihat programmer menggunakan formula

mid = start + (end - start) / 2

alih-alih menggunakan rumus yang lebih sederhana

mid = (start + end) / 2

untuk menemukan elemen tengah dalam array atau daftar.

Mengapa mereka menggunakan yang sebelumnya?

Pallavi Chauhan
sumber
51
Tebakan liar: (start + end)mungkin meluap, sementara (end - start)tidak bisa.
cadaniluk
30
karena yang terakhir tidak berfungsi kapan startdan endsedang pointer.
ENC
20
start + (end - start) / 2juga membawa makna semantik: (end - start)adalah panjang, jadi ini mengatakan: start + half the length.
njzk2
2
@ LưuVĩnhPhúc: Bukankah pertanyaan ini memiliki jawaban terbaik dan suara terbanyak? Jika demikian, pertanyaan lain mungkin harus ditutup sebagai dup yang satu ini. Usia posting tidak relevan.
Nisse Engström

Jawaban:

218

Ada tiga alasan.

Pertama-tama, start + (end - start) / 2berfungsi bahkan jika Anda menggunakan pointer, asalkan end - starttidak melimpah 1 .

int *start = ..., *end = ...;
int *mid = start + (end - start) / 2; // works as expected
int *mid = (start + end) / 2;         // type error, won't compile

Kedua, start + (end - start) / 2tidak akan meluap jika startdan endmerupakan angka positif yang besar. Dengan operan yang ditandatangani, overflow tidak ditentukan:

int start = 0x7ffffffe, end = 0x7fffffff;
int mid = start + (end - start) / 2; // works as expected
int mid = (start + end) / 2;         // overflow... undefined

(Perhatikan bahwa end - startmungkin meluap, tetapi hanya jika start < 0atau end < 0.)

Atau dengan aritmatika yang tidak ditandatangani, overflow didefinisikan tetapi memberi Anda jawaban yang salah. Namun, untuk operan yang tidak ditandatangani, start + (end - start) / 2tidak akan pernah meluap selama end >= start.

unsigned start = 0xfffffffeu, end = 0xffffffffu;
unsigned mid = start + (end - start) / 2; // works as expected
unsigned mid = (start + end) / 2;         // mid = 0x7ffffffe

Akhirnya, Anda sering ingin membulatkan ke startelemen.

int start = -3, end = 0;
int mid = start + (end - start) / 2; // -2, closer to start
int mid = (start + end) / 2;         // -1, surprise!

Catatan kaki

1 Menurut standar C, jika hasil pengurangan pointer tidak dinyatakan sebagai a ptrdiff_t, maka perilaku tidak terdefinisi. Namun, dalam praktiknya, ini membutuhkan mengalokasikan chararray menggunakan setidaknya setengah dari seluruh ruang alamat.

Dietrich Epp
sumber
Hasil (end - start)dalam signed intcase tidak terdefinisi ketika meluap.
ensc
Bisakah Anda membuktikan itu end-starttidak akan meluap? AFAIK jika Anda mengambil negatif startitu harus memungkinkan untuk membuatnya meluap. Tentu, sebagian besar waktu ketika Anda menghitung rata-rata Anda tahu bahwa nilainya adalah >= 0...
Bakuriu
12
@ Bakuriu: Tidak mungkin membuktikan sesuatu yang tidak benar.
Dietrich Epp
4
Ini sangat menarik di C, karena pengurangan pointer (per standar) rusak oleh desain. Implementasi diizinkan untuk membuat array yang begitu besar sehingga end - starttidak terdefinisi, karena ukuran objek tidak ditandai sedangkan perbedaan pointer ditandatangani. Jadi end - start"berfungsi bahkan menggunakan pointer", asalkan Anda juga entah bagaimana menjaga ukuran array di bawah ini PTRDIFF_MAX. Agar adil dengan standar, itu tidak banyak penghalang pada sebagian besar arsitektur karena itu setengah dari ukuran peta memori.
Steve Jessop
3
@ Bakuriu: Ngomong-ngomong, ada tombol "edit" pada pos yang dapat Anda gunakan untuk menyarankan perubahan (atau membuatnya sendiri) jika Anda merasa saya melewatkan sesuatu, atau ada sesuatu yang tidak jelas. Saya hanya manusia, dan posting ini telah dilihat oleh lebih dari dua ribu pasang bola mata. Jenis komentar, "Anda harus mengklarifikasi ..." benar-benar menggosok saya dengan cara yang salah.
Dietrich Epp
18

Kita dapat mengambil contoh sederhana untuk menunjukkan fakta ini. Misalkan dalam array besar tertentu , kami mencoba menemukan titik tengah rentang [1000, INT_MAX]. Sekarang, INT_MAXadalah nilai terbesar yang intbisa disimpan oleh tipe data. Bahkan jika 1ditambahkan ke ini, nilai akhir akan menjadi negatif.

Juga, start = 1000dan end = INT_MAX.

Menggunakan rumus: (start + end)/2,

titik tengah akan menjadi

(1000 + INT_MAX)/2= -(INT_MAX+999)/2, yang negatif dan dapat memberikan kesalahan segmentasi jika kami mencoba mengindeks menggunakan nilai ini.

Tapi, menggunakan rumus (start + (end-start)/2),, kita mendapatkan:

(1000 + (INT_MAX-1000)/2)= (1000 + INT_MAX/2 - 500)= (INT_MAX/2 + 500) yang tidak akan meluap .

Shubham
sumber
1
Jika Anda menambahkan 1 ke INT_MAX, hasilnya tidak akan negatif, tetapi tidak ditentukan.
celtschk
@celtschk Secara teoritis, ya. Praktis itu akan membungkus banyak waktu dari INT_MAXke -INT_MAX. Tapi itu kebiasaan buruk untuk mengandalkan itu.
Mast
17

Untuk menambah apa yang sudah dikatakan orang lain, yang pertama menjelaskan artinya lebih jelas bagi mereka yang kurang berpikir matematis:

mid = start + (end - start) / 2

terbaca sebagai:

pertengahan sama dengan awal ditambah setengah dari panjang.

sedangkan:

mid = (start + end) / 2

terbaca sebagai:

mid sama dengan setengah dari awal plus akhir

Yang sepertinya tidak sejelas yang pertama, setidaknya ketika diungkapkan seperti itu.

seperti yang ditunjukkan Kos itu juga dapat membaca:

mid sama dengan rata-rata awal dan akhir

Yang lebih jelas tapi tetap tidak, setidaknya menurut saya, sejelas yang pertama.

TheLethalCoder
sumber
3
Saya mengerti maksud Anda, tetapi ini sungguh merupakan peregangan. Jika Anda melihat "e - s" dan berpikir "panjang" maka Anda hampir pasti melihat "(s + e) ​​/ 2" dan berpikir "rata - rata" atau "pertengahan."
djechlin
2
Pemrogram @djechlin miskin dalam matematika. Mereka sibuk melakukan pekerjaan mereka. Mereka tidak punya waktu untuk menghadiri kelas matematika.
Little Alien
1

start + (end-start) / 2 dapat menghindari kemungkinan overflow, misalnya start = 2 ^ 20 dan end = 2 ^ 30

fight_club
sumber