我有两个以字符串形式出现的十进制数字,它们略有不同。我想要一个将它们视为"等于"如果它们仅相差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。)
有什么建议吗?
答案 0 :(得分:1)
我认为您正在寻找性能有效的解决方案,因为您已经提到在您的情况下使用BigDecimal
过于昂贵。虽然在不了解整个背景的情况下提供有关性能的建议是非常棘手的。您可以考虑基于比较存储为String
的两个十进制数字的字符的解决方案。如果你比较的数字从最初的数字开始通常是不同的(例如,比较120.0001
和512.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.2
和2.4
(差异为0.8
)。
首先,您应该注意BigDecimal
的唯一目的是提供无限空间和精度,而不是有限的数据类型double
(同样适用于BigInteger
和int
)。但是,您只能使用它来解析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
之类的值来表示它,并且程序也应该接受它,因此精度区域也是如此。