84

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

  • 啊啊啊
  • bbb 3 cc
  • bbb 12 cc
  • ccc 11
  • ddd
  • eee 3 ddd jpeg2000 eee
  • eee 12 ddd jpeg2000 eee

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

有没有更好的办法?

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

4

24 回答 24

107

字母算法

从网站

“人们用数字排序字符串的方式与软件不同。大多数排序算法都会比较 ASCII 值,这会产生与人类逻辑不一致的排序。下面是解决方法。”

编辑:这是来自该站点的Java 比较器实现的链接。

于 2008-09-19T19:17:14.230 回答
12

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

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

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());

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

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

于 2008-09-19T21:12:42.050 回答
9

Microsoft 的 Ian Griffiths 有一个他称之为Natural Sorting的 C# 实现。移植到 Java 应该相当容易,比从 C 移植更容易!

更新:eekboom上似乎有一个 Java 示例可以执行此操作,请参阅“compareNatural”并将其用作排序比较器。

于 2008-09-19T19:23:38.067 回答
7

我在这里提出的实现简单而高效。它不会直接或间接地通过使用正则表达式或方法(如 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);
}
于 2014-12-17T16:51:01.960 回答
6

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

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)));
                }
            }
        }
    };
}

下面是它的工作原理:

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]

于 2017-07-19T11:22:19.023 回答
5

我知道您使用的是 java,但您可以看看 StrCmpLogicalW 的工作原理。这是资源管理器用来在 Windows 中对文件名进行排序的方法。您可以在此处查看 WINE 实现。

于 2008-09-19T19:18:11.137 回答
4

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

于 2008-09-19T19:07:53.503 回答
4

这是与 Alphanum 算法相比具有以下优势的解决方案:

  1. 快 3.25 倍(根据Alphanum 描述的“尾声”一章的数据进行测试)
  2. 不消耗额外内存(无字符串拆分,无数字解析)
  3. 正确处理前导零(例如"0001"等于"1""01234"小于"4567"
public class NumberAwareComparator implements Comparator<String>
{
    @Override
    public int compare(String s1, String s2)
    {
        int len1 = s1.length();
        int len2 = s2.length();
        int i1 = 0;
        int i2 = 0;
        while (true)
        {
            // handle the case when one string is longer than another
            if (i1 == len1)
                return i2 == len2 ? 0 : -1;
            if (i2 == len2)
                return 1;

            char ch1 = s1.charAt(i1);
            char ch2 = s2.charAt(i2);
            if (Character.isDigit(ch1) && Character.isDigit(ch2))
            {
                // skip leading zeros
                while (i1 < len1 && s1.charAt(i1) == '0')
                    i1++;
                while (i2 < len2 && s2.charAt(i2) == '0')
                    i2++;

                // find the ends of the numbers
                int end1 = i1;
                int end2 = i2;
                while (end1 < len1 && Character.isDigit(s1.charAt(end1)))
                    end1++;
                while (end2 < len2 && Character.isDigit(s2.charAt(end2)))
                    end2++;

                int diglen1 = end1 - i1;
                int diglen2 = end2 - i2;

                // if the lengths are different, then the longer number is bigger
                if (diglen1 != diglen2)
                    return diglen1 - diglen2;

                // compare numbers digit by digit
                while (i1 < end1)
                {
                    if (s1.charAt(i1) != s2.charAt(i2))
                        return s1.charAt(i1) - s2.charAt(i2);
                    i1++;
                    i2++;
                }
            }
            else
            {
                // plain characters comparison
                if (ch1 != ch2)
                    return ch1 - ch2;
                i1++;
                i2++;
            }
        }
    }
}
于 2019-10-05T15:57:39.967 回答
3

我建议不要重新发明轮子,而是使用具有来自ICU4J 库的内置数字排序的区域设置感知 Unicode 兼容字符串比较器。

import com.ibm.icu.text.Collator;
import com.ibm.icu.text.RuleBasedCollator;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class CollatorExample {
    public static void main(String[] args) {
        // Make sure to choose correct locale: in Turkish uppercase of "i" is "İ", not "I"
        RuleBasedCollator collator = (RuleBasedCollator) Collator.getInstance(Locale.US);
        collator.setNumericCollation(true); // Place "10" after "2"
        collator.setStrength(Collator.PRIMARY); // Case-insensitive
        List<String> strings = Arrays.asList("10", "20", "A20", "2", "t1ab", "01", "T010T01", "t1aB",
            "_2", "001", "_200", "1", "A 02", "t1Ab", "a2", "_1", "t1A", "_01",
            "100", "02", "T0010T01", "t1AB", "10", "A01", "010", "t1a"
        );
        strings.sort(collator);
        System.out.println(String.join(", ", strings));
        // Output: _1, _01, _2, _200, 01, 001, 1,
        // 2, 02, 10, 10, 010, 20, 100, A 02, A01, 
        // a2, A20, t1A, t1a, t1ab, t1aB, t1Ab, t1AB,
        // T010T01, T0010T01
    }
}
于 2020-11-17T17:20:29.297 回答
2

Alphanum algrothim 很好,但它不符合我正在从事的项目的要求。我需要能够正确排序负数和小数。这是我提出的实现。任何反馈将不胜感激。

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;
    }
}

