Tại sao thích bắt đầu + (kết thúc - bắt đầu) / 2 hơn (bắt đầu + kết thúc) / 2 khi tính giữa của một mảng?


160

Tôi đã thấy các lập trình viên sử dụng công thức

mid = start + (end - start) / 2

thay vì sử dụng công thức đơn giản hơn

mid = (start + end) / 2

để tìm phần tử ở giữa trong mảng hoặc danh sách.

Tại sao họ sử dụng cái trước?


51
Wild đoán: (start + end)có thể tràn, trong khi (end - start)không thể.
cadaniluk

30
bởi vì sau này không hoạt động khi startendlà con trỏ.
ensc


20
start + (end - start) / 2cũng mang ý nghĩa ngữ nghĩa: (end - start)là chiều dài, vì vậy điều này nói : start + half the length.
njzk2

2
@ LưuViênPhúc: Câu hỏi này có câu trả lời hay nhất và nhiều phiếu nhất không? Nếu vậy, các câu hỏi khác có lẽ nên được đóng lại như là một bản sao của câu hỏi này. Tuổi của bài viết không liên quan.
Nisse Engström

Câu trả lời:


218

Có ba lý do.

Trước hết, start + (end - start) / 2hoạt động ngay cả khi bạn đang sử dụng con trỏ, miễn là end - startkhông tràn 1 .

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

Thứ hai, start + (end - start) / 2sẽ không tràn nếu startendlà số dương lớn. Với các toán hạng đã ký, tràn không được xác định:

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

(Lưu ý rằng end - startcó thể tràn, nhưng chỉ khi start < 0hoặc end < 0.)

Hoặc với số học không dấu, tràn được xác định nhưng cung cấp cho bạn câu trả lời sai. Tuy nhiên, đối với các toán hạng không dấu, start + (end - start) / 2sẽ không bao giờ tràn ra miễn là end >= start.

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

Cuối cùng, bạn thường muốn làm tròn về phía startphần tử.

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

Chú thích

1 Theo tiêu chuẩn C, nếu kết quả của phép trừ con trỏ không thể biểu diễn dưới dạng a ptrdiff_t, thì hành vi không được xác định. Tuy nhiên, trong thực tế, điều này đòi hỏi phải phân bổ một charmảng bằng cách sử dụng ít nhất một nửa toàn bộ không gian địa chỉ.


kết quả (end - start)trong signed inttrường hợp là không xác định khi nó tràn.
ensc

Bạn có thể chứng minh rằng end-startsẽ không tràn? AFAIK nếu bạn lấy âm bản startthì có thể làm cho nó tràn ra. Chắc chắn, hầu hết các lần bạn tính trung bình bạn đều biết rằng các giá trị là >= 0...
Bakuriu

12
@Bakuriu: Không thể chứng minh điều gì đó không đúng.
Dietrich Epp

4
Đó là mối quan tâm đặc biệt đối với C, vì phép trừ con trỏ (theo tiêu chuẩn) bị phá vỡ theo thiết kế. Việc triển khai được phép tạo ra các mảng lớn đến mức end - startkhông xác định được, bởi vì kích thước đối tượng không được ký trong khi các khác biệt con trỏ được ký. Vì vậy, end - start"hoạt động ngay cả khi sử dụng con trỏ", với điều kiện bạn cũng bằng cách nào đó giữ kích thước của mảng bên dưới PTRDIFF_MAX. Để công bằng với tiêu chuẩn, đó không phải là một cản trở trên hầu hết các kiến ​​trúc vì đó là một nửa kích thước của bản đồ bộ nhớ.
Steve Jessop

3
@Bakuriu: Nhân tiện, có một nút "chỉnh sửa" trên bài đăng mà bạn có thể sử dụng để đề xuất thay đổi (hoặc tự thực hiện) nếu bạn nghĩ rằng tôi đã bỏ lỡ điều gì đó hoặc điều gì đó không rõ ràng. Tôi chỉ là con người, và bài đăng này đã được nhìn thấy bởi hơn hai nghìn cặp nhãn cầu. Loại bình luận, "Bạn nên làm rõ ..." thực sự làm tôi hiểu lầm.
Dietrich Epp

18

Chúng ta có thể lấy một ví dụ đơn giản để chứng minh thực tế này. Giả sử trong một mảng lớn nhất định , chúng tôi đang cố gắng tìm trung điểm của phạm vi [1000, INT_MAX]. Bây giờ, INT_MAXlà giá trị lớn nhất mà intkiểu dữ liệu có thể lưu trữ. Ngay cả khi 1được thêm vào điều này, giá trị cuối cùng sẽ trở thành âm.

Ngoài ra, start = 1000end = INT_MAX.

Sử dụng công thức: (start + end)/2,

điểm giữa sẽ là

(1000 + INT_MAX)/2= -(INT_MAX+999)/2, đó là số âmcó thể đưa ra lỗi phân đoạn nếu chúng tôi cố gắng lập chỉ mục bằng cách sử dụng giá trị này.

Nhưng, bằng cách sử dụng công thức (start + (end-start)/2), chúng tôi nhận được:

(1000 + (INT_MAX-1000)/2)= (1000 + INT_MAX/2 - 500)= (INT_MAX/2 + 500) sẽ không tràn .


1
Nếu bạn thêm 1 vào INT_MAX, kết quả sẽ không âm, nhưng không xác định.
celtschk

@celtschk Về mặt lý thuyết, vâng. Thực tế nó sẽ bao quanh rất nhiều lần từ đó INT_MAXđến -INT_MAX. Đó là một thói quen xấu để dựa vào đó mặc dù.
Cột

17

Để thêm vào những gì người khác đã nói, người đầu tiên giải thích ý nghĩa của nó rõ ràng hơn với những người ít suy nghĩ toán học:

mid = start + (end - start) / 2

đọc là:

mid bằng bắt đầu cộng với một nửa chiều dài.

trong khi:

mid = (start + end) / 2

đọc là:

giữa bằng một nửa bắt đầu cộng với kết thúc

Điều này dường như không rõ ràng như lần đầu tiên, ít nhất là khi được thể hiện như thế.

như Kos chỉ ra nó cũng có thể đọc:

mid bằng trung bình của bắt đầu và kết thúc

Cái nào rõ ràng hơn nhưng vẫn không, ít nhất là theo ý kiến ​​của tôi, rõ ràng như cái đầu tiên.


3
Tôi thấy quan điểm của bạn, nhưng đây thực sự là một sự kéo dài. Nếu bạn thấy "e - s" và nghĩ "độ dài" thì bạn gần như chắc chắn sẽ thấy "(s + e) ​​/ 2" và nghĩ "trung bình" hoặc "giữa".
djechlin

2
@djechlin Lập trình viên kém về toán. Họ đang bận rộn làm công việc của họ. Họ không có thời gian để tham dự các lớp học toán.
Người ngoài hành tinh nhỏ

1

start + (end-start) / 2 có thể tránh tràn có thể xảy ra, ví dụ start = 2 ^ 20 và end = 2 ^ 30

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.