检查两个十进制字符串的一个衬垫仅相差1 ulp

时间:2017-08-29 10:19:25

标签: java

我有两个以字符串形式出现的十进制数字,它们略有不同。我想要一个将它们视为"等于"如果它们仅相差1 ulp(即只有最后一位数相差1)。

目前我能提出的最易读的形式是:

private static boolean diffByUlp(String oldVal, String newVal) {
    BigDecimal nb = new BigDecimal(newVal);
    return nb.subtract(new BigDecimal(oldVal)).abs().equals(nb.ulp());
}

但是,我真的想在一个表达式中执行此操作(因此它适合if语句)并避免使用昂贵的BigDecimal

(顺便说一下:它们相差超过1倍(二元)ulp。)

有什么建议吗?

4 个答案:

答案 0 :(得分:1)

我认为您正在寻找性能有效的解决方案,因为您已经提到在您的情况下使用BigDecimal过于昂贵。虽然在不了解整个背景的情况下提供有关性能的建议是非常棘手的。您可以考虑基于比较存储为String的两个十进制数字的字符的解决方案。如果你比较的数字从最初的数字开始通常是不同的(例如,比较120.0001512.0可以通过比较两个字符串中的第一个字符来轻松跟踪),它可以给你快速提升。但是,如果在大多数情况下你的数字非常接近,那么你可能会坚持BigDecimal - 这就是用真实数据衡量性能。

下面你可以找到一个基于比较字符串字符的示例解决方案。它处理两个十进制数使用不同精度的情况。此外,在将"1.00""1.00001"进行比较时,第一个数字被“处理”为"1.00000"。您可以将此类用作实用程序类,该类为您提供可在任何if语句中使用的单个静态方法。

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

final class StringDecimal {

    private static final Map<Integer, Integer> charToInt = new ConcurrentHashMap<>();

    static {
        charToInt.put(48, 0);
        charToInt.put(49, 1);
        charToInt.put(50, 2);
        charToInt.put(51, 3);
        charToInt.put(52, 4);
        charToInt.put(53, 5);
        charToInt.put(54, 6);
        charToInt.put(55, 7);
        charToInt.put(56, 8);
        charToInt.put(57, 9);
    }

    private static boolean areEqual(String num1, String num2) {
        int size = Math.min(num1.length(), num2.length()) - 1;

        // 1. Compare first n-1 characters where n is max common length for both strings
        for (int i = 0; i < size; i++) {
            if (num1.charAt(i) != num2.charAt(i)) {
                return false;
            }
        }

        int lastDigitDiff = Math.max(num1.charAt(size), num2.charAt(size)) - Math.min(num1.charAt(size), num2.charAt(size));

        // 2. Check last common digit
        if (lastDigitDiff > 1) {
            return false;
        }

        // 3. If both decimal numbers have same size, they are equal at this moment
        if (num1.length() == num2.length()) {
            return true;
        }

        if (num1.length() > num2.length()) {
            return testRemainingDigits(num1, size);
        }

        return testRemainingDigits(num2, size);
    }

    private static boolean testRemainingDigits(String num, int size) {
        int lastDigitsSum = 0;
        int lastDigit = charToInt.getOrDefault((int) num.charAt(num.length() - 1), 0);

        // 1. Check if last digit is equal to 1
        if (lastDigit > 1) {
            return false;
        }

        // 2. Sum all remaining digits from longer string and accept sum == 1
        for (int i = num.length() - 1; i > size; i--) {
            lastDigitsSum += charToInt.getOrDefault((int) num.charAt(i), 0);
        }

        return lastDigit == 0 && lastDigitsSum == 0 ||
            lastDigit == 1 && lastDigitsSum == 1;
    }

