【问题标题】:Sort on a string that may contain a number对可能包含数字的字符串进行排序
【发布时间】:2008-09-19 19:05:21
【问题描述】:

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

  • aaa
  • bbb 3 cc
  • bbb 12 立方厘米
  • ccc 11
  • ddd
  • eee 3 ddd jpeg2000 eee
  • eee 12 ddd jpeg2000 eee

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

有没有更好的办法?

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

【问题讨论】:

    标签: java algorithm string sorting comparison


    【解决方案1】:

    The Alphanum Algorithm

    来自网站

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

    编辑:这是来自该站点的Java Comparator Implementation 的链接。

    【讨论】:

    • 这并不能完全解决问题 - 您需要对要排序的字符串进行标记,并在每个片段上单独使用此算法进行排序。
    • 注意:Paul 接受了您的答案,但我的算法更贴近他的问题(它解释它的方式!),例如“Allegia 51B Clasteron”。没问题,他选择适合他需要的任何东西,这个 Alphanum 实现很好(而且多语言!),我只是想指出这一点。 :-P
    • 此实现处理 OP 的特定示例输入,但对于一般用途,请注意它无法处理具有前导零的数字。它认为“01234”大于“5678”。
    • 我对前导零的排序做了一些更改:pastebin.com/tbEYj2zf
    【解决方案2】:

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

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

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

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

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

    【讨论】:

    • 不错的一个!额外的 null 和 instanceof 字符串检查也会很好
    • @HRgiger 你有一点关于空检查,我认为数组是“理智的”。但是今天,我将放弃 Java 1.5 之前的语法并使用泛型,而不是 instanceof。
    • 对“1000X Radius Maximus”和“10X Radius”给出错误的结果
    • 转载java.lang.IllegalArgumentException:比较方法违反了它的一般约定!
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      我在这里提出的实现简单而高效。它不直接或间接通过使用正则表达式或方法(如 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);
      }
      

      【讨论】:

      • 我喜欢它,因为它可读。我建议将for 循环改为while 循环,如下所示:while ((x1 &lt; len1) &amp;&amp; Character.isDigit(s1.charAt(x1))) { x1++;}
      • @Michael,你能解释一下为什么你认为它更好吗?对我来说完全一样.....
      • 通过添加本地静态最终方法 isDigit() 而不是使用 Character.isDigit(),我取得了显着的性能改进。我想这有利于在编译时进行内联代码扩展。
      【解决方案5】:

      我使用正则表达式在 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]

      【讨论】:

      • 优秀!!非常感谢
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

          这是一个比字母算法具有以下优势的解决方案:

          1. 快 3.25 倍(根据 Alphanum description 的“尾声”一章的数据进行测试)
          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++;
                      }
                  }
              }
          }
          

          【讨论】:

          • 很棒的代码!我只会对char ch1 = Character.toUpperCase(s1.charAt(i1)); 不区分大小写,这样1000a 就会小于1000X
          【解决方案9】:

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

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

          【讨论】:

            【解决方案10】:

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

            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”来保留标记,但我无法让它与我正在使用的正则表达式一起工作。

            【讨论】:

            • 您可能希望缓存您的 Pattern.compile() 调用,因为它们的调用复杂度为 O(N log N)
            • 好建议。代码已更新。扫描器现在也使用“尝试资源”关闭。
            • 您可以简单地调用NUMBER_PATTERN.matcher(s),而不是处理Scanner,然后在返回的Matcher 上重复调用find。很棒的是,匹配器会告诉你每场比赛的开始和结束位置,让整个拆分操作变得微不足道。而且它不是需要 try(…) {…} 块的资源。
            • @Holger 有趣的想法。我会实施它并作为单独的答案。我会投你一票。
            • 我不知道它是否足够独特,值得另一个答案。毕竟,它仍然会做同样的事情。顺便说一句,最初的声明 if(str1 == null || str2 == null) { return 0; } 被破坏了,因为它暗示如果任何一个参数是 null,它将被报告为 equal 到另一个参数。但是当null 等于任何其他输入时,所有输入必须相等(传递性 规则)。最简单的解决方案是根本不支持null。否则,您将不得不使用 if(str1 == str2) return 0; if(str1 == null) return 1; if(str2 == null) return -1; 之类的东西。
            【解决方案11】:

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

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

            【讨论】:

              【解决方案12】:

              在发现这个线程之前,我在 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/

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

              【讨论】:

                【解决方案13】:

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

                【讨论】:

                  【解决方案14】:

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

                  【讨论】:

                    【解决方案15】:

                    添加到@stanislav 制作的answer。 我在使用提供的答案时遇到的一些问题是:

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

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

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

                    【讨论】:

                      【解决方案16】:

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

                      【讨论】:

                        【解决方案17】:

                        简短回答:根据上下文,我无法判断这是否只是一些供个人使用的快速而肮脏的代码,还是高盛最新内部会计软件的关键部分,所以我将打开说:eww。这是一个相当时髦的排序算法。如果可以的话,尝试使用一些不那么“曲折”的东西。

                        长答案:

                        在您的案例中立即想到的两个问题是性能和正确性。非正式地,确保它很快,并确保你的算法是total ordering

                        (当然,如果您排序的项目不超过 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
                        

                        【讨论】:

                          【解决方案18】:

                          虽然问题询问了 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
                             })
                          
                          }
                          

                          【讨论】:

                            【解决方案19】:

                            我的问题是我的列表由字母数字字符串(例如 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 个代码,这应该不是问题。

                            【讨论】:

                              【解决方案20】:

                              我遇到了类似的问题,我的字符串内部有空格分隔的段。我是这样解决的:

                              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.compare() 作为标准帮助。

                              【讨论】:

                                【解决方案21】:

                                修改this答案


                                • 不区分大小写的顺序(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;
                                    }
                                }
                                

                                【讨论】:

                                  【解决方案22】:

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

                                  bbb 12 ccc

                                  对比

                                  eee 12 ddd jpeg2000eee

                                  【讨论】:

                                    【解决方案23】:

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

                                    【讨论】:

                                      【解决方案24】:

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

                                      【讨论】:

                                        猜你喜欢
                                        • 1970-01-01
                                        • 2020-02-18
                                        • 2016-10-28
                                        • 2021-08-12
                                        • 2019-05-26
                                        • 2011-03-07
                                        • 1970-01-01
                                        • 2014-05-05
                                        相关资源
                                        最近更新 更多