Por que preferir start + (end - start) / 2 ao invés de (start + end) / 2 ao calcular o meio de uma matriz?

160

Já vi programadores usarem a fórmula

mid = start + (end - start) / 2

em vez de usar a fórmula mais simples

mid = (start + end) / 2

para encontrar o elemento do meio na matriz ou lista.

Por que eles usam o primeiro?

Pallavi Chauhan
fonte
51
Palpite: (start + end)pode estourar, enquanto (end - start)não pode.
cadaniluk
30
porque este último não funciona quando starte endé ponteiro.
ensc 31/07
20
start + (end - start) / 2também carrega significado semântico: (end - start)é o comprimento, de modo que este diz: start + half the length.
Njzk2 01/08/19
2
@ LưuVĩnhPhúc: Esta pergunta não tem as melhores respostas e mais votos? Nesse caso, as outras perguntas provavelmente devem ser encerradas como uma brincadeira dessa. A idade das postagens é irrelevante.
Nisse Engström

Respostas:

218

Existem três razões.

Primeiro de tudo, start + (end - start) / 2funciona mesmo se você estiver usando ponteiros, desde end - startque não exceda 1 .

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

Segundo, start + (end - start) / 2não transbordará se starte endfor um grande número positivo. Com operandos assinados, o estouro é indefinido:

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

(Observe que end - startpode estourar, mas apenas se start < 0ou end < 0.)

Ou com aritmética não assinada, o excesso é definido, mas fornece a resposta errada. No entanto, para operandos não assinados, start + (end - start) / 2nunca excederá o tempo end >= start.

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

Por fim, muitas vezes você deseja arredondar para o startelemento.

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

Notas de rodapé

1 De acordo com o padrão C, se o resultado da subtração do ponteiro não for representável como a ptrdiff_t, o comportamento será indefinido. No entanto, na prática, isso requer a alocação de uma charmatriz usando pelo menos metade do espaço de endereço inteiro.

Dietrich Epp
fonte
O resultado de (end - start)no signed intcaso é indefinido quando transborda.
ensc 31/07
Você pode provar que end-startnão vai transbordar? AFAIK, se você tomar um negativo start, deve ser possível transbordar. Claro, na maioria das vezes quando você calcular a média você sabe que os valores são >= 0...
Bakuriu
12
@Bakuriu: É impossível provar algo que não é verdade.
Dietrich Epp
4
É de particular interesse em C, pois a subtração do ponteiro (de acordo com o padrão) é interrompida pelo design. É permitido às implementações criar matrizes tão grandes que não end - startsão definidas, porque os tamanhos dos objetos não são assinados e as diferenças de ponteiro são assinadas. Portanto, end - start"funciona mesmo usando ponteiros", desde que você também mantenha o tamanho da matriz abaixo PTRDIFF_MAX. Para ser justo com o padrão, isso não é um obstáculo para a maioria das arquiteturas, já que é metade do tamanho do mapa de memória.
Steve Jessop
3
@Bakuriu: A propósito, existe um botão "editar" na postagem que você pode usar para sugerir alterações (ou fazer você mesmo) se você acha que perdi alguma coisa ou algo não está claro. Eu sou apenas humano, e este post foi visto por mais de dois mil pares de olhos. O tipo de comentário "Você deveria esclarecer ..." realmente me irrita.
Dietrich Epp
18

Podemos usar um exemplo simples para demonstrar esse fato. Suponha que em uma certa matriz grande , estamos tentando encontrar o ponto médio do intervalo [1000, INT_MAX]. Agora, INT_MAXé o maior valor que o inttipo de dados pode armazenar. Mesmo se 1for adicionado a isso, o valor final se tornará negativo.

Além disso, start = 1000e end = INT_MAX.

Usando a fórmula: (start + end)/2,

o ponto médio será

(1000 + INT_MAX)/2= -(INT_MAX+999)/2, que é negativo e pode causar falha de segmentação se tentarmos indexar usando esse valor.

Mas, usando a fórmula, (start + (end-start)/2)obtemos:

(1000 + (INT_MAX-1000)/2)= (1000 + INT_MAX/2 - 500)= (INT_MAX/2 + 500) que não transbordará .

Shubham
fonte
1
Se você adicionar 1 a INT_MAX, o resultado não será negativo, mas indefinido.
Celtschk
@celtschk Teoricamente, sim. Praticamente ele vai embrulhar-em torno de um monte de vezes que vão desde INT_MAXa -INT_MAX. É um mau hábito confiar nisso.
Mast
17

Para acrescentar ao que outros já disseram, o primeiro explica seu significado mais claramente aos menos atentos matematicamente:

mid = start + (end - start) / 2

lê como:

início igual a meio mais metade do comprimento.

enquanto que:

mid = (start + end) / 2

lê como:

meio é igual à metade do início e do fim

O que não parece tão claro quanto o primeiro, pelo menos quando expresso assim.

como Kos apontou, também pode ler:

meio é igual à média do início e do fim

O que é mais claro, mas ainda não é, pelo menos na minha opinião, tão claro quanto o primeiro.

TheLethalCoder
fonte
3
Entendo o seu ponto, mas isso realmente é um exagero. Se você vê "e - s" e pensa em "comprimento", quase certamente vê "(s + e) ​​/ 2" e pensa em "média" ou "médio".
precisa saber é o seguinte
2
@djechlin Os programadores são pobres em matemática. Eles estão ocupados fazendo seu trabalho. Eles não têm tempo para assistir às aulas de matemática.
Little Alien
1

start + (end-start) / 2 pode evitar um possível estouro, por exemplo start = 2 ^ 20 e end = 2 ^ 30

Clube de luta
fonte