【问题标题】:Sorting mixed numbers and strings对混合数字和字符串进行排序
【发布时间】:2023-03-30 03:22:01
【问题描述】:

我有一个字符串列表,其中可以包含一个字母或一个 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

我有一些想法涉及格式化它并尝试解析它,然后如果它是一个成功的 tryparse 用我自己的自定义 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);
}

【问题讨论】:

    标签: c# sorting formatting tostring


    【解决方案1】:

    我想到了两种方式,不确定哪种方式更高效。实现自定义 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 ) );
    

    【讨论】:

    • 您的 IComparer 未按正确(字母顺序)顺序返回非数字字符串。您的 LINQ 查询可以。
    • 我在 OP 中添加了我的结束代码。还注意到了字符串的事情。此外,我在每次解析之前都尝试了短路。不知道它是否对性能有很大影响,但我花了同样多的精力来重新排序它们,就像我测试它所花费的一样;)
    • 使代码更短。通过应用 short cirquiting 系统(字面翻译自荷兰语“Kortsluitingsprincipe”),我只根据需要执行尽可能多的 tryparses。
    【解决方案2】:

    也许您可以采用更通用的方法并使用 natural sorting 算法,例如 C# 实现 here

    【讨论】:

    • 确实很酷,我也刚刚找到了一个 Delphi 包装器 irsoft.de/web/strnatcmp-and-natsort-for-delphi
    • 这并不适用于所有情况。假设 ypu 有以下项目列表:“0 / 30”“0 / 248”“0 / 496”“0 / 357.6”。排序后将保留此顺序,这可能不是您所期望的。
    • 您应该在此处添加代码以避免死链接,而不是粘贴一些 url。现在这个答案只不过是一条评论
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

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

      原文:

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

      【讨论】:

      • 这正是我需要的,但我在对象列表类型中有字符串列表,你能说明如何使用这个方法吗?
      • @AakashBashyal 我在上面的答案中添加了一个示例。
      • 仅仅创建一个列表类型的对象就会对列表中的项目进行排序?
      • x = _regex.Replace(x.ToString, matchEvaluator); 上有错误,上面写着Argument 1: cannot convert from 'method group' to 'string'
      • @AakashBashyal 对不起,我的错。我修复了代码。谢谢!
      【解决方案5】:

      我想说您可以使用正则表达式(假设一切都是 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);
          }
      }
      

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

      【讨论】:

      • 是的,这比我的方法简单。 +1
      【解决方案6】:

      你可以使用函数provided by the Win32 API:

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

      并像其他人所展示的那样通过IComparer 调用它。

      【讨论】:

        【解决方案7】:
        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);
        }
        

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

        【讨论】:

          【解决方案8】:

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

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

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

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

          【讨论】:

            【解决方案9】:

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

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

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

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

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

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

            【讨论】:

              【解决方案10】:

              使用Schwartzian Transform 执行 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));
              }
              

              【讨论】:

              • 据我所知,我发布的内容并不简单。性能可能更高,但将性能置于简单之上还不够重要
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-10-07
              • 2020-03-21
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多