Сортировка по строке, которая может содержать число

Мне нужно написать класс Java Comparator, который сравнивает строки, но с одним поворотом. Если две сравниваемые строки одинаковы, в начале и в конце строки одинаковы, а средняя часть, которая отличается, является целым числом, тогда сравните на основе числовых значений этих целых чисел. Например, я хочу, чтобы следующие строки располагались в том порядке, в котором они отображаются:

  • ааа
  • bbb 3 ccc
  • BBB 12 куб.см
  • ccc 11
  • ддд
  • eee 3 ddd jpeg2000 eee
  • eee 12 ddd jpeg2000 eee

Как видите, в строке могут быть другие целые числа, поэтому я не могу просто использовать регулярные выражения для выделения любого целого числа. Я думаю о том, чтобы просто пройтись по струнам с начала, пока не найду бит, который не совпадает, затем пройтись с конца, пока не найду бит, который не совпадает, а затем сравнить бит в середине с регулярное выражение "[0-9] +", и если оно сравнивается, то выполняется числовое сравнение, в противном случае - лексическое сравнение.

Есть ли способ лучше?

Обновлять Я не думаю, что могу гарантировать, что другие числа в строке, те, которые могут совпадать, не имеют пробелов вокруг них или что те, которые отличаются, имеют пробелы.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
75
0
87 111
23
Перейти к ответу Данный вопрос помечен как решенный

Ответы 23

Разделите строку на ряды букв и цифр, так что «foo 12 bar» станет списком («foo», 12, «bar»), затем используйте список в качестве ключа сортировки. Таким образом, номера будут упорядочены в числовом порядке, а не в алфавитном порядке.

В данном примере числа, которые вы хотите сравнить, имеют пробелы вокруг них, а другие числа - нет, так почему же регулярное выражение не работает?

bbb 12 ccc

против.

eee 12 ddd jpeg2000 eee

Я думаю, вам придется провести сравнение по персонажам. Возьмите символ, если это числовой символ, продолжайте захват, затем соберите символы в одну числовую строку и преобразуйте ее в int. Повторите то же самое с другой строкой и только после этого проведите сравнение.

Ответ принят как подходящий

Алгоритм Alphanum

С веб-сайта

«Люди сортируют строки с числами иначе, чем программное обеспечение. Большинство алгоритмов сортировки сравнивают значения ASCII, что приводит к упорядочению, несовместимому с человеческой логикой. Вот как это исправить».

Обновлено: вот ссылка на Реализация компаратора Java с этого сайта.

Это не полностью решает проблему - вам нужно будет токенизировать строку для сортировки и сортировки с использованием этого алгоритма для каждого фрагмента отдельно.

Nick Johnson 20.09.2008 00:06

Примечание: Пол принял ваш ответ, но мой алгоритм больше придерживается его проблемы (как он ее объяснил!) Для таких случаев, как «Allegia 51B Clasteron». Не проблема, он выбирает то, что подходит его потребностям, и эта реализация Alphanum прекрасна (и многоязычна!), Я просто хотел указать на это. :-П

PhiLho 25.10.2008 12:34

Эта реализация имеет дело с конкретными входными данными OP, но для общего использования имейте в виду, что она не справляется с числами, которые имеют ведущие нули. Он считает, что «01234» больше, чем «5678».

Klitos Kyriacou 25.01.2018 20:23

Я понимаю, что вы используете java, но вы можете посмотреть, как работает StrCmpLogicalW. Это то, что Explorer использует для сортировки имен файлов в Windows. Вы можете посмотреть на реализацию WINE здесь.

У Яна Гриффитса из Microsoft есть реализация C#, которую он называет Естественная сортировка. Перенос на Java должен быть довольно простым, в любом случае проще, чем с C!

Обновлено: Кажется, есть пример Java на eekboom, который делает это, посмотрите "compareNatural" и используйте его в качестве средства сравнения для сортировки.

Если вы пишете класс компаратора, вы должны реализовать свой собственный метод сравнения, который будет сравнивать две строки посимвольно. Этот метод сравнения должен проверять, имеете ли вы дело с буквенными символами, числовыми символами или смешанными типами (включая пробелы). Вам нужно будет определить, как должен действовать смешанный тип, будут ли числа стоять перед буквенными символами или после них, и где вписываются пробелы и т. д.

В Linux glibc предоставляет strverscmp (), она также доступна в gnulib для переносимости. Однако по-настоящему «человечная» сортировка имеет множество других причуд, например, «Битлз», отсортированные как «Битлз, The». У этой общей проблемы нет простого решения.

Краткий ответ: исходя из контекста, я не могу сказать, это просто какой-то быстрый и грязный код для личного использования или ключевая часть последней версии программного обеспечения для внутреннего учета Goldman Sachs, поэтому я начну, сказав: 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

Интересная маленькая задача, мне понравилось ее решать.

Вот мой взгляд на проблему:

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

Этот алгоритм требует гораздо большего тестирования, но, похоже, ведет себя довольно хорошо.

[РЕДАКТИРОВАТЬ] Я добавил еще несколько комментариев, чтобы было понятнее. Я вижу, что ответов гораздо больше, чем когда я начинал кодировать это ... Но я надеюсь, что я предоставил хорошую стартовую базу и / или некоторые идеи.

