我需要编写一个比较比较字符串的Java比较器类,但是只有一个扭曲。如果要比较的两个字符串在字符串的开头和结尾处是相同的,而不同的中间部分是整数,则根据这些整数的数值进行比较。例如,我希望以下字符串按显示顺序结束:

美国农业协会

BBB 3 CCC

BBB 12 CCC

CCC 11

滴滴涕

EEE 3 DDD JPEG2000 EEE

EEE 12 DDD JPEG2000 EEE

如您所见,字符串中可能还有其他整数,所以我不能只使用正则表达式来分解任何整数。我正在考虑从一开始就遍历字符串,直到找到一个不匹配的位,然后从末尾走进,直到找到一个不匹配的位,然后将中间的位与正则表达式"[0-9]+"进行比较,如果进行比较,则进行数值比较,否则进行词汇比较。

有更好的方法吗?

更新我认为我不能保证字符串中的其他数字,可能匹配的数字,它们周围没有空格,或者不同的数字确实有空格。

alphanum算法

从网站上

"人们对数字字符串的排序与软件不同。大多数排序算法比较ASCII值,它产生的排序与人类逻辑不一致。以下是解决问题的方法。"

编辑:这里有一个指向该站点的Java比较器实现的链接。

这并不能完全解决这个问题——您需要对要排序的字符串进行标记,并使用该算法对每个片段进行单独排序。

注:保罗接受了你的答案,但我的算法更接近他的问题(解释的方式!),例如"Allegia 51B Cristeron"。没问题,他选择了适合自己需要的任何东西,这个alphanum实现很好(而且是多语言!)我只是想指出这一点。-P

请注意,它是GNU较低许可证

谢谢。为什么不把它推到gist.github.com?!

此实现处理操作的特定示例输入,但一般情况下,请注意,它无法处理前导零的数字。认为"01234"大于"5678"。

有趣的小挑战,我喜欢解决它。

以下是我对这个问题的看法:

String[] strs =
{
"eee 5 ddd jpeg2001 eee",
"eee 123 ddd jpeg2000 eee",
"ddd",
"aaa 5 yy 6",
"ccc 555",
"bbb 3 ccc",
"bbb 9 a",
"",
"eee 4 ddd jpeg2001 eee",
"ccc 11",
"bbb 12 ccc",
"aaa 5 yy 22",
"aaa",
"eee 3 ddd jpeg2000 eee",
"ccc 5",
};
Pattern splitter = Pattern.compile("(\\d+|\\D+)");
public class InternalNumberComparator implements Comparator
{
public int compare(Object o1, Object o2)
{
// I deliberately use the Java 1.4 syntax,
// all this can be improved with 1.5's generics
String s1 = (String)o1, s2 = (String)o2;
// We split each string as runs of number/non-number strings
ArrayList sa1 = split(s1);
ArrayList sa2 = split(s2);
// Nothing or different structure
if (sa1.size() == 0 || sa1.size() != sa2.size())
{
// Just compare the original strings
return s1.compareTo(s2);
}
int i = 0;
String si1 ="";
String si2 ="";
// Compare beginning of string
for (; i < sa1.size(); i++)
{
si1 = (String)sa1.get(i);
si2 = (String)sa2.get(i);
if (!si1.equals(si2))
break;  // Until we find a difference
}
// No difference found?
if (i == sa1.size())
return 0; // Same strings!
// Try to convert the different run of characters to number
int val1, val2;
try
{
val1 = Integer.parseInt(si1);
val2 = Integer.parseInt(si2);
}
catch (NumberFormatException e)
{
return s1.compareTo(s2);  // Strings differ on a non-number
}
// Compare remainder of string
for (i++; i < sa1.size(); i++)
{
si1 = (String)sa1.get(i);
si2 = (String)sa2.get(i);
if (!si1.equals(si2))
{
return s1.compareTo(s2);  // Strings differ
}
}
// Here, the strings differ only on a number
return val1 < val2 ? -1 : 1;
}
ArrayList split(String s)
{
ArrayList r = new ArrayList();
Matcher matcher = splitter.matcher(s);
while (matcher.find())
{
String m = matcher.group(1);
r.add(m);
}
return r;
}
}
Arrays.sort(strs, new InternalNumberComparator());

