16

我有一个字符串列表,其中可以包含一个字母或一个 int 的字符串表示形式(最多 2 位数字)。它们需要按字母顺序或(当它实际上是一个 int 时)按它所代表的数值排序。

例子:

IList<string> input = new List<string>()
    {"a", 1.ToString(), 2.ToString(), "b", 10.ToString()};

input.OrderBy(s=>s)
  // 1
  // 10
  // 2
  // a
  // b

我想要的是

  // 1
  // 2
  // 10
  // a
  // b

我有一些想法涉及格式化它并尝试解析它,然后如果它是一个成功的尝试解析,则使用我自己的自定义 stringformatter 对其进行格式化以使其具有前面的零。我希望有更简单和高性能的东西。

编辑
我最终制作了一个 IComparer,我将其转储到我的 Utils 库中以供以后使用。
当我在做的时候,我也加入了双打。

public class MixedNumbersAndStringsComparer : IComparer<string> {
    public int Compare(string x, string y) {
        double xVal, yVal;

        if(double.TryParse(x, out xVal) && double.TryParse(y, out yVal))
            return xVal.CompareTo(yVal);
        else 
            return string.Compare(x, y);
    }
}

//Tested on int vs int, double vs double, int vs double, string vs int, string vs doubl, string vs string.
//Not gonna put those here
[TestMethod]
public void RealWorldTest()
{
    List<string> input = new List<string>() { "a", "1", "2,0", "b", "10" };
    List<string> expected = new List<string>() { "1", "2,0", "10", "a", "b" };
    input.Sort(new MixedNumbersAndStringsComparer());
    CollectionAssert.AreEquivalent(expected, input);
}
4

10 回答 10

22

我想到了两种方法,不确定哪个更高效。实现自定义 IComparer:

class MyComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        int xVal, yVal;
        var xIsVal = int.TryParse( x, out xVal );
        var yIsVal = int.TryParse( y, out yVal );

        if (xIsVal && yIsVal)   // both are numbers...
            return xVal.CompareTo(yVal);
        if (!xIsVal && !yIsVal) // both are strings...
            return x.CompareTo(y);
        if (xIsVal)             // x is a number, sort first
            return -1;
        return 1;               // x is a string, sort last
    }
}

var input = new[] {"a", "1", "10", "b", "2", "c"};
var e = input.OrderBy( s => s, new MyComparer() );

或者,将序列拆分为数字和非数字,然后对每个子组进行排序,最后加入排序后的结果;就像是:

var input = new[] {"a", "1", "10", "b", "2", "c"};

var result = input.Where( s => s.All( x => char.IsDigit( x ) ) )
                  .OrderBy( r => { int z; int.TryParse( r, out z ); return z; } )
                  .Union( input.Where( m => m.Any( x => !char.IsDigit( x ) ) )
                               .OrderBy( q => q ) );
于 2009-06-23T14:21:04.517 回答
13

也许您可以采用更通用的方法并使用自然排序算法,例如此处的 C# 实现。

于 2009-06-23T14:48:35.537 回答
3

使用OrderByIComparer参数的另一个重载。

然后你可以实现你自己的IComparer,用来int.TryParse判断它是否是一个数字。

于 2009-06-23T14:12:28.323 回答
3

我遇到了类似的问题并落在这里:对具有数字后缀的字符串进行排序,如下例所示。

原来的:

"Test2", "Test1", "Test10", "Test3", "Test20"

默认排序结果:

"Test1", "Test10", "Test2", "Test20", "Test3"

期望的排序结果:

"Test1", "Test2", "Test3, "Test10", "Test20"

我最终使用了自定义比较器:

public class NaturalComparer : IComparer
{

    public NaturalComparer()
    {
        _regex = new Regex("\\d+$", RegexOptions.IgnoreCase);
    }

    private Regex _regex;

    private string matchEvaluator(System.Text.RegularExpressions.Match m)
    {
        return Convert.ToInt32(m.Value).ToString("D10");
    }

    public int Compare(object x, object y)
    {
        x = _regex.Replace(x.ToString(), matchEvaluator);
        y = _regex.Replace(y.ToString(), matchEvaluator);

        return x.CompareTo(y);
    }
}   

用法:

var input = new List<MyObject>(){...};
var sorted = input.OrderBy(o=>o.SomeStringMember, new NaturalComparer());

HTH ;o)

于 2017-01-31T21:29:14.633 回答
2

我想说您可以使用正则表达式(假设所有内容都是 int)拆分值,然后将它们重新连接在一起。

//create two lists to start
string[] data = //whatever...
List<int> numbers = new List<int>();
List<string> words = new List<string>();

