84

String を比較する Java Comparator クラスを作成する必要がありますが、1 つひねりがあります。比較している 2 つの文字列が同じで、文字列の先頭と末尾が同じで、異なる中間部分が整数である場合、それらの整数の数値に基づいて比較します。たとえば、次の文字列が表示される順序で終了するようにします。

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

ご覧のとおり、文字列には他の整数が含まれている可能性があるため、正規表現を使用して整数を分割することはできません。最初から一致しないビットが見つかるまで文字列をたどり、最後から一致しないビットが見つかるまで歩いて、途中のビットを比較することを考えています。正規表現 "[0-9]+" であり、比較する場合は数値比較を行い、それ以外の場合は字句比較を行います。

より良い方法はありますか?

更新文字列内の他の数字、一致する可能性のある数字、それらの周りにスペースがないこと、または異なる数字にスペースがあることを保証できるとは思いません。

4

24 に答える 24

107

英数字アルゴリズム

ウェブサイトから

「人々はソフトウェアとは異なる数字で文字列を並べ替えます。ほとんどの並べ替えアルゴリズムはASCII値を比較するため、人間の論理と矛盾する順序が生成されます。これを修正する方法は次のとおりです。」

編集:これは、そのサイトからのJavaコンパレータ実装へのリンクです。

于 2008-09-19T19:17:14.230 に答える
12

興味深い小さな挑戦、私はそれを解決することを楽しんだ。

ここに問題に対する私の見解があります:

String[] strs =
{
  "eee 5 ddd jpeg2001 eee",
  "eee 123 ddd jpeg2000 eee",
  "ddd",
  "aaa 5 yy 6",
  "ccc 555",
  "bbb 3 ccc",
  "bbb 9 a",
  "",
  "eee 4 ddd jpeg2001 eee",
  "ccc 11",
  "bbb 12 ccc",
  "aaa 5 yy 22",
  "aaa",
  "eee 3 ddd jpeg2000 eee",
  "ccc 5",
};

Pattern splitter = Pattern.compile("(\\d+|\\D+)");

public class InternalNumberComparator implements Comparator
{
  public int compare(Object o1, Object o2)
  {
    // I deliberately use the Java 1.4 syntax, 
    // all this can be improved with 1.5's generics
    String s1 = (String)o1, s2 = (String)o2;
    // We split each string as runs of number/non-number strings
    ArrayList sa1 = split(s1);
    ArrayList sa2 = split(s2);
    // Nothing or different structure
    if (sa1.size() == 0 || sa1.size() != sa2.size())
    {
      // Just compare the original strings
      return s1.compareTo(s2);
    }
    int i = 0;
    String si1 = "";
    String si2 = "";
    // Compare beginning of string
    for (; i < sa1.size(); i++)
    {
      si1 = (String)sa1.get(i);
      si2 = (String)sa2.get(i);
      if (!si1.equals(si2))
        break;  // Until we find a difference
    }
    // No difference found?
    if (i == sa1.size())
      return 0; // Same strings!

    // Try to convert the different run of characters to number
    int val1, val2;
    try
    {
      val1 = Integer.parseInt(si1);
      val2 = Integer.parseInt(si2);
    }
    catch (NumberFormatException e)
    {
      return s1.compareTo(s2);  // Strings differ on a non-number
    }

    // Compare remainder of string
    for (i++; i < sa1.size(); i++)
    {
      si1 = (String)sa1.get(i);
      si2 = (String)sa2.get(i);
      if (!si1.equals(si2))
      {
        return s1.compareTo(s2);  // Strings differ
      }
    }

    // Here, the strings differ only on a number
    return val1 < val2 ? -1 : 1;
  }

  ArrayList split(String s)
  {
    ArrayList r = new ArrayList();
    Matcher matcher = splitter.matcher(s);
    while (matcher.find())
    {
      String m = matcher.group(1);
      r.add(m);
    }
    return r;
  }
}

Arrays.sort(strs, new InternalNumberComparator());

このアルゴリズムにはさらに多くのテストが必要ですが、かなりうまく動作しているようです。

[編集] より明確にするために、さらにいくつかのコメントを追加しました。これをコーディングし始めたときよりもはるかに多くの答えがあることがわかります...しかし、良い出発点および/またはいくつかのアイデアを提供したことを願っています.

于 2008-09-19T21:12:42.050 に答える
9

Microsoft の Ian Griffiths は、彼がNatural Sortingと呼ぶ C# 実装を持っています。Java への移植は、C から移植するよりもかなり簡単です。

更新:これを行う eekboomに Java の例があるようです。「compareNatural」を参照して、並べ替えの比較子として使用してください。

于 2008-09-19T19:23:38.067 に答える
7

ここで提案する実装​​はシンプルで効率的です。substring()、split()、toCharArray() などの正規表現またはメソッドを使用して、直接的または間接的に追加のメモリを割り当てることはありません。

この実装では、最初に両方の文字列を調べて、異なる最初の文字を最大速度で検索します。その間、特別な処理は一切行いません。特定の数値比較は、これらの文字が両方とも数字である場合にのみトリガーされます。この実装の副作用は、デフォルトの辞書順とは反対に、数字が他の文字よりも大きいと見なされることです。

