簡體   English   中英

按 2 列對二維數組進行排序

[英]Sorting 2D array by 2 columns

我正在尋找一種對二維數組中的數據進行排序的有效方法。 該數組可以有很多行和列,但在本例中,我將其限制為 6 行和 5 列。 數據是字符串,因為有些是單詞。 我在下面只包含一個詞,但在真實數據中有幾列詞。 我意識到如果我們排序,我們應該將數據視為數字?

string[,] WeatherDataArray = new string[6,5];

數據是一組每天讀取並記錄的天氣數據。 這些數據經過他們系統的許多部分,我無法更改,並且以需要分類的方式到達我這里。 示例布局可以是:

Day number, temperature, rainfall, wind, cloud

數據矩陣可能看起來像這樣

3,20,0,12,cumulus
1,20,0,11,none
23,15,0,8,none
4,12,0,1,cirrus
12,20,0,12,cumulus
9,15,2,11,none

他們現在希望對數據進行排序,以便溫度降序排列,天數升序排列。 結果將是

1,20,0,11,none
3,20,0,12,cumulus
12,20,0,12,cumulus
9,15,2,11,none
23,15,0,0,none
4,12,0,1,cirrus

該數組已存儲,稍后他們可以將其提取到表中並對其進行大量分析。 提取端沒有改變,所以我無法對表中的數據進行排序,我必須以正確的格式創建數據以匹配它們現有的規則。

我可以解析數組的每一行並對它們進行排序,但這似乎是一種非常長手的方法。 必須有一種更快更有效的方法來按兩列對這個二維數組進行排序嗎? 我想我可以將它發送到 function 並返回排序后的數組,例如:

private string[,]  SortData(string[,] Data)
{

    //In here we do the sorting

}

有什么想法嗎?

我建議將數據解析為可以通過常規方法排序的對象。 比如使用 LINQ:

myObjects.OrderBy(obj => obj.Property1)
         .ThenBy(obj=> obj.Property2);

將數據視為字符串表只會使處理變得更加困難,因為在每一步都需要解析值,處理潛在的錯誤,因為字符串可能為空或包含無效值等。這是一個更好的設計來完成所有工作在讀取數據時進行一次解析和錯誤處理,在寫入磁盤或交給下一個系統時再次將其轉換為文本形式。

如果這是一個有很多部分以文本形式處理數據的遺留系統,我仍然會爭辯說首先解析數據,然后在一個單獨的模塊中進行,這樣它就可以被重用。 這應該允許其他部分逐部分重寫以使用 object 格式。

如果這完全不可行,您要么需要將數據轉換為鋸齒狀數組,即string[][] 或者編寫您自己的排序,可以交換多維數組中的行。

我同意另一個答案,即最好將每一行數據解析為封裝數據的 class 實例,從該數據創建新的一維數組或列表。 然后您對該一維集合進行排序並將其轉換回二維數組。

然而,另一種方法是編寫一個IComparer class,您可以使用它來比較二維數組中的兩行,如下所示:

public sealed class WeatherComparer: IComparer
{
    readonly string[,] _data;

    public WeatherComparer(string[,] data)
    {
        _data = data;
    }

    public int Compare(object? x, object? y)
    {
        int row1 = (int)x;
        int row2 = (int)y;

        double temperature1 = double.Parse(_data[row1, 1]);
        double temperature2 = double.Parse(_data[row2, 1]);

        if (temperature1 < temperature2)
            return 1;

        if (temperature2 < temperature1)
            return -1;

        int day1 = int.Parse(_data[row1,0]);
        int day2 = int.Parse(_data[row2,0]);

        return day1.CompareTo(day2);
    }
}

請注意,這包括對要排序的二維數組的引用,並在必要時解析要排序的元素。

然后您需要創建一個一維索引數組,這就是您實際要排序的內容。 (您不能對二維數組進行排序,但可以對引用二維數組行的一維索引數組進行排序。)

public static string[,] SortData(string[,] data)
{
    int[] indexer = Enumerable.Range(0, data.GetLength(0)).ToArray();
    var comparer = new WeatherComparer(data);
    Array.Sort(indexer, comparer);

    string[,] result = new string[data.GetLength(0), data.GetLength(1)];

    for (int row = 0; row < indexer.Length; ++row)
    {
        int dest = indexer[row];

        for (int col = 0; col < data.GetLength(1); ++col)
            result[dest, col] = data[row, col];
    }

    return result;
}

然后你可以調用SortData對數據進行排序:

public static void Main()
{
    string[,] weatherDataArray = new string[6, 5]
    {
        { "3",  "20", "0", "12", "cumulus" },
        { "1",  "20", "0", "11", "none" },
        { "23", "15", "0", "8",  "none" },
        { "4",  "12", "0", "1",  "cirrus" },
        { "12", "20", "0", "12", "cumulus" },
        { "9",  "15", "2", "11", "none" }
    };

    var sortedWeatherData = SortData(weatherDataArray);

    for (int i = 0; i < sortedWeatherData.GetLength(0); ++i)
    {
        for (int j = 0; j < sortedWeatherData.GetLength(1); ++j)
             Console.Write(sortedWeatherData[i,j] + ", ");
            
        Console.WriteLine();
    }
}

Output:

1, 20, 0, 11, none,
3, 20, 0, 12, cumulus,
12, 20, 0, 12, cumulus,
9, 15, 2, 11, none,
23, 15, 0, 8, none,
4, 12, 0, 1, cirrus,

請注意,此代碼不包含任何錯誤檢查 - 它假定數據中沒有空值,並且所有已解析的數據實際上都是可解析的。 您可能想要添加適當的錯誤處理。

