java 匹配字符串排序_关于java:对可能包含数字的字符串进行排序

我需要编写一个比较比较字符串的Java比较器类,但是只有一个扭曲。如果要比较的两个字符串在字符串的开头和结尾处是相同的,而不同的中间部分是整数,则根据这些整数的数值进行比较。例如,我希望以下字符串按显示顺序结束:

美国农业协会

BBB 3 CCC

BBB 12 CCC

CCC 11

滴滴涕

EEE 3 DDD JPEG2000 EEE

EEE 12 DDD JPEG2000 EEE

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

有更好的方法吗?

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

alphanum算法

从网站上

"人们对数字字符串的排序与软件不同。大多数排序算法比较ASCII值,它产生的排序与人类逻辑不一致。以下是解决问题的方法。"

编辑:这里有一个指向该站点的Java比较器实现的链接。

这并不能完全解决这个问题——您需要对要排序的字符串进行标记,并使用该算法对每个片段进行单独排序。

注:保罗接受了你的答案,但我的算法更接近他的问题(解释的方式!),例如"Allegia 51B Cristeron"。没问题,他选择了适合自己需要的任何东西,这个alphanum实现很好(而且是多语言!)我只是想指出这一点。-P

请注意,它是GNU较低许可证

谢谢。为什么不把它推到gist.github.com?!

此实现处理操作的特定示例输入,但一般情况下,请注意,它无法处理前导零的数字。认为"01234"大于"5678"。

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

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

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

这个算法需要更多的测试,但它的性能似乎相当好。

[编辑]我添加了更多的评论以便更清楚。我看到有比我开始编写代码时更多的答案…但我希望我能提供一个良好的起点和/或一些想法。

好一个!另外一个空值和instanceof字符串检查也很好。

@hrgiger您有一个关于空检查的观点,我假设数组是"正常的"。但今天,我只会放弃前Java 1.5语法,使用泛型,而不是使用实例。

微软的Ian Griffiths有一个他称之为"自然排序"的C实现。无论如何,移植到Java应该相当容易,比C更容易!

更新:在EkkRoad上有一个Java示例,它可以做到这一点,看到"比较自然"并使用它作为比较器来排序。

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

}

我知道你在Java,但是你可以看看StrucPrLogiCW是如何工作的。这是资源管理器在Windows中用来排序文件名的方法。您可以在这里查看Wine实现。

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

我用正则表达式在Java中提出了一个非常简单的实现:

public static Comparator 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 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]

Alphanum Algrothim很不错,但它不符合我正在进行的项目的要求。我需要能够正确地对负数和小数进行排序。这是我提出的实现。任何反馈都将非常感谢。

public class StringAsNumberComparator implements Comparator {

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 split1 = split(str1);

List 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 split(String s) {

List list = new ArrayList();

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;

}

}

我想使用Java.Lang.Stulk.SultId()方法,使用"LooWald/LoopBeld'"来保留令牌,但我不能让它与我正在使用的正则表达式一起工作。

您可能需要缓存您的Pattern.compile()调用,因为它们是以O(N log N)复杂性调用的!

好建议。代码已更新。扫描仪现在也关闭使用"尝试资源"。

您可以直接调用NUMBER_PATTERN.matcher(s),然后在返回的Matcher上重复调用find,而不是处理Scanner。最棒的是,Matcher会告诉您每次匹配的开始和结束位置,使得整个拆分操作变得微不足道。它不是一个需要try(…) {…}块的资源。

@霍尔格有趣的想法。我将实现它并作为一个单独的答案。我给你投赞成票。

我不知道它是否独特,值得另一个答案。毕竟,它仍然会做同样的事情。顺便说一下,最初的声明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我不同意如何处理nulls stackoverflow.com/a/2401629/724835

链接的答案描述了一致的行为。它甚至符合我的第二个建议,if(str1 == str2) return 0; if(str1 == null) return 1; if(str2 == null) return -1;。在所有三种情况下,您的答案代码都返回零,这三种情况不一致并且违反了Comparator的合同。它与链接答案中描述的行为不匹配。

@霍尔格,我站得对。我误解了你的第一个建议。请更新我的答案以更好地处理空值。

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

}

在发现这个线程之前,我在JavaScript中实现了类似的解决方案。也许我的策略会很好地找到你,尽管语法不同。与上面类似,我解析了要比较的两个字符串,并将它们都拆分为数组,以连续的数字分隔字符串。

...

var regex = /(\d+)/g,

str1Components = str1.split(regex),

str2Components = str2.split(regex),

...

例如,"hello 22再见33"=>["hello",22,"再见",33];因此,您可以在string1和string2之间成对遍历数组元素,执行一些类型强制(例如,此元素真的是数字吗?)并在走路时进行比较。

这里的工作示例:http://jsfiddle.net/f46s6/3/

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

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

import java.util.Collections;

import java.util.Vector;

public class CompareToken implements Comparable

{

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 data = new Vector();

for(String s : strings)

data.add(new CompareToken(s));

Collections.shuffle(data);

Collections.sort(data);

for (CompareToken c : data)

System.out.println ("" + c);

}

}

我想你得把一个字一个字地比较一下。抓取一个字符,如果是数字字符,则继续抓取,然后重新组合成一个数字字符串,并将其转换为一个int。对另一个字符串重复,然后再进行比较。

简短的回答:根据上下文,我无法判断这是否只是一些用于个人用途的快速而肮脏的代码,还是高盛最新内部会计软件的关键部分,因此我将以"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

我的问题是我有一个列表,由字母数字字符串(如c22、c3、c5等)的组合、字母字符串(如a、h、r等)和只需要按顺序A、c3、c5、c22、h、r、45、99排序的数字(如99、45等)组成。我也有需要删除的副本,所以我只得到一个条目。

我不仅处理字符串,还对对象排序,并使用对象中的特定字段来获得正确的顺序。

一个对我有用的解决方案是:

SortedSet codeSet;

codeSet = new TreeSet(new Comparator() {

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上找到的一些代码,加上我自己的一些调整,让它按照我需要的方式工作。

由于试图排序对象,需要一个比较器以及重复的删除,我不得不使用一种消极的敷衍方法,那就是在将对象写入TreeSet之前,我必须先将对象写入TreeMap。它可能会对性能有一点影响,但是考虑到列表最多可以有80个代码,这不应该是一个问题。

尽管这个问题问了一个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

})

}

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

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

BBB 12 CCC

VS

EEE 12 DDD JPEG2000 EEE

在Linux上,glibc提供strverscmp(),它也可以从gnulib获得可移植性。然而真正的"人类"分类还有很多其他的怪癖,比如"披头士",被分类为"披头士,the"。对于这个一般问题没有简单的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>