public static final int compareNatural (String s1, String s2)
{
   // Skip all identical characters
   int len1 = s1.length();
   int len2 = s2.length();
   int i;
   char c1, c2;
   for (i = 0, c1 = 0, c2 = 0; (i < len1) && (i < len2) && (c1 = s1.charAt(i)) == (c2 = s2.charAt(i)); i++);

   // Check end of string
   if (c1 == c2)
      return(len1 - len2);

   // Check digit in first string
   if (Character.isDigit(c1))
   {
      // Check digit only in first string 
      if (!Character.isDigit(c2))
         return(1);

      // Scan all integer digits
      int x1, x2;
      for (x1 = i + 1; (x1 < len1) && Character.isDigit(s1.charAt(x1)); x1++);
      for (x2 = i + 1; (x2 < len2) && Character.isDigit(s2.charAt(x2)); x2++);

      // Longer integer wins, first digit otherwise
      return(x2 == x1 ? c1 - c2 : x1 - x2);
   }

   // Check digit only in second string
   if (Character.isDigit(c2))
      return(-1);

   // No digits
   return(c1 - c2);
}
于 2014-12-17T16:51:01.960 に答える
5

あなたはJavaを使用していると思いますが、StrCmpLogicalWがどのように機能するかを見ることができます。これは、エクスプローラーがWindowsでファイル名を並べ替えるために使用するものです。ここでWINEの実装を見ることができます。

于 2008-09-19T19:18:11.137 に答える
4

文字列を文字と数字の連続に分割して、「foo 12 bar」がリスト(「foo」、12、「bar」)になり、リストをソートキーとして使用します。このように、番号はアルファベット順ではなく、番号順に並べられます。

于 2008-09-19T19:07:53.503 に答える
4

Alphanumアルゴリズムよりも次の利点があるソリューションを次に示します。

  1. 3.25 倍高速 ( Alphanum の説明の「エピローグ」の章のデータでテスト)
  2. 余分なメモリを消費しません (文字列の分割や数値の解析はありません)
  3. 先頭のゼロを正しく処理します (例: "0001"equals "1""01234"is less than "4567")
