Сортировать по строке, которая может содержать число - PullRequest
71 голосов
/ 19 сентября 2008

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

  • ааа
  • BBB 3 CCC
  • bbb 12 ccc
  • куб. См 11
  • 1012 * ддд *
  • eee 3 ddd jpeg2000 eee
  • ее 12 ддп jpeg2000 ее

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

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

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

Ответы [ 18 ]

96 голосов
/ 19 сентября 2008

Алфавитный алгоритм

с сайта

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

Изменить: Вот ссылка на Реализация компаратора Java с этого сайта.

12 голосов
/ 20 сентября 2008

Интересный маленький вызов, мне понравилось его решать.

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

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

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

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

8 голосов
/ 19 сентября 2008

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

ОБНОВЛЕНИЕ: Кажется, что есть пример Java на eekboom , который делает это, смотрите «compareNatural» и используйте его как свой компаратор для сортировки.

5 голосов
/ 19 сентября 2008

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

5 голосов
/ 17 декабря 2014

Реализация, которую я предлагаю здесь, проста и эффективна. Он не выделяет никакой дополнительной памяти, прямо или косвенно, используя регулярные выражения или методы, такие как 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);
}
4 голосов
/ 19 сентября 2008

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

3 голосов
/ 19 июля 2017

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

2 голосов
/ 20 мая 2011

Algrothim 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» для сохранения токенов, но не смог заставить его работать с регулярным выражением, которое я использовал.

1 голос
/ 07 июня 2015

Мои 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);
        }
1 голос
/ 25 июля 2014

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...