//check each value
foreach (string item in data) {
    if (Regex.IsMatch("^\d+$", item)) {
        numbers.Add(int.Parse(item));
    }
    else {
        words.Add(item);
    }
}

然后使用您的两个列表,您可以对它们中的每一个进行排序,然后以您想要的任何格式将它们合并在一起。

于 2009-06-23T14:11:51.400 回答
2

您可以只使用Win32 API 提供的函数

[DllImport ("shlwapi.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
static extern int StrCmpLogicalW (String x, String y);

IComparer并像其他人所展示的那样从 a 中调用它。

于 2009-06-23T14:53:53.950 回答
1
public static int? TryParse(string s)
{
    int i;
    return int.TryParse(s, out i) ? (int?)i : null;
}

// in your method
IEnumerable<string> input = new string[] {"a", "1","2", "b", "10"};
var list = input.Select(s => new { IntVal = TryParse(s), String =s}).ToList();
list.Sort((s1, s2) => {
    if(s1.IntVal == null && s2.IntVal == null)
    {
        return s1.String.CompareTo(s2.String);
    }
    if(s1.IntVal == null)
    {
        return 1;
    }
    if(s2.IntVal == null)
    {
        return -1;
    }
    return s1.IntVal.Value.CompareTo(s2.IntVal.Value);
});
input = list.Select(s => s.String);

foreach(var x in input)
{
    Console.WriteLine(x);
}

它仍然进行转换,但只有一次/项目。

于 2009-06-23T14:20:49.807 回答
1

您可以使用自定义比较器 - 订购语句将是:

var result = input.OrderBy(s => s, new MyComparer());

其中 MyComparer 的定义如下:

public class MyComparer : Comparer<string>
{
    public override int Compare(string x, string y)
    {

        int xNumber;
        int yNumber;
        var xIsNumber = int.TryParse(x, out xNumber);
        var yIsNumber = int.TryParse(y, out yNumber);

        if (xIsNumber && yIsNumber)
        {
            return xNumber.CompareTo(yNumber);
        }
        if (xIsNumber)
        {
            return -1;
        }
        if (yIsNumber)
        {
            return 1;
        }
        return x.CompareTo(y);
    }
}

虽然这可能看起来有点冗长,但它将排序逻辑封装为适当的类型。然后,如果您愿意,您可以轻松地对比较器进行自动化测试(单元测试)。它也是可重复使用的。

(也许可以使算法更清晰一些,但这是我能快速拼凑的最好的。)

于 2009-06-23T14:42:12.320 回答
1

你也可以在某种意义上“作弊”。根据您对问题的描述,您知道任何长度为 2 的字符串都是一个数字。所以只需对所有长度为 1 的字符串进行排序。然后对所有长度为 2 的字符串进行排序。然后进行一堆交换以以正确的顺序重新排序字符串。本质上,该过程将按如下方式工作:(假设您的数据在数组中。)

步骤 1:将所有长度为 2 的字符串推送到数组的末尾。跟踪你有多少。

第 2 步:就地排序长度为 1 的字符串和长度为 2 的字符串。

第 3 步:对位于两半边界上的“a”进行二分搜索。

第 4 步:根据需要用字母交换两位数的字符串。

也就是说,虽然这种方法可行,但不涉及正则表达式,也不会尝试将非 int 值解析为 int——我不推荐它。与已经建议的其他方法相比,您将编写更多的代码。它混淆了你正在尝试做的事情。如果你突然得到两个字母的字符串或三位数字的字符串,它就不起作用了。等等。我只是将它包括在内,以展示您如何以不同的方式看待问题,并提出替代解决方案。

于 2009-06-23T14:44:22.487 回答
1

使用Schwartzian 变换执行 O(n) 转换!

private class Normalized : IComparable<Normalized> {
  private readonly string str;
  private readonly int val;

  public Normalized(string s) {
    str = s;

    val = 0;
    foreach (char c in s) {
      val *= 10;

      if (c >= '0' && c <= '9')
        val += c - '0';
      else
        val += 100 + c;
    }
  }

  public String Value { get { return str; } }

  public int CompareTo(Normalized n) { return val.CompareTo(n.val); }
};

private static Normalized In(string s) { return new Normalized(s); }
private static String Out(Normalized n) { return n.Value; }

public static IList<String> MixedSort(List<String> l) {
  var tmp = l.ConvertAll(new Converter<String,Normalized>(In));
  tmp.Sort();
  return tmp.ConvertAll(new Converter<Normalized,String>(Out));
}
于 2009-06-23T15:23:17.167 回答