这个算法需要更多的测试,但它的性能似乎相当好。

[编辑]我添加了更多的评论以便更清楚。我看到有比我开始编写代码时更多的答案…但我希望我能提供一个良好的起点和/或一些想法。

好一个!另外一个空值和instanceof字符串检查也很好。

@hrgiger您有一个关于空检查的观点,我假设数组是"正常的"。但今天,我只会放弃前Java 1.5语法,使用泛型,而不是使用实例。

微软的Ian Griffiths有一个他称之为"自然排序"的C实现。无论如何,移植到Java应该相当容易,比C更容易!

更新:在EkkRoad上有一个Java示例,它可以做到这一点,看到"比较自然"并使用它作为比较器来排序。

我在这里提出的实现简单高效。它不通过使用正则表达式或方法(如substring()、split()、tochararray()等)直接或间接分配任何额外内存。

这个实现首先遍历两个字符串,以最大速度搜索不同的第一个字符,在此期间不做任何特殊处理。只有当这些字符都是数字时,才会触发特定的数字比较。这种实现的一个副作用是,一个数字被认为比其他字母大,与默认的词典顺序相反。

public static final int compareNatural (String s1, String s2)
{
// Skip all identical characters
int len1 = s1.length();
int len2 = s2.length();
int i;
char c1, c2;
for (i = 0, c1 = 0, c2 = 0; (i < len1) && (i < len2) && (c1 = s1.charAt(i)) == (c2 = s2.charAt(i)); i++);
// Check end of string
if (c1 == c2)
return(len1 - len2);
// Check digit in first string
if (Character.isDigit(c1))
{
// Check digit only in first string
if (!Character.isDigit(c2))
return(1);
// Scan all integer digits
int x1, x2;
for (x1 = i + 1; (x1 < len1) && Character.isDigit(s1.charAt(x1)); x1++);
for (x2 = i + 1; (x2 < len2) && Character.isDigit(s2.charAt(x2)); x2++);
// Longer integer wins, first digit otherwise
return(x2 == x1 ? c1 - c2 : x1 - x2);
}
// Check digit only in second string
if (Character.isDigit(c2))
return(-1);
// No digits
return(c1 - c2);
}

我知道你在Java,但是你可以看看StrucPrLogiCW是如何工作的。这是资源管理器在Windows中用来排序文件名的方法。您可以在这里查看Wine实现。

将字符串拆分为字母和数字,使"foo 12 bar"成为列表("foo",12,"bar"),然后使用列表作为排序键。这样,数字将按数字顺序排列,而不是按字母顺序排列。

我用正则表达式在Java中提出了一个非常简单的实现:

public static Comparator naturalOrdering() {
final Pattern compile = Pattern.compile("(\\d+)|(\\D+)");
return (s1, s2) -> {
final Matcher matcher1 = compile.matcher(s1);
final Matcher matcher2 = compile.matcher(s2);
while (true) {
final boolean found1 = matcher1.find();
final boolean found2 = matcher2.find();
if (!found1 || !found2) {
return Boolean.compare(found1, found2);
} else if (!matcher1.group().equals(matcher2.group())) {
if (matcher1.group(1) == null || matcher2.group(1) == null) {
return matcher1.group().compareTo(matcher2.group());
} else {
return Integer.valueOf(matcher1.group(1)).compareTo(Integer.valueOf(matcher2.group(1)));
}
}
}
};
}

它的工作原理如下:

final List strings = Arrays.asList("x15","xa","y16","x2a","y11","z","z5","x2b","z");
strings.sort(naturalOrdering());
System.out.println(strings);
[x2a, x2b, x15, xa, y11, y16, z, z, z5]

Alphanum Algrothim很不错,但它不符合我正在进行的项目的要求。我需要能够正确地对负数和小数进行排序。这是我提出的实现。任何反馈都将非常感谢。

public class StringAsNumberComparator implements Comparator {
public static final Pattern NUMBER_PATTERN = Pattern.compile("(\\-?\\d+\\.\\d+)|(\\-?\\.\\d+)|(\\-?\\d+)");
/**
* Splits strings into parts sorting each instance of a number as a number if there is
* a matching number in the other String.
*
* For example A1B, A2B, A11B, A11B1, A11B2, A11B11 will be sorted in that order instead
* of alphabetically which will sort A1B and A11B together.
*/
public int compare(String str1, String str2) {
if(str1 == str2) return 0;
else if(str1 == null) return 1;
else if(str2 == null) return -1;
List split1 = split(str1);
List split2 = split(str2);
int diff = 0;
for(int i = 0; diff == 0 && i < split1.size() && i < split2.size(); i++) {
String token1 = split1.get(i);
String token2 = split2.get(i);
if((NUMBER_PATTERN.matcher(token1).matches() && NUMBER_PATTERN.matcher(token2).matches()) {
diff = (int) Math.signum(Double.parseDouble(token1) - Double.parseDouble(token2));
} else {
diff = token1.compareToIgnoreCase(token2);
}
}
if(diff != 0) {
return diff;
} else {
return split1.size() - split2.size();
}
}
/**
* Splits a string into strings and number tokens.
*/
private List split(String s) {
List list = new ArrayList();
try (Scanner scanner = new Scanner(s)) {
int index = 0;
String num = null;
while ((num = scanner.findInLine(NUMBER_PATTERN)) != null) {
int indexOfNumber = s.indexOf(num, index);
if (indexOfNumber > index) {
list.add(s.substring(index, indexOfNumber));
}
list.add(num);
index = indexOfNumber + num.length();
}
if (index < s.length()) {
list.add(s.substring(index));
}
}
return list;
}
}

我想使用Java.Lang.Stulk.SultId()方法,使用"LooWald/LoopBeld'"来保留令牌,但我不能让它与我正在使用的正则表达式一起工作。

您可能需要缓存您的Pattern.compile()调用,因为它们是以O(N log N)复杂性调用的!

好建议。代码已更新。扫描仪现在也关闭使用"尝试资源"。

您可以直接调用NUMBER_PATTERN.matcher(s),然后在返回的Matcher上重复调用find,而不是处理Scanner。最棒的是,Matcher会告诉您每次匹配的开始和结束位置,使得整个拆分操作变得微不足道。它不是一个需要try(…) {…}块的资源。

@霍尔格有趣的想法。我将实现它并作为一个单独的答案。我给你投赞成票。

我不知道它是否独特,值得另一个答案。毕竟,它仍然会做同样的事情。顺便说一下,最初的声明if(str1 == null || str2 == null) { return 0; }被破坏了,因为它意味着如果其中一个论点是null,它将被报告为与另一个论点相等。但当null等于任何其他输入时,所有输入必须相等(传递性规则)。最简单的解决方案是根本不支持null。否则,您将不得不使用类似于if(str1 == str2) return 0; if(str1 == null) return 1; if(str2 == null) return -1;的东西。

@Holger我不同意如何处理nulls stackoverflow.com/a/2401629/724835

链接的答案描述了一致的行为。它甚至符合我的第二个建议,if(str1 == str2) return 0; if(str1 == null) return 1; if(str2 == null) return -1;。在所有三种情况下,您的答案代码都返回零,这三种情况不一致并且违反了Comparator的合同。它与链接答案中描述的行为不匹配。

@霍尔格,我站得对。我误解了你的第一个建议。请更新我的答案以更好地处理空值。

我的2美分。对我来说工作得很好。我主要把它用于文件名。

private final boolean isDigit(char ch)
{
return ch >= 48 && ch <= 57;
}
private int compareNumericalString(String s1,String s2){
int s1Counter=0;
int s2Counter=0;
while(true){
if(s1Counter>=s1.length()){
break;
}
if(s2Counter>=s2.length()){
break;
}
char currentChar1=s1.charAt(s1Counter++);
char currentChar2=s2.charAt(s2Counter++);
if(isDigit(currentChar1) &&isDigit(currentChar2)){
String digitString1=""+currentChar1;
String digitString2=""+currentChar2;
while(true){
if(s1Counter>=s1.length()){
break;
}
if(s2Counter>=s2.length()){
break;
}
if(isDigit(s1.charAt(s1Counter))){
digitString1+=s1.charAt(s1Counter);
s1Counter++;
}
if(isDigit(s2.charAt(s2Counter))){
digitString2+=s2.charAt(s2Counter);
s2Counter++;
}
if((!isDigit(s1.charAt(s1Counter))) && (!isDigit(s2.charAt(s2Counter)))){
currentChar1=s1.charAt(s1Counter);
currentChar2=s2.charAt(s2Counter);
break;
}
}
if(!digitString1.equals(digitString2)){
return Integer.parseInt(digitString1)-Integer.parseInt(digitString2);
}
}
if(currentChar1!=currentChar2){
return currentChar1-currentChar2;
}
}
return s1.compareTo(s2);
}

在发现这个线程之前,我在JavaScript中实现了类似的解决方案。也许我的策略会很好地找到你,尽管语法不同。与上面类似,我解析了要比较的两个字符串,并将它们都拆分为数组,以连续的数字分隔字符串。

...
var regex = /(\d+)/g,
str1Components = str1.split(regex),
str2Components = str2.split(regex),
...

例如,"hello 22再见33"=>["hello",22,"再见",33];因此,您可以在string1和string2之间成对遍历数组元素,执行一些类型强制(例如,此元素真的是数字吗?)并在走路时进行比较。

这里的工作示例:http://jsfiddle.net/f46s6/3/

注意,我目前只支持整数类型,尽管处理十进制值不会太难修改。

有趣的问题,这里是我提出的解决方案:

import java.util.Collections;
import java.util.Vector;
public class CompareToken implements Comparable
{
int valN;
String valS;
String repr;
public String toString() {
return repr;
}
public CompareToken(String s) {
int l = 0;
char data[] = new char[s.length()];
repr = s;
valN = 0;
for (char c : s.toCharArray()) {
if(Character.isDigit(c))
valN = valN * 10 + (c - '0');
else
data[l++] = c;
}
valS = new String(data, 0, l);
}
public int compareTo(CompareToken b) {
int r = valS.compareTo(b.valS);
if (r != 0)
return r;
return valN - b.valN;
}
public static void main(String [] args) {
String [] strings = {
"aaa",
"bbb3ccc",
"bbb12ccc",
"ccc 11",
"ddd",
"eee3dddjpeg2000eee",
"eee12dddjpeg2000eee"
};
Vector data = new Vector();
for(String s : strings)
data.add(new CompareToken(s));
Collections.shuffle(data);
Collections.sort(data);
for (CompareToken c : data)
System.out.println ("" + c);
}
}

我想你得把一个字一个字地比较一下。抓取一个字符,如果是数字字符,则继续抓取,然后重新组合成一个数字字符串,并将其转换为一个int。对另一个字符串重复,然后再进行比较。

简短的回答:根据上下文,我无法判断这是否只是一些用于个人用途的快速而肮脏的代码,还是高盛最新内部会计软件的关键部分,因此我将以"eww"开头。这是一个相当棘手的排序算法;如果可以的话,尽量使用一些不那么"曲折"的方法。

长回答:

在您的案例中,立即想到的两个问题是性能和正确性。非正式地说,确保它是快速的,并确保您的算法是完全排序的。

(当然,如果排序不超过100个项目,您可能会忽略这一段。)性能很重要,因为比较器的速度将是排序速度的最大因素(假设排序算法对于典型列表是"理想的")。在您的例子中,比较器的速度主要取决于字符串的大小。字符串似乎相当短,因此它们可能不会像您的列表大小那样占据主导地位。

将每个字符串转换成字符串数字字符串元组,然后按照另一个答案中的建议对该元组列表进行排序,在某些情况下会失败,因为显然会出现多个数字的字符串。

另一个问题是正确性。具体来说,如果您描述的算法允许A>B>。>A,那么您的类型将是非确定性的。在你的情况下,我担心这可能,尽管我不能证明。考虑一些分析案例,例如:

aa 0 aa
aa 23aa
aa 2a3aa
aa 113aa
aa 113 aa
a 1-2 a
a 13 a
a 12 a
a 2-3 a
a 21 a
a 2.3 a

我的问题是我有一个列表,由字母数字字符串(如c22、c3、c5等)的组合、字母字符串(如a、h、r等)和只需要按顺序A、c3、c5、c22、h、r、45、99排序的数字(如99、45等)组成。我也有需要删除的副本,所以我只得到一个条目。

我不仅处理字符串,还对对象排序,并使用对象中的特定字段来获得正确的顺序。

一个对我有用的解决方案是:

SortedSet codeSet;
codeSet = new TreeSet(new Comparator() {
private boolean isThereAnyNumber(String a, String b) {
return isNumber(a) || isNumber(b);
}
private boolean isNumber(String s) {
return s.matches("[-+]?\\d*\\.?\\d+");
}
private String extractChars(String s) {
String chars = s.replaceAll("\\d","");
return chars;
}
private int extractInt(String s) {
String num = s.replaceAll("\\D","");
return num.isEmpty() ? 0 : Integer.parseInt(num);
}
private int compareStrings(String o1, String o2) {
if (!extractChars(o1).equals(extractChars(o2))) {
return o1.compareTo(o2);
} else
return extractInt(o1) - extractInt(o2);
}
@Override
public int compare(Code a, Code b) {
return isThereAnyNumber(a.getPrimaryCode(), b.getPrimaryCode())
? isNumber(a.getPrimaryCode()) ? 1 : -1
: compareStrings(a.getPrimaryCode(), b.getPrimaryCode());
}
});

它"借用"了我在StackOverflow上找到的一些代码,加上我自己的一些调整,让它按照我需要的方式工作。

由于试图排序对象,需要一个比较器以及重复的删除,我不得不使用一种消极的敷衍方法,那就是在将对象写入TreeSet之前,我必须先将对象写入TreeMap。它可能会对性能有一点影响,但是考虑到列表最多可以有80个代码,这不应该是一个问题。

尽管这个问题问了一个Java解决方案,但是对于任何想要Scala解决方案的人来说:

object Alphanum {
private[this] val regex ="((?<=[0-9])(?=[^0-9]))|((?<=[^0-9])(?=[0-9]))"
private[this] val alphaNum: Ordering[String] = Ordering.fromLessThan((ss1: String, ss2: String) => (ss1, ss2) match {
case (sss1, sss2) if sss1.matches("[0-9]+") && sss2.matches("[0-9]+") => sss1.toLong < sss2.toLong
case (sss1, sss2) => sss1 < sss2
})
def ordering: Ordering[String] = Ordering.fromLessThan((s1: String, s2: String) => {
import Ordering.Implicits.infixOrderingOps
implicit val ord: Ordering[List[String]] = Ordering.Implicits.seqDerivedOrdering(alphaNum)
s1.split(regex).toList < s2.split(regex).toList
})
}

如果您正在编写一个Comparator类,那么您应该实现自己的Compare方法,该方法将逐个字符比较两个字符串。这个比较方法应该检查您处理的是字母字符、数字字符还是混合类型(包括空格)。您必须定义混合类型的操作方式、数字是在字母字符之前还是在字母字符之后,以及空格的位置等。

在给定的示例中,要比较的数字周围有空格,而其他数字周围没有空格,那么为什么正则表达式不起作用呢?

BBB 12 CCC
VS
EEE 12 DDD JPEG2000 EEE

在Linux上,glibc提供strverscmp(),它也可以从gnulib获得可移植性。然而真正的"人类"分类还有很多其他的怪癖,比如"披头士",被分类为"披头士,the"。对于这个一般问题没有简单的解决方案。