web-dev-qa-db-de.com

Sortiert nach einer Zeichenfolge, die möglicherweise eine Zahl enthält

Ich muss eine Java Comparator-Klasse schreiben, die Strings vergleicht, jedoch mit einer Wendung. Wenn die beiden verglichenen Zeichenfolgen am Anfang und am Ende der Zeichenfolge gleich sind und der mittlere Teil, der sich unterscheidet, eine ganze Zahl ist, dann vergleichen Sie anhand der numerischen Werte dieser ganzen Zahlen. Zum Beispiel möchte ich, dass die folgenden Zeichenfolgen in der angegebenen Reihenfolge enden:

  • aaa
  • bbb 3 ccc
  • bbb 12 ccc
  • ccc 11
  • ddd
  • eee 3 ddd jpeg2000 eee
  • eee 12 ddd jpeg2000 eee

Wie Sie sehen, gibt es möglicherweise andere Ganzzahlen in der Zeichenfolge. Ich kann also nicht einfach reguläre Ausdrücke verwenden, um eine ganze Zahl auszubrechen. Ich denke, ich gehe einfach die Saiten von Anfang an durch, bis ich ein Stück finde, das nicht passt, und dann vom Ende bis zu einem Stück, das nicht passt, und dann das Bit in der Mitte mit dem vergleichen regulärer Ausdruck "[0-9] +" und wenn es vergleicht, dann einen numerischen Vergleich durchführen, andernfalls einen lexikalischen Vergleich durchführen.

Gibt es einen besseren Weg?

Update Ich glaube nicht, dass ich garantieren kann, dass die anderen Zahlen in der Zeichenfolge, die möglicherweise übereinstimmen, keine Leerzeichen enthalten, oder dass die anderen Zahlen Leerzeichen enthalten.

71
Paul Tomblin

Der Alphanum-Algorithmus

Von der Website

"Menschen sortieren Strings mit Zahlen anders als mit Software. Die meisten Sortieralgorithmen vergleichen die Werte von ASCII, wodurch eine Reihenfolge erzeugt wird, die nicht mit der menschlichen Logik übereinstimmt. So beheben Sie das Problem."

Bearbeiten: Hier ist ein Link zur Java Comparator-Implementierung von dieser Site.

96
ScArcher2

Interessante kleine Herausforderung, ich habe es genossen, es zu lösen.

Hier ist mein Blick auf das Problem:

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

Dieser Algorithmus erfordert viel mehr Tests, aber er scheint sich ziemlich gut zu benehmen.

[EDIT] Ich habe einige Anmerkungen hinzugefügt, um klarer zu sein. Ich sehe, es gibt viel mehr Antworten als zu Beginn des Codierens ... Aber ich hoffe, ich habe eine gute Ausgangsbasis und/oder einige Ideen bereitgestellt.

12
PhiLho

Ian Griffiths von Microsoft hat eine C # -Implementierung, die er Natural Sorting nennt. Die Portierung nach Java sollte ohnehin relativ einfach sein.

UPDATE: Es scheint ein Java-Beispiel auf eekboom zu geben, das dies tut. Siehe "compareNatural" und verwende es als Vergleicher für Sortierungen.

8
Ray Hayes

Ich weiß, dass Sie Java verwenden, aber Sie können einen Blick darauf werfen, wie StrCmpLogicalW funktioniert. Es ist das, was Explorer verwendet, um Dateinamen in Windows zu sortieren. Sie können die WEIN-Implementierung hier anschauen.

5
Eclipse

Die hier vorgeschlagene Implementierung ist einfach und effizient. Es reserviert keinen zusätzlichen Speicher direkt oder indirekt mithilfe von regulären Ausdrücken oder Methoden wie substring (), split (), toCharArray () usw. 

Bei dieser Implementierung werden zuerst beide Zeichenfolgen durchsucht, um nach ersten Zeichen zu suchen, die sich mit maximaler Geschwindigkeit unterscheiden, ohne dabei eine besondere Verarbeitung durchzuführen. Der spezifische Zahlenvergleich wird nur ausgelöst, wenn diese Zeichen beide Ziffern sind. Ein Nebeneffekt dieser Implementierung ist, dass eine Ziffer im Gegensatz zur standardmäßigen lexikographischen Reihenfolge als größer als andere Buchstaben betrachtet wird.

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);
}
5
Olivier OUDOT

Teilen Sie die Zeichenfolge in Buchstaben und Zahlen auf, so dass "foo 12 bar" zur Liste wird ("foo", 12, "bar"), und verwenden Sie dann die Liste als Sortierschlüssel. Auf diese Weise werden die Zahlen in numerischer Reihenfolge und nicht in alphabetischer Reihenfolge angeordnet.

4
John Millikin

Ich fand eine recht einfache Implementierung in Java mit regulären Ausdrücken:

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

So funktioniert es:

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]

3
Helder Pereira

Das Alphanum algrothim ist Nice, aber es entsprach nicht den Anforderungen für ein Projekt, an dem ich gerade arbeite. Ich muss negative Zahlen und Dezimalzahlen korrekt sortieren können. Hier ist die Implementierung, die ich mir vorgestellt habe. Über Feedback würden wir uns sehr freuen.

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. Ich wollte die Java.lang.String.split () -Methode verwenden und "lookahead/lookbehind" verwenden, um die Token beizubehalten, aber ich konnte nicht mit dem regulären Ausdruck arbeiten, den ich verwendete.

2
JustinKSU

Meine 2 Cent. Funktioniert gut für mich. Ich verwende es hauptsächlich für Dateinamen.

    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
