关于java:对可能包含数字的字符串进行排序

Sort on a string that may contain a number

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

  • 美国农业协会
  • BBB 3 CCC
  • BBB 12 CCC
  • CCC 11
  • 滴滴涕
  • EEE 3 DDD JPEG2000 EEE
  • EEE 12 DDD JPEG2000 EEE

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

有更好的方法吗?

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


alphanum算法

从网站上

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

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


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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());

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

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


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

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


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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中提出了一个非常简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Comparator<String> 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)));
                }
            }
        }
    };
}

它的工作原理如下:

1
2
3
final List<String> 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很不错,但它不符合我正在进行的项目的要求。我需要能够正确地对负数和小数进行排序。这是我提出的实现。任何反馈都将非常感谢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class StringAsNumberComparator implements Comparator<String> {

    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<String> split1 = split(str1);
        List<String> 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<String> split(String s) {
        List<String> list = new ArrayList<String>();
        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'"来保留令牌,但我不能让它与我正在使用的正则表达式一起工作。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    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中实现了类似的解决方案。也许我的策略会很好地找到你,尽管语法不同。与上面类似,我解析了要比较的两个字符串,并将它们都拆分为数组,以连续的数字分隔字符串。

1
2
3
4
5
...
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/

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.util.Collections;
import java.util.Vector;

public class CompareToken implements Comparable<CompareToken>
{
    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<CompareToken> data = new Vector<CompareToken>();
    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,那么您的类型将是非确定性的。在你的情况下,我担心这可能,尽管我不能证明。考虑一些分析案例,例如:

1
2
3
4
5
6
7
8
9
10
11
  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等)组成。我也有需要删除的副本,所以我只得到一个条目。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
SortedSet<wyn> codeSet;
codeSet = new TreeSet<wyn>(new Comparator<wyn>() {

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解决方案的人来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"。对于这个一般问题没有简单的解决方案。