public class NumberAwareComparator implements Comparator<String>
{
    @Override
    public int compare(String s1, String s2)
    {
        int len1 = s1.length();
        int len2 = s2.length();
        int i1 = 0;
        int i2 = 0;
        while (true)
        {
            // handle the case when one string is longer than another
            if (i1 == len1)
                return i2 == len2 ? 0 : -1;
            if (i2 == len2)
                return 1;

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

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

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

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

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

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() メソッドを使用し、「先読み/後読み」を使用してトークンを保持したかったのですが、使用していた正規表現で動作させることができませんでした。

于 2011-05-19T23:57:59.650 に答える
1

興味深い問題、そしてここで私が提案した解決策:

import java.util.Collections;
import java.util.Vector;

public class CompareToken implements Comparable<CompareToken>
{
    int valN;
    String valS;
    String repr;

    public String toString() {
    return repr;
    }

    public CompareToken(String s) {
    int l = 0;
    char data[] = new char[s.length()];
    repr = s;
    valN = 0;
    for (char c : s.toCharArray()) {
        if(Character.isDigit(c))
        valN = valN * 10 + (c - '0');
        else
        data[l++] = c;
    }

    valS = new String(data, 0, l);
    }

    public int compareTo(CompareToken b) {
    int r = valS.compareTo(b.valS);
    if (r != 0)
        return r;

    return valN - b.valN;
    }


    public static void main(String [] args) {
    String [] strings = {
        "aaa",
        "bbb3ccc",
        "bbb12ccc",
        "ccc 11",
        "ddd",
        "eee3dddjpeg2000eee",
        "eee12dddjpeg2000eee"
    };

    Vector<CompareToken> data = new Vector<CompareToken>();
    for(String s : strings)
        data.add(new CompareToken(s));
    Collections.shuffle(data);

    Collections.sort(data);
    for (CompareToken c : data)
        System.out.println ("" + c);
    }

}
于 2012-07-21T22:13:02.250 に答える
1

私の2セント。私にとってはうまくいっています。主にファイル名に使用しています。

    private final boolean isDigit(char ch)
        {
            return ch >= 48 && ch <= 57;
        }


        private int compareNumericalString(String s1,String s2){

            int s1Counter=0;
            int s2Counter=0;
            while(true){
                if(s1Counter>=s1.length()){
                    break;
                }
                if(s2Counter>=s2.length()){
                    break;
                }
                char currentChar1=s1.charAt(s1Counter++);
                char currentChar2=s2.charAt(s2Counter++);
                if(isDigit(currentChar1) &&isDigit(currentChar2)){
                    String digitString1=""+currentChar1;
                    String digitString2=""+currentChar2;
                    while(true){
                        if(s1Counter>=s1.length()){
                            break;
                        }
                        if(s2Counter>=s2.length()){
                            break;
                        }

                        if(isDigit(s1.charAt(s1Counter))){
                            digitString1+=s1.charAt(s1Counter);
                            s1Counter++;
                        }

                        if(isDigit(s2.charAt(s2Counter))){
                            digitString2+=s2.charAt(s2Counter);
                            s2Counter++;
                        }

                        if((!isDigit(s1.charAt(s1Counter))) && (!isDigit(s2.charAt(s2Counter)))){
                            currentChar1=s1.charAt(s1Counter);
                            currentChar2=s2.charAt(s2Counter);
                            break;
                        }
                    }
                    if(!digitString1.equals(digitString2)){
                        return Integer.parseInt(digitString1)-Integer.parseInt(digitString2);
                    }
                }

                if(currentChar1!=currentChar2){
                    return currentChar1-currentChar2;
                }

            }
            return s1.compareTo(s2);
        }
于 2015-06-07T14:53:23.183 に答える
0

キャラクターごとに比較する必要があると思います。文字をつかみ、それが数字の場合はつかみ続け、次に文字を単一の数字文字列に再組み立てし、それをint. 他の文字列で繰り返してから、比較を行います。

于 2008-09-19T19:15:28.663 に答える
0

簡単な答え: 文脈に基づいて、これが個人的な使用のための簡単なコードなのか、それとも Goldman Sachs の最新の内部会計ソフトウェアの重要な部分なのかはわかりません。 . これはかなり変わったソート アルゴリズムです。可能であれば、少し「ねじれ」の少ないものを使用してみてください。

長い答え:

あなたのケースですぐに頭に浮かぶ2つの問題は、パフォーマンスと正確さです。非公式に、それが高速であることを確認し、アルゴリズムが全順序付けであることを確認してください。

(もちろん、約 100 個を超えるアイテムを並べ替えない場合は、おそらくこの段落を無視してもかまいません。) パフォーマンスは重要です。コンパレータの速度が並べ替えの速度の最大の要因となるためです (並べ替えアルゴリズムが典型的なリストに「理想的」)。あなたの場合、コンパレータの速度は主に文字列のサイズに依存します。文字列はかなり短いように見えるので、おそらくリストのサイズほど大きくはありません。

別の回答で提案されているように、各文字列を文字列-数字-文字列のタプルに変換してから、このタプルのリストを並べ替えると、複数の数字が表示される文字列が表示されるため、一部のケースでは失敗します。

もう一つの問題は正確さです。具体的には、説明したアルゴリズムが A > B > ... > A を許可する場合、ソートは非決定論的になります。あなたの場合、私はそれを証明することはできませんが、そうかもしれないと恐れています. 次のようないくつかの解析ケースを検討してください。

  aa 0 aa
  aa 23aa
  aa 2a3aa
  aa 113aa
  aa 113 aa
  a 1-2 a
  a 13 a
  a 12 a
  a 2-3 a
  a 21 a
  a 2.3 a
于 2008-09-19T19:45:38.390 に答える
0

質問はJavaソリューションを求めましたが、scalaソリューションが必要な人のために:

object Alphanum {

   private[this] val regex = "((?<=[0-9])(?=[^0-9]))|((?<=[^0-9])(?=[0-9]))"

   private[this] val alphaNum: Ordering[String] = Ordering.fromLessThan((ss1: String, ss2: String) => (ss1, ss2) match {
     case (sss1, sss2) if sss1.matches("[0-9]+") && sss2.matches("[0-9]+") => sss1.toLong < sss2.toLong
     case (sss1, sss2) => sss1 < sss2
   })

   def ordering: Ordering[String] = Ordering.fromLessThan((s1: String, s2: String) => {
     import Ordering.Implicits.infixOrderingOps
     implicit val ord: Ordering[List[String]] = Ordering.Implicits.seqDerivedOrdering(alphaNum)

     s1.split(regex).toList < s2.split(regex).toList
   })

}
于 2016-05-03T12:11:19.123 に答える
-1

Linuxでは、glibcはstrverscmp()を提供しますが、移植性のためにgnulibからも入手できます。ただし、本当に「人間」の並べ替えには、「ビートルズ」が「ビートルズ、ザ」として並べ替えられるなど、他にも多くの癖があります。この一般的な問題に対する簡単な解決策はありません。

于 2008-09-19T19:30:29.143 に答える
-1

あなたの与えられた例では、あなたが比較したい数はそれらの周りにスペースがありますが、他の数はそうではありません、それでなぜ正規表現は機能しないのでしょうか?

bbb 12 ccc

対。

eee 12 ddd jpeg2000 eee

于 2008-09-19T19:09:27.557 に答える
-1

コンパレータ クラスを作成している場合は、2 つの文字列を 1 文字ずつ比較する独自の比較メソッドを実装する必要があります。この比較メソッドは、アルファベット文字、数字、または混合型 (スペースを含む) を扱っているかどうかを確認する必要があります。混合型をどのように動作させたいか、数字がアルファベット文字の前に来るか後に来るか、スペースがどこに収まるかなどを定義する必要があります.

于 2008-09-19T19:27:49.447 に答える