在 .NET 上試用 Fiddle: https://do.netfiddle.net/mwXyMs

我很高興嘗試做出比公認的答案更好的東西,我想我做到了。

更好的原因:

  • 它使用哪些列進行排序以及是按升序還是降序排序都沒有硬編碼,而是作為參數傳入。 在帖子中,我了解到他們將來可能會改變他們對如何對數據進行排序的想法。
  • 它支持按不包含數字的列排序,如果他們想按名稱列排序。
  • 在我的測試中,對於大數據,它更快並且分配更少 memory。

它更快的原因:

  • 它從不對相同的數據索引進行兩次解析。 它緩存數字。
  • 復制時,它使用Span.CopyTo而不是索引。
  • 它不會創建新的數據數組,它會在適當的位置對行進行排序。 這也意味着它不會復制已經在正確位置的行。

這是用法:

DataSorter.SortDataWithSortAguments(array, (1, false), (0, true));

這是代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

namespace YourNamespace;

public static class DataSorter
{
    public static void SortDataWithSortAguments(string[,] Data, params (int columnIndex, bool ascending)[] sortingParams)
    {
        if (sortingParams.Length == 0)
        {
            return;
            // maybe throw an exception instead? depends on what you want
        }

        if (sortingParams.Length > 1)
        {
            var duplicateColumns =
                from sortingParam in sortingParams
                group false by sortingParam.columnIndex
                into sortingGroup
                where sortingGroup.Skip(1).Any()
                select sortingGroup.Key;

            var duplicateColumnsArray = duplicateColumns.ToArray();
            if (duplicateColumnsArray.Length > 0)
            {
                throw new ArgumentException($"Cannot sort by the same column twice. Duplicate columns are: {string.Join(", ", duplicateColumnsArray)}");
            }
        }

        for (int i = 0; i < sortingParams.Length; i++)
        {
            int col = sortingParams[i].columnIndex;
            if (col < 0 || col >= Data.GetLength(1))
            {
                throw new ArgumentOutOfRangeException($"Column index {col} is not within range 0 to {Data.GetLength(1)}");
            }
        }

        int[] linearRowIndeces = new int[Data.GetLength(0)];
        for (int i = 0; i < linearRowIndeces.Length; i++)
        {
            linearRowIndeces[i] = i;
        }

        Span<int> sortedRows = SortIndecesByParams(Data, sortingParams, linearRowIndeces);

        SortDataRowsByIndecesInPlace(Data, sortedRows);
    }


    private static float[]? GetColumnAsNumbersOrNull(string[,] Data, int columnIndex)
    {
        if (!float.TryParse(Data[0, columnIndex], out float firstNumber))
        {
            return null;
        }

        // if the first row of the given column is a number, assume all rows of the column should be numbers as well

        float[] column = new float[Data.GetLength(0)];
        column[0] = firstNumber;

        for (int row = 1; row < column.Length; row++)
        {
            if (!float.TryParse(Data[row, columnIndex], out column[row]))
            {
                throw new ArgumentException(
                    $"Rows 0 to {row - 1} of column {columnIndex} contained numbers, but row {row} doesn't");
            }
        }

        return column;
    }

    private static Span<int> SortIndecesByParams(
        string[,] Data,
        ReadOnlySpan<(int columnIndex, bool ascending)> sortingParams,
        IEnumerable<int> linearRowIndeces)
    {
        var (firstColumnIndex, firstAscending) = sortingParams[0];

        var firstColumn = GetColumnAsNumbersOrNull(Data, firstColumnIndex);

        IOrderedEnumerable<int> sortedRowIndeces = (firstColumn, firstAscending) switch
        {
            (null, true) => linearRowIndeces.OrderBy(row => Data[row, firstColumnIndex]),
            (null, false) => linearRowIndeces.OrderByDescending(row => Data[row, firstColumnIndex]),
            (not null, true) => linearRowIndeces.OrderBy(row => firstColumn[row]),
            (not null, false) => linearRowIndeces.OrderByDescending(row => firstColumn[row])
        };

        for (int i = 1; i < sortingParams.Length; i++)
        {
            var (columnIndex, ascending) = sortingParams[i];

            var column = GetColumnAsNumbersOrNull(Data, columnIndex);

            sortedRowIndeces = (column, ascending) switch
            {
                (null, true) => sortedRowIndeces.ThenBy(row => Data[row, columnIndex]),
                (null, false) => sortedRowIndeces.ThenByDescending(row => Data[row, columnIndex]),
                (not null, true) => sortedRowIndeces.ThenBy(row => column[row]),
                (not null, false) => sortedRowIndeces.ThenByDescending(row => column[row])
            };
        }

        return sortedRowIndeces.ToArray();
    }

    private static void SortDataRowsByIndecesInPlace(string[,] Data, Span<int> sortedRows)
    {
        Span<string> tempRow = new string[Data.GetLength(1)];

        for (int i = 0; i < sortedRows.Length; i++)
        {
            while (i != sortedRows[i])
            {
                Span<string> firstRow = MemoryMarshal.CreateSpan(ref Data[i, 0], tempRow.Length);
                Span<string> secondRow = MemoryMarshal.CreateSpan(ref Data[sortedRows[i], 0], tempRow.Length);

                firstRow.CopyTo(tempRow);
                secondRow.CopyTo(firstRow);
                tempRow.CopyTo(secondRow);

                (sortedRows[i], sortedRows[sortedRows[i]]) = (sortedRows[sortedRows[i]], sortedRows[i]);
            }
        }
    }
}

PS:考慮到我的責任,我不應該花這么多時間在這上面,但這很有趣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM