Проблемы с переполнением при реализации математических формул

Я слышал, что при вычислении среднего значения start + (end-start)/2 отличается от (start + end)/2, потому что последнее может вызвать переполнение. Я не совсем понимаю, почему эта вторая может вызвать переполнение, а первая - нет. Каково общее правило для реализации математической формулы, которая может избежать переполнения.

Ответ 1

Предположим, вы используете компьютер, где максимальное целочисленное значение равно 10, и вы хотите вычислить среднее значение 5 и 7.

Первый метод (begin + (end-begin)/2) дает

5 + (7-5)/2 == 5 + 2/2 == 6

Второй метод (begin + end)/2 дает переполнение, так как промежуточное значение 12 превышает максимальное значение 10, которое мы принимаем и "обертываем" на что-то другое (если вы используете неподписанные числа, обычные для оберните назад к нулю, но если ваши номера подписаны, вы можете получить отрицательное число!).

12/2 => overflow occurs => 2/2 == 1

Конечно, в реальных компьютерах целые числа переполняются с большим значением, например 2 ^ 32 вместо 10, но идея такая же. К сожалению, нет "общего" способа избавиться от переполнения, о котором я знаю, и это сильно зависит от того, какой конкретный алгоритм вы используете. И тогда события становятся все сложнее. Вы можете получить другое поведение в зависимости от того, какой тип номера вы используете под капотом, и есть другие виды числовых ошибок, о которых можно беспокоиться в дополнение к переполнению и недоиспользованию.

Ответ 2

Оба ваши формулы будут переполняться, но при разных обстоятельствах:

  • Часть (start+end) вашей формулы (start+end)/2 будет переполняться, когда start и end будут близки к целочисленному пределу на той же стороне диапазона (т.е. как положительные, так и отрицательные).
  • Часть (end-start) формулы start+(end-start)/2 будет переполняться, когда start будет положительной, а end будет отрицательной, и оба значения будут близки к соответствующим концам представляемых целых значений.

Нет "общих" правил, вы делаете это в каждом конкретном случае: посмотрите на части своей формулы, подумайте о ситуациях, которые могут вызвать переполнение, и придумайте, как избежать этого. Например, можно показать формулу start+(end-start)/2, чтобы избежать переполнения при средних значениях с тем же знаком.

Это трудный путь; простым способом является использование представлений более высокой емкости для промежуточных результатов. Например, если вы используете long long вместо int для промежуточных вычислений и скопируйте результаты обратно на int только когда вы закончите, вы избежите переполнения, считая, что конечный результат соответствует int.

Ответ 3

При использовании целых чисел вы, вероятно, заботитесь о целочисленном переполнении при принятии таких стратегий.

Обратите внимание, что с использованием формулы b+(b-a)/2 вы должны убедиться, что a <= b. В противном случае вы можете получить ту же проблему на нижней границе возможного диапазона значений. Подумайте о a/2+b/2. Однако есть и другие недостатки этого подхода.


При работе с числами с плавающей запятой возникает еще одна проблема, катастрофическая отмена. Из-за ограниченного числа значимых цифр представления с плавающей запятой точность теряется при добавлении больших чисел (даже если это всего лишь промежуточный шаг).

Для решения этой проблемы численной стабильности, например. этот алгоритм может быть использован (слегка адаптирован из wikipedia):

def online_mean(data):
  n = 0
  mean = 0

  for x in data:
    n = n + 1
    delta = x - mean
    mean = mean + delta/n

  return mean

Я как-то чувствовал, что существует связь с формулой, которую вы указали выше...

Ответ 4

В двоичном поиске мы напишем следующий код:

if(start > end){
   return;
}
int mid = start + (end - start) / 2;

Используя start + (end - start) / 2, мы можем избежать проблем, на которые указывает @dasblinkenlight

если мы используем (start + end) / 2, он будет переполняться, как показано dasblinkenlight