Как лучше суммировать много чисел с плавающей точкой? - PullRequest
32 голосов
/ 26 декабря 2008

Представьте, что у вас есть большой массив чисел с плавающей запятой разных размеров. Как правильно рассчитать сумму с наименьшей ошибкой? Например, когда массив выглядит так:

[1.0, 1e-10, 1e-10, ... 1e-10.0]

и вы сложите слева направо с помощью простого цикла, как

sum = 0
numbers.each do |val|
    sum += val
end

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

EDIT : Спасибо за ответ, теперь у меня есть рабочий код, который отлично суммирует двойные значения в Java. Это прямой порт из сообщения Python о победившем ответе. Решение проходит все мои юнит-тесты. (Более длинная, но оптимизированная версия доступна здесь Summarizer.java )

/**
 * Adds up numbers in an array with perfect precision, and in O(n).
 * 
 * @see http://code.activestate.com/recipes/393090/
 */
public class Summarizer {

    /**
     * Perfectly sums up numbers, without rounding errors (if at all possible).
     * 
     * @param values
     *            The values to sum up.
     * @return The sum.
     */
    public static double msum(double... values) {
        List<Double> partials = new ArrayList<Double>();
        for (double x : values) {
            int i = 0;
            for (double y : partials) {
                if (Math.abs(x) < Math.abs(y)) {
                    double tmp = x;
                    x = y;
                    y = tmp;
                }
                double hi = x + y;
                double lo = y - (hi - x);
                if (lo != 0.0) {
                    partials.set(i, lo);
                    ++i;
                }
                x = hi;
            }
            if (i < partials.size()) {
                partials.set(i, x);
                partials.subList(i + 1, partials.size()).clear();
            } else {
                partials.add(x);
            }
        }
        return sum(partials);
    }

    /**
     * Sums up the rest of the partial numbers which cannot be summed up without
     * loss of precision.
     */
    public static double sum(Collection<Double> values) {
        double s = 0.0;
        for (Double d : values) {
            s += d;
        }
        return s;
    }
}

Ответы [ 5 ]

24 голосов
/ 26 декабря 2008

Для «более точного»: этот рецепт в Python Cookbook имеет алгоритмы суммирования, которые сохраняют полную точность (отслеживая промежуточные итоги). Код написан на Python, но даже если вы не знаете Python, он достаточно понятен для адаптации к любому другому языку.

Все детали приведены в этом документе .

13 голосов
/ 28 января 2009

См. Также: Алгоритм суммирования Кахана Не требует хранения O (n), а только O (1).

3 голосов
/ 30 декабря 2010

Есть много алгоритмов, в зависимости от того, что вы хотите. Обычно они требуют отслеживания частичных сумм. Если вы сохраняете только суммы x [k + 1] - x [k], вы получаете алгоритм Кахана. Если вы отслеживаете все частичные суммы (отсюда и алгоритм O (n ^ 2)), вы получите ответ @dF.

Обратите внимание, что в дополнение к вашей проблеме суммирование чисел различных знаков очень проблематично.

Теперь есть более простые рецепты, чем отслеживание всех частичных сумм:

  • Сортируйте числа перед суммированием, суммируйте все негативы и позитивы независимо. Если у вас есть отсортированные числа, хорошо, в противном случае у вас есть алгоритм O (n log n). Сумма по нарастающей величине.
  • Сумма по парам, затем по парам и т. Д.

Личный опыт показывает, что вам обычно не нужны более причудливые вещи, чем метод Кахана.

0 голосов
/ 27 декабря 2008

Если ваше приложение использует числовую обработку для поиска арифметической библиотеки произвольной точности, однако я не знаю, существуют ли библиотеки Python такого рода. Конечно, все зависит от того, сколько прецизионных цифр вы хотите - вы можете добиться хороших результатов со стандартным IEEE с плавающей запятой, если будете использовать его осторожно.

0 голосов
/ 27 декабря 2008

Что ж, если вы не хотите сортировать, вы можете просто сохранить итоговое значение в переменной с типом более высокой точности, чем отдельные значения (например, используйте двойное число, чтобы сохранить сумму с плавающей запятой, или «квад»). сохранить сумму двойников). Это приведет к снижению производительности, но может быть меньше стоимости сортировки.

...