如何按字母顺序对字符串进行排序,同时考虑到其中包含数字的情况?

126

我在尝试对一个由字符串组成的数字数组进行排序,希望按照数值大小排序。

但是问题在于我不能将这些数字转换为整数。

以下是代码:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => x))
{
    Console.WriteLine(thing);
}

输出:

101, 102, 103, 105, 90
我希望拥有:

我希望:

90, 101, 102, 103, 105

编辑: 输出不能为090, 101, 102...

更新了代码示例,将“sizes”更改为“things”。 数组可能像这样:

string[] things= new string[] { "paul", "bob", "lauren", "007", "90" };

这意味着需要按字母和数字进行排序:

007, 90, bob, lauren, paul

9
为什么不能将它们转换为整数? - Femaref
2
"Sizes" 可以是其他类似于 "name" 的东西。代码示例只是简化的。 - sf.
4
这些数字中会有负数吗?它们都是整数吗?整数的范围是什么? - Eric Lippert
1
“things” 可以是任何类型的字符串。我希望列表按照对非计算机专业人士有意义的逻辑顺序排序。负数应该排在正数之前。就字符串长度而言,它不会超过100个字符。 - sf.
7
你想走多远? image10 应该在 image2 之后吗? January 应该在 February 之前吗? - svick
显示剩余3条评论
24个回答

126

可以向 OrderBy 方法中传入自定义比较器。 Enumerable.OrderBy 允许您指定任何想要的比较器。

以下是一种实现方式:

void Main()
{
    string[] things = new string[] { "paul", "bob", "lauren", "007", "90", "101"};

    foreach (var thing in things.OrderBy(x => x, new SemiNumericComparer()))
    {    
        Console.WriteLine(thing);
    }
}


public class SemiNumericComparer: IComparer<string>
{
    /// <summary>
    /// Method to determine if a string is a number
    /// </summary>
    /// <param name="value">String to test</param>
    /// <returns>True if numeric</returns>
    public static bool IsNumeric(string value)
    {
        return int.TryParse(value, out _);
    }

    /// <inheritdoc />
    public int Compare(string s1, string s2)
    {
        const int S1GreaterThanS2 = 1;
        const int S2GreaterThanS1 = -1;

        var IsNumeric1 = IsNumeric(s1);
        var IsNumeric2 = IsNumeric(s2);

        if (IsNumeric1 && IsNumeric2)
        {
            var i1 = Convert.ToInt32(s1);
            var i2 = Convert.ToInt32(s2);

            if (i1 > i2)
            {
                return S1GreaterThanS2;
            }

            if (i1 < i2)
            {
                return S2GreaterThanS1;
            }

            return 0;
        }

        if (IsNumeric1)
        {
            return S2GreaterThanS1;
        }

        if (IsNumeric2)
        {
            return S1GreaterThanS2;
        }

        return string.Compare(s1, s2, true, CultureInfo.InvariantCulture);
    }
}

1
对于给定的输入,这将产生与递归答案相同的结果,其中包括PadLeft()。我假设您的输入实际上比此示例显示的更复杂,在这种情况下,最好使用自定义比较器。 - Jeff Paulsen
谢谢。这个解决方案可行,而且似乎是一种易于阅读和清晰实现的方式。+1,因为你向我展示了如何在OrderBy中使用IComparer :) - sf.
20
“IsNumeric” 方法不好,基于异常的编程方式总是不好的。最好使用 “int.TryParse” 方法。如果使用大型列表测试代码将会非常耗时。 - Nean Der Thal
如果有帮助的话,我在这个版本上添加了一个扩展链接,它支持使用单词进行排序。对于我的需求,按空格分割就足够了,而且我很少需要担心混合使用的单词(例如test12与test3)。 - matt.bungard
如果两个值都是数字,我们可以返回 i1.CompareTo(i2) - Dariusz
显示剩余2条评论

119

只需用零填充到相同长度即可:

int maxlen = sizes.Max(x => x.Length);
var result = sizes.OrderBy(x => x.PadLeft(maxlen, '0'));

好主意,但下一个问题是我需要显示这些值,所以“90”必须是“90”,而不是“090”。 - sf.
12
尝试一下,你可能会喜欢结果。记住,排序关键字并不是被排序的东西。如果我说按姓氏顺序排序客户列表,那么我得到的是客户列表,而不是姓氏列表。如果你说要按转换后的字符串排序字符串列表,那么结果是原始字符串的有序列表,而不是转换后的字符串列表。 - Eric Lippert
1
@gorgabal:通常重新分配给sizes也行不通,因为结果是不同的类型。答案有些简略,第二行显示了表达式的结果,但由读者自行处理。我添加了另一个变量赋值以使其更清晰。 - recursive
我真的希望我知道为什么这个有效,但现在已经太晚了,我不再关心。 - Joshua Lumley
应该选择这个答案。 - Desolator
显示剩余2条评论

