对混合数字和字符串进行排序

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

例:

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

我想要的是

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

我有一些想法涉及通过尝试解析它来格式化它,然后如果它是一个成功的tryparse用我自己的自定义stringformatter格式化它使它有前面的零。 我希望能有更简单,更高效的东西。

编辑
我最终制作了一个IComparer,我把它放在我的Utils库中供以后使用。
当我在它的时候,我也在混合物中投掷了双打。

 public class MixedNumbersAndStringsComparer : IComparer { 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 input = new List() { "a", "1", "2,0", "b", "10" }; List expected = new List() { "1", "2,0", "10", "a", "b" }; input.Sort(new MixedNumbersAndStringsComparer()); CollectionAssert.AreEquivalent(expected, input); } 

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

我想到了两种方式,不确定哪种方式更具性能。 实现自定义IComparer:

 class MyComparer : IComparer { 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 ) ); 

使用带有IComparer参数的OrderBy的另一个重载。

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

我会说你可以使用RegularExpression拆分值(假设一切都是int),然后将它们重新加入。

 //create two lists to start string[] data = //whatever... List numbers = new List(); List words = new List(); //check each value foreach (string item in data) { if (Regex.IsMatch("^\d+$", item)) { numbers.Add(int.Parse(item)); } else { words.Add(item); } } 

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

您可以使用Win32 API提供的function:

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

并像其他人所示,从IComparer调用它。

 public static int? TryParse(string s) { int i; return int.TryParse(s, out i) ? (int?)i : null; } // in your method IEnumerable 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); } 

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

您可以使用自定义比较器 – 订购声明将是:

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

MyComparer的定义如下:

 public class MyComparer : Comparer { 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); } } 

虽然这看起来有点冗长,但它将排序逻辑封装成适当的类型。 如果您愿意,您可以轻松地让Comparer进行自动化测试(unit testing)。 它也是可重复使用的。

(有可能使算法更清晰一点,但这是我能迅速拼凑起来的最佳方法。)

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

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

第2步:在适当的位置排序长度为1的字符串和长度为2的字符串。

第3步:二进制搜索’a’,它将位于你的两半的边界上。

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

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

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

 private class Normalized : IComparable { 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 MixedSort(List l) { var tmp = l.ConvertAll(new Converter(In)); tmp.Sort(); return tmp.ConvertAll(new Converter(Out)); } 

我有一个类似的问题,并在这里登陆:排序具有数字后缀的字符串,如下例所示。

原版的:

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

默认排序结果:

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

期望的排序结果:

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

我最终使用自定义Comparer:

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

HTH; o)