Список сортировки ‹String› в C #

Как отсортировать список по целочисленному значению элемента

Список похож на

"1"
"5"
"3"
"6"
"11"
"9"
"NUM1"
"NUM0"

Результат должен быть как

"1"
"3"
"5"
"6"
"9"
"11"
"NUM0"
"NUM1"

есть ли идея сделать это с помощью LINQ или лямбда-выражения?

заранее спасибо


person Thorin Oakenshield    schedule 15.09.2010    source источник
comment
Представляют ли эти строковые значения шестнадцатеричные числа? Или возможно, что S2 появится в списке скажем?   -  person El Ronnoco    schedule 15.09.2010
comment
@ Эль Ронноко: Не в шестнадцатеричной системе счисления. Это может быть S2 и т. Д. (Отредактировано)   -  person Thorin Oakenshield    schedule 15.09.2010


Ответы (7)


Как насчет:

    list.Sort((x, y) =>
    {
        int ix, iy;
        return int.TryParse(x, out ix) && int.TryParse(y, out iy)
              ? ix.CompareTo(iy) : string.Compare(x, y);
    });
person Marc Gravell    schedule 15.09.2010
comment
как насчет различий в NUM10 и NUM2, для нас NUM2, очевидно, раньше NUM10, но он не будет отсортирован таким образом - person Xander; 15.09.2010
comment
@Xander - определите явно; вопрос процитировал целочисленное значение - но если OP не определяет термины / шаблоны, которые должны быть разрешены, я не включаю NUM10 в качестве целочисленного значения. - person Marc Gravell; 15.09.2010
comment
Извините, когда я имел в виду, очевидно, это было с точки зрения того, как человек будет воспринимать значение как означающее ... как в NUM и целое число десять NUM10 ... может / не может быть желательным. - person Xander; 15.09.2010

Это называется «естественным порядком сортировки» и обычно используется для сортировки таких элементов, как те, которые у вас есть, например, имен файлов и т.п.

Вот наивная (в том смысле, что с ней, вероятно, много проблем с Unicode) реализация, которая, кажется, помогает:

Вы можете скопировать приведенный ниже код в LINQPad, чтобы выполнить и протестировать его.

В основном алгоритм сравнения идентифицирует числа внутри строк и обрабатывает их, дополняя самую короткую из них ведущими нулями, поэтому, например, две строки "Test123Abc" и "Test7X" следует сравнивать, как если бы они были "Test123Abc" и "Test007X", что должно дать то, что вы хотите.

Однако, когда я сказал «наивный», я имел в виду, что у меня, вероятно, есть масса настоящих проблем с Unicode, таких как обработка диакритических знаков и символов с несколькими кодовыми точками. Если кто-то может дать лучшую реализацию, я хотел бы ее увидеть.

Примечания:

  • Реализация на самом деле не анализирует числа, поэтому произвольно длинные числа должны работать нормально.
  • Поскольку на самом деле он не анализирует числа как «числа», числа с плавающей запятой не будут обрабатываться должным образом, «123,45» и «123,789» будут сравниваться как «123,045» и «123,789», что неверно.

Код:

void Main()
{
    List<string> input = new List<string>
    {
        "1", "5", "3", "6", "11", "9", "A1", "A0"
    };
    var output = input.NaturalSort();
    output.Dump();
}

public static class Extensions
{
    public static IEnumerable<string> NaturalSort(
        this IEnumerable<string> collection)
    {
        return NaturalSort(collection, CultureInfo.CurrentCulture);
    }

    public static IEnumerable<string> NaturalSort(
        this IEnumerable<string> collection, CultureInfo cultureInfo)
    {
        return collection.OrderBy(s => s, new NaturalComparer(cultureInfo));
    }

    private class NaturalComparer : IComparer<string>
    {
        private readonly CultureInfo _CultureInfo;

        public NaturalComparer(CultureInfo cultureInfo)
        {
            _CultureInfo = cultureInfo;
        }