    public static void main(String[] args) {
        List<List<Object>> numbers = Arrays.asList(
            Arrays.asList("1.00", "1.000000", true),
            Arrays.asList("120.0", "121.0", false),
            Arrays.asList("120.0", "120.1", true),
            Arrays.asList("1024.00001", "1024.00000", true),
            Arrays.asList("1024.00002", "1024.00000", false),
            Arrays.asList("1024.00001", "1024.0000", true),
            Arrays.asList("1024.00001", "1024", true),
            Arrays.asList("1024.00010", "1024", false),
            Arrays.asList("1024.00002", "1024", false),
            Arrays.asList("1024.00001", "1025.00001", false)
        );

        for (List<Object> data : numbers) {
            String num1 = (String) data.get(0);
            String num2 = (String) data.get(1);
            boolean expected = (boolean) data.get(2);
            boolean result = areEqual(num1, num2);
            String status = expected == result ? "OK" : "FAILED";

            System.out.println("["+status+"] " + num1 + " == " + num2 + " ? " + result);
        }
    }
}

这是非常必要的,但它仍然很容易理解幕后发生的事情。该算法的复杂度为 O(n)

运行此示例程序会产生以下输出:

[OK] 1.00 == 1.000000 ? true
[OK] 120.0 == 121.0 ? false
[OK] 120.0 == 120.1 ? true
[OK] 1024.00001 == 1024.00000 ? true
[OK] 1024.00002 == 1024.00000 ? false
[OK] 1024.00001 == 1024.0000 ? true
[OK] 1024.00001 == 1024 ? true
[OK] 1024.00010 == 1024 ? false
[OK] 1024.00002 == 1024 ? false
[OK] 1024.00001 == 1025.00001 ? false   

我希望它能帮助您找到问题的最佳解决方案。

答案 1 :(得分:1)

你对一个非常“漂浮”的领域抱有很高的期望。 还有一个,不是那么认真,回答:

static boolean probablySame(String x, String y) {
    return Math.abs(x.hashCode() - y.hashCode()) <= 1;
}

答案 2 :(得分:0)

假设您只需要具有相同长度的字符串。以下可能是一种可能的解决方案。

  • 检查字符串是否相等,除了最后一位
  • 检查最后一位数字不是一个

该代码段应仅展示主体。可以进一步优化。

static boolean diffByUlp(String s1, String s2) {
    for (int i = 0; i < s1.length() - 1; i++) {
        if (s1.charAt(i) != s2.charAt(i)) {
            return false;
        }
    }
    char c1 = s1.charAt(s1.length() - 1);
    char c2 = s2.charAt(s2.length() - 1);
    if (c1 >= c2) {
        return c1-c2 <= 1;
    }
    return c2-c1 <= 1;
}

答案 3 :(得分:0)

因此,您要仔细检查两个十进制值是否仅相差最多1。例如3.22.4(差异为0.8)。

首先,您应该注意BigDecimal的唯一目的是提供无限空间和精度,而不是有限的数据类型double(同样适用于BigIntegerint)。但是,您只能使用它来解析String中的十进制值。正如您已经提到的那样,仅为此目的使用该类是一个相当大的开销。解析值也可以使用Double#parseDouble方法(documentation)来完成,它会返回一个紧凑的小double值。

总而言之,您的代码可能如下所示:

private static boolean differAtMostByOne(final String oldVal, final String newVal) {
    final double oldValAsDouble = Double.parseDouble(oldVal);
    final double newValAsDouble = Double.parseDouble(newVal),

    final double difference = Math.abs(oldValAsDouble - newValAsDouble);

    final double compareTo = 1.0;
    final double precision = 0.000001;
    final boolean differByAtMostOne = difference <= compareTo + precision;

    return differByAtMostOne;
}

或者同样的契约:

private static boolean differAtMostByOne(final String oldVal, final String newVal) {
    return Math.abs(Double.parseDouble(oldVal) - Double.parseDouble(newVal)) < 1.000001;
}

请注意,与十进制值比较时,应避免与值1.0直接比较。相反,您应该允许值周围的小区域来解释精度损失。

否则,您可能会输入差别正好为1的值,但计算机可能会使用1.000000000000000001之类的值来表示它,并且程序也应该接受它,因此精度区域也是如此。