按 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
数组已存储,稍后他们可以将其提取到 table 并对其进行大量分析。提取端没有改变,所以我无法对 table 中的数据进行排序,我必须以正确的格式创建数据以匹配它们现有的规则。
我可以解析数组的每一行并对它们进行排序,但这似乎是一种非常冗长的方法。必须有一种更快更有效的方法来按两列对这个二维数组进行排序吗?我想我可以将它发送到一个函数并返回排序后的数组,如:
private string[,] SortData(string[,] Data)
{
//In here we do the sorting
}
有什么想法吗?
我建议将数据解析为可以通过常规方法排序的对象。喜欢使用 LINQ:
myObjects.OrderBy(obj => obj.Property1)
.ThenBy(obj=> obj.Property2);
将数据视为 table 字符串只会让处理变得更加困难,因为在每一步都需要解析值,处理潜在的错误,因为字符串可能为空或包含无效值等。在读取数据时将所有这些解析和错误处理一次,并在将其写入磁盘或移交时再次将其转换为text-form是一个更好的设计到下一个系统。
如果这是一个遗留系统,有很多部分处理 text-form 中的数据,我仍然会争辩先解析数据,然后在单独的模块中进行,以便可以重复使用。这应该允许其他部分逐部分重写以使用对象格式。
如果这完全不可行,您要么需要将数据转换为锯齿状数组,即 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();
}
}
输出:
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://dotnetfiddle.net/mwXyMs
我很高兴尝试做出比公认的答案更好的东西,我想我做到了。
它更好的原因:
- 它使用哪些列进行排序,是升序还是降序,没有硬编码,而是作为参数传入。在 post 中,我了解到他们将来可能会改变主意如何对数据进行排序。
- 它支持按不包含数字的列排序,因为如果他们想按名称列排序。
- 在我的测试中,对于大数据,它更快并且分配更少的内存。
速度更快的原因:
- 它从不解析同一个数据索引两次。它缓存数字。
- 复制时,它使用
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:考虑到我的责任,我不应该花这么多时间在这上面,但是很有趣。
我正在寻找一种对二维数组中的数据进行排序的有效方法。该数组可以有很多行和列,但在本例中,我将其限制为 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
数组已存储,稍后他们可以将其提取到 table 并对其进行大量分析。提取端没有改变,所以我无法对 table 中的数据进行排序,我必须以正确的格式创建数据以匹配它们现有的规则。
我可以解析数组的每一行并对它们进行排序,但这似乎是一种非常冗长的方法。必须有一种更快更有效的方法来按两列对这个二维数组进行排序吗?我想我可以将它发送到一个函数并返回排序后的数组,如:
private string[,] SortData(string[,] Data)
{
//In here we do the sorting
}
有什么想法吗?
我建议将数据解析为可以通过常规方法排序的对象。喜欢使用 LINQ:
myObjects.OrderBy(obj => obj.Property1)
.ThenBy(obj=> obj.Property2);
将数据视为 table 字符串只会让处理变得更加困难,因为在每一步都需要解析值,处理潜在的错误,因为字符串可能为空或包含无效值等。在读取数据时将所有这些解析和错误处理一次,并在将其写入磁盘或移交时再次将其转换为text-form是一个更好的设计到下一个系统。
如果这是一个遗留系统,有很多部分处理 text-form 中的数据,我仍然会争辩先解析数据,然后在单独的模块中进行,以便可以重复使用。这应该允许其他部分逐部分重写以使用对象格式。
如果这完全不可行,您要么需要将数据转换为锯齿状数组,即 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();
}
}
输出:
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://dotnetfiddle.net/mwXyMs
我很高兴尝试做出比公认的答案更好的东西,我想我做到了。
它更好的原因:
- 它使用哪些列进行排序,是升序还是降序,没有硬编码,而是作为参数传入。在 post 中,我了解到他们将来可能会改变主意如何对数据进行排序。
- 它支持按不包含数字的列排序,因为如果他们想按名称列排序。
- 在我的测试中,对于大数据,它更快并且分配更少的内存。
速度更快的原因:
- 它从不解析同一个数据索引两次。它缓存数字。
- 复制时,它使用
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:考虑到我的责任,我不应该花这么多时间在这上面,但是很有趣。