        public int Compare(string x, string y)
        {
            // simple cases
            if (x == y) // also handles null
                return 0;
            if (x == null)
                return -1;
            if (y == null)
                return +1;

            int ix = 0;
            int iy = 0;
            while (ix < x.Length && iy < y.Length)
            {
                if (Char.IsDigit(x[ix]) && Char.IsDigit(y[iy]))
                {
                    // We found numbers, so grab both numbers
                    int ix1 = ix++;
                    int iy1 = iy++;
                    while (ix < x.Length && Char.IsDigit(x[ix]))
                        ix++;
                    while (iy < y.Length && Char.IsDigit(y[iy]))
                        iy++;
                    string numberFromX = x.Substring(ix1, ix - ix1);
                    string numberFromY = y.Substring(iy1, iy - iy1);

                    // Pad them with 0's to have the same length
                    int maxLength = Math.Max(
                        numberFromX.Length,
                        numberFromY.Length);
                    numberFromX = numberFromX.PadLeft(maxLength, '0');
                    numberFromY = numberFromY.PadLeft(maxLength, '0');

                    int comparison = _CultureInfo
                        .CompareInfo.Compare(numberFromX, numberFromY);
                    if (comparison != 0)
                        return comparison;
                }
                else
                {
                    int comparison = _CultureInfo
                        .CompareInfo.Compare(x, ix, 1, y, iy, 1);
                    if (comparison != 0)
                        return comparison;
                    ix++;
                    iy++;
                }
            }

            // we should not be here with no parts left, they're equal
            Debug.Assert(ix < x.Length || iy < y.Length);

            // we still got parts of x left, y comes first
            if (ix < x.Length)
                return +1;

            // we still got parts of y left, x comes first
            return -1;
        }
    }
}
person Lasse V. Karlsen    schedule 15.09.2010
comment
Я открыл вопрос, чтобы узнать, как лучше обрабатывать диакритические знаки и символы с несколькими кодовыми точками, здесь: stackoverflow.com/questions/3717132/ - person Lasse V. Karlsen; 15.09.2010

Попробуйте написать небольшой вспомогательный класс для анализа и представления ваших токенов. Например, без лишних проверок:

public class NameAndNumber
{
    public NameAndNumber(string s)
    {
        OriginalString = s;
        Match match = Regex.Match(s,@"^(.*?)(\d*)$");
        Name = match.Groups[1].Value;
        int number;
        int.TryParse(match.Groups[2].Value, out number);
        Number = number; //will get default value when blank
    }

    public string OriginalString { get; private set; }
    public string Name { get; private set; }
    public int Number { get; private set; }
}

Теперь стало легко написать средство сравнения или отсортировать его вручную:

var list = new List<string> { "ABC", "1", "5", "NUM44", "3", 
                              "6", "11", "9", "NUM1", "NUM0" };

var sorted = list.Select(str => new NameAndNumber(str))
    .OrderBy(n => n.Name)
    .ThenBy(n => n.Number);

Дает результат:

1, 3, 5, 6, 9, 11, ABC, NUM0, NUM1, NUM44

person Kobi    schedule 15.09.2010
comment
В качестве примечания, код учитывает только число в конце строки - a123b12 - ›Name:a123b, Number:12 - person Kobi; 15.09.2010

У Джеффа Этвуда есть запись в блоге о естественной сортировке, где он ссылается на некоторые доступные реализации желаемого алгоритма.

Одна из ссылок Джеффа указывает на Дэйва Коэлла, как Реализация C #:

/*
 * The Alphanum Algorithm is an improved sorting algorithm for strings
 * containing numbers.  Instead of sorting numbers in ASCII order like
 * a standard sort, this algorithm sorts numbers in numeric order.
 *
 * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
 *
 * Based on the Java implementation of Dave Koelle's Alphanum algorithm.
 * Contributed by Jonathan Ruckwood <[email protected]>
 *
 * Adapted by Dominik Hurnaus <[email protected]> to
 *   - correctly sort words where one word starts with another word
 *   - have slightly better performance
 *
 * Released under the MIT License - https://opensource.org/licenses/MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
using System;
using System.Collections;
using System.Text;

/*
 * Please compare against the latest Java version at http://www.DaveKoelle.com
 * to see the most recent modifications
 */