хороший! Также было бы неплохо провести дополнительную проверку null и instanceof String

HRgiger 10.05.2016 12:34

@HRgiger У вас есть точка зрения на нулевую проверку, я предположил, что массив "нормальный". Но сегодня я бы просто отказался от синтаксиса до Java 1.5 и использовал дженерики, а не instanceof.

PhiLho 11.05.2016 17:50

Алгротим Альфанум хорош, но не соответствует требованиям проекта, над которым я работаю. Мне нужно правильно сортировать отрицательные числа и десятичные дроби. Вот реализация, которую я придумал. Любая обратная связь будет очень признательна.

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)!

Lukas Eder 23.02.2018 14:42

Хорошее предложение. Код обновлен. Сканер теперь также закрывается с помощью «попробовать с ресурсами».

JustinKSU 23.02.2018 19:20

Вместо того, чтобы иметь дело с Scanner, вы можете просто вызвать NUMBER_PATTERN.matcher(s), а затем несколько раз вызвать find для возвращенного Matcher. Замечательно то, что сопоставитель сообщит вам начальную и конечную позицию для каждого совпадения, что делает всю операцию разделения тривиальной. И это не ресурс, требующий блока try(…) {…}.

Holger 29.11.2018 18:06

@Holger Интересная идея. Я бы это реализовал и поставил как отдельный ответ. Я поставлю тебе голос.

JustinKSU 29.11.2018 18:12

Я не знаю, достаточно ли он уникален, чтобы заслужить еще один ответ. В конце концов, он все равно будет делать то же самое. Между прочим, начальный оператор 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 29.11.2018 18:20

@Holger Я не согласен с тем, как обрабатывать нули stackoverflow.com/a/2401629/724835

JustinKSU 29.11.2018 21:47

Этот связанный ответ описывает последовательное поведение. Он даже соответствует второму из моих предложений, if (str1 == str2) return 0; if (str1 == null) return 1; if (str2 == null) return -1;. Код вашего ответа возвращает ноль во всех трех случаях, что несовместимо и нарушает договор Comparator. Это не соответствует поведению, описанному в связанном ответе.

Holger 29.11.2018 22:07

@Holger Я прав. Я неправильно понял ваше первое предложение. Пожалуйста, обновите мой ответ, чтобы лучше обрабатывать нули.

JustinKSU 29.11.2018 22:25

интересная проблема, и вот мое предлагаемое решение:

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

}

Прежде чем открыть для себя эту ветку, я реализовал аналогичное решение в javascript. Возможно, моя стратегия вам понравится, несмотря на другой синтаксис. Как и в предыдущем случае, я анализирую две сравниваемые строки и разделяю их на массивы, разделяя строки на непрерывные числа.

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

То есть, 'hello22goodbye 33' => ['привет', 22, 'до свидания', 33]; Таким образом, вы можете проходить по элементам массива парами между строкой1 и строкой2, выполнять какое-либо приведение типов (например, действительно ли этот элемент является числом?) И сравнивать по ходу.

Рабочий пример здесь: http://jsfiddle.net/F46s6/3/

Обратите внимание, что в настоящее время я поддерживаю только целочисленные типы, хотя обработка десятичных значений не будет слишком сложной задачей.

Предлагаемая здесь реализация проста и эффективна. Он не выделяет дополнительную память, прямо или косвенно с помощью регулярных выражений или методов, таких как 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 < len1) && Character.isDigit(s1.charAt(x1))) { x1++;}

Michael Böckling 18.06.2019 12:15

@ Майкл, не могли бы вы объяснить, почему вы думаете, что это лучше? Для меня это точно так же .....

Olivier OUDOT 09.08.2019 12:00

Я значительно улучшил производительность, добавив локальный статический конечный метод isDigit () вместо использования Character.isDigit (). Я полагаю, это способствует расширению встроенного кода во время компиляции.

Olivier OUDOT 09.08.2019 12:10

Мои 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);
        }

Хотя вопрос был задан 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
   })

}

Я придумал довольно простую реализацию на 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]

Моя проблема заключалась в том, что у меня есть списки, состоящие из комбинации буквенно-цифровых строк (например, 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 кодов, это не должно быть проблемой.

У меня была аналогичная проблема, когда в моих строках были сегменты, разделенные пробелами. Я решил это так:

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

Как видите, я использовал Apache StringUtils.compare () и NumberUtils.compere () в качестве стандартной справки.

Я создал проект для сравнения различных реализаций. Это далеко не полная версия, но это отправная точка.

Вот решение со следующими преимуществами перед алгоритмом Alphanum:

  1. В 3,25 раза быстрее (проверено на данных из главы «Эпилог» Описание алфавита)
  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++;
            }
        }
    }
}

Добавление к отвечать сделано @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");
    }

}

Предложения приветствуются! Я не уверен, меняет ли добавление функций что-нибудь, кроме читабельности.

P.S: Извините, что добавляю еще один ответ на этот вопрос. Но у меня недостаточно представителей, чтобы прокомментировать ответ, который я изменил для себя.

Вместо того, чтобы изобретать колесо, я бы предложил использовать совместимый с Юникодом компаратор строк, который имеет встроенную сортировку чисел из Библиотека ICU4J.

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

Другие вопросы по теме