108

值是一个字符串

List = List.OrderBy(c => c.Value.Length).ThenBy(c => c.Value).ToList();

工作


7
这个答案是我最喜欢的。 - LacOniC
3
谢谢,我刚刚发现存在一个“ThenBy”方法。 - ganchito55
4
这是最佳答案,它有效并且具有很好的学习价值。谢谢! - Incredible
4
但这样会混合字母字符串,比如:"b"、"ab"、"101"、"103"、"bob"、"abcd"。 - Andrew
2
你的例子不好,因为new List<string> { "01", "2" }.OrderBy(c => c.Length).ThenBy(c => c).ToList();排序结果是"2","01",而不是作者想要的"01","2"。 - Vitalij Roscinski
显示剩余3条评论

77

那么,这个怎么样呢...

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

var size = from x in sizes
           orderby x.Length, x
           select x;

foreach (var p in size)
{
    Console.WriteLine(p);
}

呵呵,我真的很喜欢这个 - 非常聪明。如果我没有提供完整的初始数据,对不起。 - sf.
3
这很像上面的“pad”选项,只是在我看来更好。 - dudeNumber4
4
变量size等于按长度排序后再按字典序排序的sizes列表。 - Phillip Davis
4
但是这样会混合字母字符串,比如:"b", "ab", "101", "103", "bob", "abcd" - Andrew

24

在Windows中有一个本地函数StrCmpLogicalW,它可以将字符串中的数字按照数字而不是字母进行比较。很容易编写一个调用该函数并使用它进行比较的比较器。

public class StrCmpLogicalComparer : Comparer<string>
{
    [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string x, string y);

    public override int Compare(string x, string y)
    {
        return StrCmpLogicalW(x, y);
    }
}

它甚至可以处理既包含文本又包含数字的字符串。下面是一个示例程序,将展示默认排序和StrCmpLogicalW排序之间的区别。

class Program
{
    static void Main()
    {
        List<string> items = new List<string>()
        {
            "Example1.txt", "Example2.txt", "Example3.txt", "Example4.txt", "Example5.txt", "Example6.txt", "Example7.txt", "Example8.txt", "Example9.txt", "Example10.txt",
            "Example11.txt", "Example12.txt", "Example13.txt", "Example14.txt", "Example15.txt", "Example16.txt", "Example17.txt", "Example18.txt", "Example19.txt", "Example20.txt"
        };

        items.Sort();

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine();

        items.Sort(new StrCmpLogicalComparer());

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
        Console.ReadLine();
    }
}

输出

Example1.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example2.txt
Example20.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt

Example1.txt
Example2.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example20.txt

我希望在C#中使用系统库更加容易。 - Kyle Delaney
这本来是完美的,但不幸的是它不能处理负数。-1 0 10 2 被排序为 0 -1 2 10 - nphx

6

试试这个

sizes.OrderBy(x => Convert.ToInt32(x)).ToList<string>();

注意:当所有内容都可以转换为整数时,这将非常有用。

1
这会将字符串转换为整数。 - Femaref
1
"大小"也可以是非数字的。 - sf.
对于“LINQ to SQL”,在 sizes.ToList().OrderBy(x => Convert.ToInt32(x)) 之前不要忘记使用 ToList() - A. Morel

6
这个网站讨论字母数字排序,并将按照逻辑顺序而不是ASCII顺序对数字进行排序。它还考虑了周围的字母。

http://www.dotnetperls.com/alphanumeric-sorting

例子:

  • C:/TestB/333.jpg
  • 11
  • C:/TestB/33.jpg
  • 1
  • C:/TestA/111.jpg
  • 111F
  • C:/TestA/11.jpg
  • 2
  • C:/TestA/1.jpg
  • 111D
  • 22
  • 111Z
  • C:/TestB/03.jpg

  • 1
  • 2
  • 11
  • 22
  • 111D
  • 111F
  • 111Z
  • C:/TestA/1.jpg
  • C:/TestA/11.jpg
  • C:/TestA/111.jpg
  • C:/TestB/03.jpg
  • C:/TestB/33.jpg
  • C:/TestB/333.jpg
代码如下:
class Program
{
    static void Main(string[] args)
    {
        var arr = new string[]
        {
           "C:/TestB/333.jpg",
           "11",
           "C:/TestB/33.jpg",
           "1",
           "C:/TestA/111.jpg",
           "111F",
           "C:/TestA/11.jpg",
           "2",
           "C:/TestA/1.jpg",
           "111D",
           "22",
           "111Z",
           "C:/TestB/03.jpg"
        };
        Array.Sort(arr, new AlphaNumericComparer());
        foreach(var e in arr) {
            Console.WriteLine(e);
        }
    }
}