PS。我想使用 java.lang.String.split() 方法并使用“lookahead/lookbehind”来保留标记,但我无法让它与我正在使用的正则表达式一起工作。

于 2011-05-19T23:57:59.650 回答
1

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

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);
    }

}
于 2012-07-21T22:13:02.250 回答
1

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

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

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

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

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

于 2014-07-25T05:59:12.907 回答
1

我的 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);
        }
于 2015-06-07T14:53:23.183 回答
1

我创建了一个项目来比较不同的实现。它远未完成,但它是一个起点。

于 2019-09-07T20:09:31.963 回答
1

补充@stanislav的答案。我在使用提供的答案时遇到的一些问题是:

  1. 大写和小写字母由它们的 ASCII 代码之间的字符分隔。当正在排序的字符串具有 _ 或其他介于 ASCII 小写字母和大写字母之间的字符时,这会中断流程。
  2. 如果除了前导零计数不同之外,两个字符串相同,则函数返回 0,这将使排序取决于字符串在列表中的原始位置。

这两个问题已在新代码中得到修复。我做了一些函数而不是一些重复的代码集。differentCaseCompared 变量跟踪两个字符串是否相同,除了大小写不同。如果是这样,则返回减去的第一个不同大小写字符的值。这样做是为了避免两个字符串因大小写不同而返回为 0 的问题。


public class NaturalSortingComparator implements Comparator<String> {

    @Override
    public int compare(String string1, String string2) {
        int lengthOfString1 = string1.length();
        int lengthOfString2 = string2.length();
        int iteratorOfString1 = 0;
        int iteratorOfString2 = 0;
        int differentCaseCompared = 0;
        while (true) {
            if (iteratorOfString1 == lengthOfString1) {
                if (iteratorOfString2 == lengthOfString2) {
                    if (lengthOfString1 == lengthOfString2) {
                        // If both strings are the same except for the different cases, the differentCaseCompared will be returned
                        return differentCaseCompared;
                    }
                    //If the characters are the same at the point, returns the difference between length of the strings
                    else {
                        return lengthOfString1 - lengthOfString2;
                    }
                }
                //If String2 is bigger than String1
                else
                    return -1;
            }
            //Check if String1 is bigger than string2
            if (iteratorOfString2 == lengthOfString2) {
                return 1;
            }

            char ch1 = string1.charAt(iteratorOfString1);
            char ch2 = string2.charAt(iteratorOfString2);

            if (Character.isDigit(ch1) && Character.isDigit(ch2)) {
                // skip leading zeros
                iteratorOfString1 = skipLeadingZeroes(string1, lengthOfString1, iteratorOfString1);
                iteratorOfString2 = skipLeadingZeroes(string2, lengthOfString2, iteratorOfString2);

                // find the ends of the numbers
                int endPositionOfNumbersInString1 = findEndPositionOfNumber(string1, lengthOfString1, iteratorOfString1);
                int endPositionOfNumbersInString2 = findEndPositionOfNumber(string2, lengthOfString2, iteratorOfString2);

                int lengthOfDigitsInString1 = endPositionOfNumbersInString1 - iteratorOfString1;
                int lengthOfDigitsInString2 = endPositionOfNumbersInString2 - iteratorOfString2;

                // if the lengths are different, then the longer number is bigger
                if (lengthOfDigitsInString1 != lengthOfDigitsInString2)
                    return lengthOfDigitsInString1 - lengthOfDigitsInString2;

                // compare numbers digit by digit
                while (iteratorOfString1 < endPositionOfNumbersInString1) {

                    if (string1.charAt(iteratorOfString1) != string2.charAt(iteratorOfString2))
                        return string1.charAt(iteratorOfString1) - string2.charAt(iteratorOfString2);

                    iteratorOfString1++;
                    iteratorOfString2++;
                }
            } else {
                // plain characters comparison
                if (ch1 != ch2) {
                    if (!ignoreCharacterCaseEquals(ch1, ch2))
                        return Character.toLowerCase(ch1) - Character.toLowerCase(ch2);

                    // Set a differentCaseCompared if the characters being compared are different case.
                    // Should be done only once, hence the check with 0
                    if (differentCaseCompared == 0) {
                        differentCaseCompared = ch1 - ch2;
                    }
                }

                iteratorOfString1++;
                iteratorOfString2++;
            }
        }
    }