specialscope

interessantes Problem, und hier meine vorgeschlagene Lösung:

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

}
1

Bevor ich diesen Thread entdeckte, implementierte ich eine ähnliche Lösung in Javascript. Vielleicht wird meine Strategie Sie trotz unterschiedlicher Syntax gut finden. Ähnlich wie oben analysiere ich die beiden verglichenen Zeichenfolgen und teile sie in Arrays auf, wobei die Zeichenfolgen in fortlaufenden Zahlen unterteilt werden. 

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

D.h. "hallo22goodbye 33" => ["hallo", 22, "auf wiedersehen", 33]; So können Sie die Elemente der Arrays paarweise zwischen string1 und string2 durchgehen, eine Typübernahme vornehmen (z. B. ist dieses Element wirklich eine Zahl?) Und während des Gehens vergleichen.

Arbeitsbeispiel hier: http://jsfiddle.net/F46s6/3/

Beachten Sie, dass ich derzeit nur Ganzzahltypen unterstütze, obwohl die Handhabung von Dezimalwerten einer Änderung nicht zu schwer wäre.

1
cdaringe

Obwohl die Frage eine Java-Lösung stellte, für alle, die eine Scala-Lösung wünschen:

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

}
0
Bennie Krijger

Mein Problem war, dass ich Listen habe, die aus einer Kombination von alphanumerischen Zeichenketten (z. B. C22, C3, C5 usw.), Alpha-Zeichenketten (z. B. A, H, R usw.) und nur Ziffern (z. B. 99, 45 usw.) bestehen, die eingeteilt werden müssen die Reihenfolge A, C3, C5, C22, H, R, 45, 99. Ich habe auch Duplikate, die entfernt werden müssen, also bekomme ich nur einen einzigen Eintrag. 

Ich arbeite auch nicht nur mit Strings, ich bestelle ein Objekt und benutze ein bestimmtes Feld innerhalb des Objekts, um die richtige Reihenfolge zu erhalten.

Eine Lösung, die für mich zu funktionieren scheint, ist:

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

Es „borgt“ etwas Code, den ich hier auf Stackoverflow gefunden habe, und einige eigene Verbesserungen, damit er so funktioniert, wie ich es auch brauchte.

Aufgrund des Versuchs, Objekte zu ordnen, einen Vergleicher zu benötigen und ein Duplikat zu entfernen, musste ich zunächst einmal meine Objekte in eine TreeMap schreiben, bevor sie in ein Treeset geschrieben wurden. Dies kann die Leistung etwas beeinträchtigen, aber da die Listen maximal 80 Codes umfassen, sollte dies kein Problem sein.

0
mavisto

Ich hatte ein ähnliches Problem, bei dem meine Saiten durch Leerzeichen getrennte Segmente enthielten. Ich habe es so gelöst:

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

Wie Sie sehen, habe ich Apaches StringUtils.compare () und NumberUtils.compere () als Standardhilfe verwendet.

0
Sasa

Kurze Antwort: Aufgrund des Kontextes kann ich nicht sagen, ob es sich nur um einen schnellen Code für den persönlichen Gebrauch oder um einen wichtigen Teil der neuesten internen Buchhaltungssoftware von Goldman Sachs handelt. Ich werde also sagen: eww . Das ist ein ziemlich funky Sortieralgorithmus. Versuchen Sie etwas zu verwenden, das etwas weniger "kurvig" ist, wenn Sie können.

Lange Antwort:

Die zwei Probleme, die Ihnen in Ihrem Fall sofort einfallen, sind Leistung und Korrektheit. Informell, stellen Sie sicher, dass es schnell ist, und stellen Sie sicher, dass Ihr Algorithmus eine Gesamtbestellung ist.

(Wenn Sie nicht mehr als 100 Artikel sortieren, können Sie diesen Absatz natürlich außer Acht lassen.) Die Leistung ist wichtig, da die Geschwindigkeit des Komparators der größte Faktor für die Sortiergeschwindigkeit ist (vorausgesetzt, der Sortieralgorithmus ist dies) "ideal" zur typischen Liste). In Ihrem Fall hängt die Geschwindigkeit des Komparators hauptsächlich von der Größe der Zeichenfolge ab. Die Zeichenfolgen scheinen ziemlich kurz zu sein, so dass sie wahrscheinlich nicht so stark dominieren wie Ihre Liste.

Wenn Sie aus jedem String ein String-Number-String-Tupel machen und diese Liste von Tupeln sortieren, wie in einer anderen Antwort vorgeschlagen, schlägt dies in einigen Fällen fehl, da anscheinend Strings mit mehreren Nummern angezeigt werden.

Das andere Problem ist die Korrektheit. Wenn der von Ihnen beschriebene Algorithmus A> B> ...> A zulässt, ist Ihre Sortierung nicht deterministisch. In Ihrem Fall befürchte ich, dass dies der Fall sein könnte, obwohl ich es nicht beweisen kann. Betrachten Sie einige Analyse-Fälle wie:

  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
0
Paul Brinkley

Ich denke, Sie müssen den Vergleich auf Zeichenweise durchführen. Schnappen Sie sich ein Zeichen (wenn es sich um ein Zahlenzeichen handelt), greifen Sie es weiter auf, setzen Sie es dann zu Zeichen in eine einzige Ziffernfolge zusammen und konvertieren Sie es in eine int. Wiederholen Sie den Vorgang für die andere Zeichenfolge und führen Sie erst dann den Vergleich durch. 

0
sblundy