public class AlphaNumericComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string s1 = x as string;
        if (s1 == null)
        {
            return 0;
        }
        string s2 = y as string;
        if (s2 == null)
        {
            return 0;
        }

        int len1 = s1.Length;
        int len2 = s2.Length;
        int marker1 = 0;
        int marker2 = 0;

        // Walk through two the strings with two markers.
        while (marker1 < len1 && marker2 < len2)
        {
            char ch1 = s1[marker1];
            char ch2 = s2[marker2];

            // Some buffers we can build up characters in for each chunk.
            char[] space1 = new char[len1];
            int loc1 = 0;
            char[] space2 = new char[len2];
            int loc2 = 0;

            // Walk through all following characters that are digits or
            // characters in BOTH strings starting at the appropriate marker.
            // Collect char arrays.
            do
            {
                space1[loc1++] = ch1;
                marker1++;

                if (marker1 < len1)
                {
                    ch1 = s1[marker1];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch1) == char.IsDigit(space1[0]));

            do
            {
                space2[loc2++] = ch2;
                marker2++;

                if (marker2 < len2)
                {
                    ch2 = s2[marker2];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch2) == char.IsDigit(space2[0]));

            // If we have collected numbers, compare them numerically.
            // Otherwise, if we have strings, compare them alphabetically.
            string str1 = new string(space1);
            string str2 = new string(space2);

            int result;

            if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]))
            {
                int thisNumericChunk = int.Parse(str1);
                int thatNumericChunk = int.Parse(str2);
                result = thisNumericChunk.CompareTo(thatNumericChunk);
            }
            else
            {
                result = str1.CompareTo(str2);
            }

            if (result != 0)
            {
                return result;
            }
        }
        return len1 - len2;
    }
}

5

如果字符串中有一些数字的话,我觉得这会更好。希望能对你有所帮助。

PS: 我不确定性能或复杂字符串值的情况,但这种方式对于如下内容是有效的:

lorem ipsum
lorem ipsum 1
lorem ipsum 2
lorem ipsum 3
...
lorem ipsum 20
lorem ipsum 21

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        int s1r, s2r;
        var s1n = IsNumeric(s1, out s1r);
        var s2n = IsNumeric(s2, out s2r);

        if (s1n && s2n) return s1r - s2r;
        else if (s1n) return -1;
        else if (s2n) return 1;

        var num1 = Regex.Match(s1, @"\d+$");
        var num2 = Regex.Match(s2, @"\d+$");

        var onlyString1 = s1.Remove(num1.Index, num1.Length);
        var onlyString2 = s2.Remove(num2.Index, num2.Length);

        if (onlyString1 == onlyString2)
        {
            if (num1.Success && num2.Success) return Convert.ToInt32(num1.Value) - Convert.ToInt32(num2.Value);
            else if (num1.Success) return 1;
            else if (num2.Success) return -1;
        }

        return string.Compare(s1, s2, true);
    }

    public bool IsNumeric(string value, out int result)
    {
        return int.TryParse(value, out result);
    }
}

2
正是我所需要的。谢谢! - klugerama

5

您说无法将数字转换为int,因为数组可能包含无法转换为int的元素,但尝试一下是没有问题的:

string[] things = new string[] { "105", "101", "102", "103", "90", "paul", "bob", "lauren", "007", "90" };
Array.Sort(things, CompareThings);

foreach (var thing in things)
    Debug.WriteLine(thing);

然后像这样进行比较:
private static int CompareThings(string x, string y)
{
    int intX, intY;
    if (int.TryParse(x, out intX) && int.TryParse(y, out intY))
        return intX.CompareTo(intY);

    return x.CompareTo(y);
}

输出结果:007,90,90,101,102,103,105,鲍勃,劳伦,保罗


顺便说一下,我为了简单起见使用了Array.Sort,但你可以在IComparer中使用相同的逻辑并使用OrderBy。 - Ulf Kristiansen
这个解决方案似乎比使用IComparer更快(我的意见)。我有15000个结果,感觉这样可以节省大约一秒钟的时间。 - Jason Foglia

3

这似乎是一个奇怪的要求,需要一个奇怪的解决方案:

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

foreach (var size in sizes.OrderBy(x => {
    double sum = 0;
    int position = 0;
    foreach (char c in x.ToCharArray().Reverse()) {
        sum += (c - 48) * (int)(Math.Pow(10,position));
        position++;
    }
    return sum;
}))

{
    Console.WriteLine(size);
}

我当然是指0x30。此外,数组仍然可能包含非数字字符串,对于这种情况,解决方案将产生有趣的结果。 - Femaref
请注意,-48或不是绝对没有变化的,我们可以直接使用字符的整数值,因此如果它让您感到困扰,请删除-48... - Marino Šimić
字符值为0x30,如果将其转换为int,它仍然是0x30,这不是数字0。 - Femaref
唯一转换为整数的是从Math.Pow返回的double。 - Marino Šimić
@Marino:sum不代表整个值表示为int吗? - recursive
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接