namespace AlphanumComparator
{
    public class AlphanumComparator : IComparer
    {
        private enum ChunkType {Alphanumeric, Numeric};
        private bool InChunk(char ch, char otherCh)
        {
            ChunkType type = ChunkType.Alphanumeric;

            if (char.IsDigit(otherCh))
            {
                type = ChunkType.Numeric;
            }

            if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
                || (type == ChunkType.Numeric && !char.IsDigit(ch)))
            {
                return false;
            }

            return true;
        }

        public int Compare(object x, object y)
        {
            String s1 = x as string;
            String s2 = y as string;
            if (s1 == null || s2 == null)
            {
                return 0;
            }

            int thisMarker = 0, thisNumericChunk = 0;
            int thatMarker = 0, thatNumericChunk = 0;

            while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
            {
                if (thisMarker >= s1.Length)
                {
                    return -1;
                }
                else if (thatMarker >= s2.Length)
                {
                    return 1;
                }
                char thisCh = s1[thisMarker];
                char thatCh = s2[thatMarker];

                StringBuilder thisChunk = new StringBuilder();
                StringBuilder thatChunk = new StringBuilder();

                while ((thisMarker < s1.Length) && (thisChunk.Length==0 ||InChunk(thisCh, thisChunk[0])))
                {
                    thisChunk.Append(thisCh);
                    thisMarker++;

                    if (thisMarker < s1.Length)
                    {
                        thisCh = s1[thisMarker];
                    }
                }

                while ((thatMarker < s2.Length) && (thatChunk.Length==0 ||InChunk(thatCh, thatChunk[0])))
                {
                    thatChunk.Append(thatCh);
                    thatMarker++;

                    if (thatMarker < s2.Length)
                    {
                        thatCh = s2[thatMarker];
                    }
                }

                int result = 0;
                // If both chunks contain numeric characters, sort them numerically
                if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
                {
                    thisNumericChunk = Convert.ToInt32(thisChunk.ToString());
                    thatNumericChunk = Convert.ToInt32(thatChunk.ToString());

                    if (thisNumericChunk < thatNumericChunk)
                    {
                        result = -1;
                    }

                    if (thisNumericChunk > thatNumericChunk)
                    {
                        result = 1;
                    }
                }
                else
                {
                    result = thisChunk.ToString().CompareTo(thatChunk.ToString());
                }

                if (result != 0)
                {
                    return result;
                }
            }

            return 0;
        }
    }
}
person Oliver    schedule 15.09.2010

Это самый быстрый алгоритм - мне потребовалось 2 мили, чтобы отсортировать 50 элементов ~

static void Sort()
{
    string[] partNumbers = new string[] {"A1", "A2", "A10", "A111"};
    string[] result = partNumbers.OrderBy(x => PadNumbers(x)).ToArray();
}


public static string PadNumbers(string input)
{
        const int MAX_NUMBER_LEN = 10;

        string newInput = "";
        string currentNumber = "";
        foreach (char a in input)
        {
            if (!char.IsNumber(a))
            {
                if (currentNumber == "")
                {
                    newInput += a;
                    continue;
                }
                newInput += "0000000000000".Substring(0, MAX_NUMBER_LEN - currentNumber.Length) + currentNumber;
                currentNumber = "";
            }
            currentNumber += a;
        }
        if (currentNumber != "")
        {
            newInput += "0000000000000".Substring(0, MAX_NUMBER_LEN - currentNumber.Length) + currentNumber;
        }

        return newInput;
    }

~

person asaf    schedule 08.01.2017

Я не думаю, что вам нужно что-то, кроме listName.Sort (), потому что метод sort () использует компаратор по умолчанию для узлов быстрой сортировки. Компаратор по умолчанию делает именно то, что вам нужно.

person eugeneK    schedule 15.09.2010

Вот решение C # 7 (при условии, что список имеет имя a):

    var numericList = a.Where(i => int.TryParse(i, out _)).OrderBy(j => int.Parse(j)).ToList();
    var nonNumericList = a.Where(i => !int.TryParse(i, out _)).OrderBy(j => j).ToList();
    a.Clear();
    a.AddRange(numericList);
    a.AddRange(nonNumericList);
person Daan    schedule 27.09.2017