    private boolean ignoreCharacterCaseEquals(char character1, char character2) {

        return Character.toLowerCase(character1) == Character.toLowerCase(character2);
    }

    private int findEndPositionOfNumber(String string, int lengthOfString, int end) {

        while (end < lengthOfString && Character.isDigit(string.charAt(end)))
            end++;

        return end;
    }

    private int skipLeadingZeroes(String string, int lengthOfString, int iteratorOfString) {

        while (iteratorOfString < lengthOfString && string.charAt(iteratorOfString) == '0')
            iteratorOfString++;

        return iteratorOfString;
    }
}

以下是我使用的单元测试。


public class NaturalSortingComparatorTest {

    private int NUMBER_OF_TEST_CASES = 100000;

    @Test
    public void compare() {

        NaturalSortingComparator naturalSortingComparator = new NaturalSortingComparator();

        List<String> expectedStringList = getCorrectStringList();
        List<String> testListOfStrings = createTestListOfStrings();
        runTestCases(expectedStringList, testListOfStrings, NUMBER_OF_TEST_CASES, naturalSortingComparator);

    }

    private void runTestCases(List<String> expectedStringList, List<String> testListOfStrings,
                              int numberOfTestCases, Comparator<String> comparator) {

        for (int testCase = 0; testCase < numberOfTestCases; testCase++) {
            Collections.shuffle(testListOfStrings);
            testListOfStrings.sort(comparator);
            Assert.assertEquals(expectedStringList, testListOfStrings);
        }
    }

    private List<String> getCorrectStringList() {
        return Arrays.asList(
                "1", "01", "001", "2", "02", "10", "10", "010",
                "20", "100", "_1", "_01", "_2", "_200", "A 02",
                "A01", "a2", "A20", "t1A", "t1a", "t1AB", "t1Ab",
                "t1aB", "t1ab", "T010T01", "T0010T01");
    }

    private List<String> createTestListOfStrings() {
        return Arrays.asList(
                "10", "20", "A20", "2", "t1ab", "01", "T010T01", "t1aB",
                "_2", "001", "_200", "1", "A 02", "t1Ab", "a2", "_1", "t1A", "_01",
                "100", "02", "T0010T01", "t1AB", "10", "A01", "010", "t1a");
    }

}

欢迎提出建议!我不确定添加这些功能是否会改变除了可读性部分之外的任何东西。

PS:很抱歉为这个问题添加另一个答案。但是我没有足够的代表来评论我为我使用而修改的答案。

于 2020-03-08T14:57:27.960 回答
0

我认为您必须逐个字符地进行比较。抓取一个字符,如果是数字字符,继续抓取,然后将字符重新组合成单​​个数字字符串并将其转换为int. 在另一个字符串上重复,然后才进行比较。

于 2008-09-19T19:15:28.663 回答
0

简短回答:根据上下文,我无法判断这只是一些供个人使用的快速而肮脏的代码,还是高盛最新内部会计软件的关键部分,所以我会说: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
于 2008-09-19T19:45:38.390 回答
0

尽管问题询问了 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
   })

}
于 2016-05-03T12:11:19.123 回答
0

我的问题是我的列表由字母数字字符串(例如 C22、C3、C5 等)、字母字符串(例如 A、H、R 等)和仅需要排序的数字(例如 99、45 等)组成顺序 A、C3、C5、C22、H、R、45、99。我也有需要删除的重复项,所以我只得到一个条目。

我也不只是使用字符串,我正在订购一个对象并使用对象中的特定字段来获得正确的顺序。

似乎对我有用的解决方案是:

SortedSet<Code> codeSet;
codeSet = new TreeSet<Code>(new Comparator<Code>() {

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 上找到的一些代码以及我自己的一些调整,以使其按照我需要的方式工作。

由于尝试订购对象,需要比较器以及重复删除,我不得不采用的一个消极软糖是我首先必须将我的对象写入 TreeMap,然后再将它们写入 Treeset。它可能会稍微影响性能,但鉴于列表最多约为 80 个代码,这应该不是问题。

于 2018-11-09T15:38:32.430 回答
0

我有一个类似的问题,我的字符串里面有空格分隔的段。我以这种方式解决了它:

public class StringWithNumberComparator implements Comparator<MyClass> {

@Override
public int compare(MyClass o1, MyClass o2) {
    if (o1.getStringToCompare().equals(o2.getStringToCompare())) {
        return 0;
    }
    String[] first = o1.getStringToCompare().split(" ");
    String[] second = o2.getStringToCompare().split(" ");
    if (first.length == second.length) {
        for (int i = 0; i < first.length; i++) {

            int segmentCompare = StringUtils.compare(first[i], second[i]);
            if (StringUtils.isNumeric(first[i]) && StringUtils.isNumeric(second[i])) {

                segmentCompare = NumberUtils.compare(Integer.valueOf(first[i]), Integer.valueOf(second[i]));
                if (0 != segmentCompare) {
                    // return only if uneven numbers in case there are more segments to be checked
                    return segmentCompare;
                }
            }
            if (0 != segmentCompare) {
                return segmentCompare;
            }
        }
    } else {
        return StringUtils.compare(o1.getDenominazione(), o2.getDenominazione());
    }

    return 0;
}

如您所见,我使用了 Apaches StringUtils.compare() 和 NumberUtils.compere() 作为标准帮助。

于 2019-06-27T16:41:09.987 回答
0

修改这个答案


  • 不区分大小写的顺序(1000a 小于 1000X)
  • 空值处理

执行:

import static java.lang.Math.pow;

import java.util.Comparator;

public class AlphanumComparator implements Comparator<String> {
    
    public static final AlphanumComparator ALPHANUM_COMPARATOR = new AlphanumComparator();
    private static char[] upperCaseCache = new char[(int) pow(2, 16)];
    private boolean nullIsLess;
    
    public AlphanumComparator() {
    }
    
    public AlphanumComparator(boolean nullIsLess) {
        this.nullIsLess = nullIsLess;
    }
    
    @Override
    public int compare(String s1, String s2) {
        if (s1 == s2)
            return 0;
        if (s1 == null)
            return nullIsLess ? -1 : 1;
        if (s2 == null)
            return nullIsLess ? 1 : -1;
        
        int i1 = 0;
        int i2 = 0;
        int len1 = s1.length();
        int len2 = s2.length();
        while (true) {
            // handle the case when one string is longer than another
            if (i1 == len1)
                return i2 == len2 ? 0 : -1;
            if (i2 == len2)
                return 1;
            
            char ch1 = s1.charAt(i1);
            char ch2 = s2.charAt(i2);
            if (isDigit(ch1) && isDigit(ch2)) {
                // skip leading zeros
                while (i1 < len1 && s1.charAt(i1) == '0')
                    i1++;
                while (i2 < len2 && s2.charAt(i2) == '0')
                    i2++;
                
                // find the ends of the numbers
                int end1 = i1;
                int end2 = i2;
                while (end1 < len1 && isDigit(s1.charAt(end1)))
                    end1++;
                while (end2 != len2 && isDigit(s2.charAt(end2)))
                    end2++;
                
                // if the lengths are different, then the longer number is bigger
                int diglen1 = end1 - i1;
                int diglen2 = end2 - i2;
                if (diglen1 != diglen2)
                    return diglen1 - diglen2;
                
                // compare numbers digit by digit
                while (i1 < end1) {
                    ch1 = s1.charAt(i1);
                    ch2 = s2.charAt(i2);
                    if (ch1 != ch2)
                        return ch1 - ch2;
                    i1++;
                    i2++;
                }
            } else {
                ch1 = toUpperCase(ch1);
                ch2 = toUpperCase(ch2);
                if (ch1 != ch2)
                    return ch1 - ch2;
                i1++;
                i2++;
            }
        }
    }
    
    private boolean isDigit(char ch) {
        return ch >= 48 && ch <= 57;
    }
    
    private char toUpperCase(char ch) {
        char cached = upperCaseCache[ch];
        if (cached == 0) {
            cached = Character.toUpperCase(ch);
            upperCaseCache[ch] = cached;
        }
        return cached;
    }
}
于 2021-03-19T00:31:26.453 回答
-1

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

bbb 12 cc

对比

eee 12 ddd jpeg2000 eee

于 2008-09-19T19:09:27.557 回答
-1

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

于 2008-09-19T19:27:49.447 回答
-1

在 Linux 上 glibc 提供了 strverscmp(),它也可以从 gnulib 获得以实现可移植性。然而,真正的“人类”排序还有很多其他的怪癖,比如“披头士”被排序为“披头士”。这个通用问题没有简单的解决方案。

于 2008-09-19T19:30:29.143 回答