¿Por qué preferir start + (end - start) / 2 sobre (start + end) / 2 al calcular el centro de una matriz?

160

He visto a programadores usar la fórmula

mid = start + (end - start) / 2

en lugar de usar la fórmula más simple

mid = (start + end) / 2

para encontrar el elemento del medio en la matriz o lista.

¿Por qué usan el anterior?

Pallavi Chauhan
fuente
51
Suposición salvaje: (start + end)puede desbordarse, mientras (end - start)que no puede.
cadaniluk
30
porque este último no funciona cuando starty endson puntero.
ensc
20
start + (end - start) / 2También tiene un significado semántico: (end - start)es la longitud, por lo que este dice: start + half the length.
njzk2
2
@ LưuVĩnhPhúc: ¿Esta pregunta no tiene las mejores respuestas y la mayoría de los votos? Si es así, las otras preguntas probablemente deberían cerrarse como un duplicado de esta. La edad de las publicaciones es irrelevante.
Nisse Engström

Respuestas:

218

Hay tres razones

En primer lugar, start + (end - start) / 2funciona incluso si está utilizando punteros, siempre que end - startno se desborde 1 .

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

En segundo lugar, start + (end - start) / 2¿No desbordamiento si starty endson grandes números positivos. Con operandos firmados, el desbordamiento no está definido:

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

(Tenga en cuenta que end - startpuede desbordarse, pero solo si start < 0o end < 0.)

O con aritmética sin signo, se define el desbordamiento pero le da la respuesta incorrecta. Sin embargo, para operandos sin firmar, start + (end - start) / 2nunca se desbordará mientras end >= start.

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

Finalmente, a menudo quieres redondear hacia el startelemento.

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

Notas al pie

1 Según el estándar C, si el resultado de la resta del puntero no es representable como a ptrdiff_t, entonces el comportamiento es indefinido. Sin embargo, en la práctica, esto requiere asignar una charmatriz utilizando al menos la mitad del espacio de direcciones completo.

Dietrich Epp
fuente
El resultado de (end - start)en el signed intcaso es indefinido cuando se desborda.
ensc
¿Puedes demostrar que end-startno se desbordará? AFAIK si toma un negativo start, debería ser posible hacer que se desborde. Claro, la mayoría de las veces cuando calculas el promedio sabes que los valores son >= 0...
Bakuriu
12
@Bakuriu: Es imposible probar algo que no es cierto.
Dietrich Epp
44
Es de particular interés en C, ya que la resta del puntero (según el estándar) se rompe por diseño. Se permite que las implementaciones creen matrices tan grandes que end - startno estén definidas, porque los tamaños de los objetos no están firmados, mientras que las diferencias de puntero están firmadas. Entonces, end - start"funciona incluso utilizando punteros", siempre que de alguna manera también mantenga el tamaño de la matriz a continuación PTRDIFF_MAX. Para ser justos con el estándar, eso no es una gran obstrucción en la mayoría de las arquitecturas, ya que es la mitad del tamaño del mapa de memoria.
Steve Jessop
3
@Bakuriu: Por cierto, hay un botón "editar" en la publicación que puedes usar para sugerir cambios (o hacerlos tú mismo) si crees que me he perdido algo o algo no está claro. Solo soy humano, y esta publicación ha sido vista por más de dos mil pares de globos oculares. El tipo de comentario, "Deberías aclarar ..." realmente me molesta.
Dietrich Epp
18

Podemos tomar un ejemplo simple para demostrar este hecho. Supongamos que en una determinada matriz grande , estamos tratando de encontrar el punto medio del rango [1000, INT_MAX]. Ahora, INT_MAXes el valor más grande que intpuede almacenar el tipo de datos. Incluso si 1se agrega a esto, el valor final será negativo.

Además, start = 1000y end = INT_MAX.

Utilizando la fórmula: (start + end)/2,

el punto medio será

(1000 + INT_MAX)/2= -(INT_MAX+999)/2, que es negativo y puede dar un error de segmentación si intentamos indexar utilizando este valor.

Pero, usando la fórmula (start + (end-start)/2), obtenemos:

(1000 + (INT_MAX-1000)/2)= (1000 + INT_MAX/2 - 500)= (INT_MAX/2 + 500) que no se desbordará .

Shubham
fuente
1
Si agrega 1 a INT_MAX, el resultado no será negativo, sino indefinido.
celtschk
@celtschk Teóricamente, sí. Prácticamente terminará muchas veces pasando de INT_MAXa -INT_MAX. Sin embargo, es un mal hábito confiar en eso.
Mástil el
17

Para agregar a lo que otros ya han dicho, el primero explica su significado más claramente para aquellos con una mentalidad menos matemática:

mid = start + (end - start) / 2

se lee como:

mediados es igual a inicio más la mitad de la longitud.

mientras:

mid = (start + end) / 2

se lee como:

mediados es igual a la mitad del inicio más el final

Lo que no parece tan claro como el primero, al menos cuando se expresa así.

como señaló Kos, también puede leer:

mediados es igual al promedio de inicio y fin

Lo cual es más claro pero aún no, al menos en mi opinión, tan claro como el primero.

TheLethalCoder
fuente
3
Entiendo tu punto, pero esto realmente es un tramo. Si ve "e - s" y piensa "longitud", entonces seguramente verá "(s + e) ​​/ 2" y piensa "promedio" o "medio".
djechlin
2
@djechlin Los programadores son pobres en matemáticas. Están ocupados haciendo su trabajo. No tienen tiempo para asistir a las clases de matemáticas.
Little Alien
1

start + (end-start) / 2 puede evitar un posible desbordamiento, por ejemplo start = 2 ^ 20 y end = 2 ^ 30

lucha_club
fuente