.Net Sort

今天來複習一下 .Net 中排序的各種用法,首先是最簡單的由小到大排序

使用 Sort 方法可以直接將資料做正向排序,由於 Generic List 集合是屬於可變集合,因此 Sort 方法呼叫後會直接修改 myList 內容。

var myList = new List<int>() { 1, 7, 4, 2, 6, 3 };
myList.Sort();

另外 Immutable List 集合是屬於不可變集合,因此 Sort 方法不會修改原集合,而是回傳一個新的 ImmutableList 物件,因此需要重新指派。

var myList = ImmutableList.Create(1, 7, 4, 2, 6, 3);
myList = myList.Sort();

反向排序也很簡單,可以先呼叫 Sort 方法,之後在呼叫一次 Reverse 方法,可讀性會比較高。 也可以直接傳入客製的比較器,能夠做到一步倒序。

var myList = new List<int>() { 1, 7, 4, 2, 6, 3 };
myList.Sort();
myList.Reverse();

var myList = new List<int>() { 1, 7, 4, 2, 6, 3 };
myList.Sort((a, b) => b.CompareTo(a));

LINQ Lambda Expression And LINQ Query Expression

使用 Lambda 表達式的 OrderByThenByOrderByDescendingThenByDescending擴充方法, 不直接修改原集合,而是回傳 OrderedEnumerable 集合,特性是擁有 Lazy 特性,代表只有在 enumerate 結果的時候才會真正進行排序。

void Main()
{
	var users = new List<User>()
	{
		new User("Charlie", 31),
		new User("Bob", 35),
		new User("Eve", 34),
		new User("David", 24),
		new User("Alice", 24)
	};

	// 先按照 Age 排,再按照 Name 排
	var orderUser = users
		.OrderBy(p => p.Age)
		.ThenBy(p => p.Name);

	// 先按照 Age 倒序排,再按照 Name 倒序排
	var orderUser1 = users
		.OrderByDescending(p => p.Age)
		.ThenByDescending(p => p.Name);

    // 因為有 Lazy 特性,所以需要增加下面這兩行正式取值,否則就不會做任何事情。
	Console.WriteLine($"{orderUser.First().Name} ({orderUser.First().Age})");
	Console.WriteLine($"{orderUser1.First().Name} ({orderUser1.First().Age})");
}

public record User(string Name, int Age);

另外也可以使用 Query 表達式,在一些複雜的排序場合下可讀性會比較高

void Main()
{
	var users = new List<User>()
	{
		new User("Charlie", 31),
		new User("Bob", 35),
		new User("Eve", 34),
		new User("David", 24),
		new User("Alice", 24)
	};

	// 先按照 Age 排,再按照 Name 排
	var orderUser = from p in users
					orderby p.Age, p.Name
					select p;

	// 先按照 Age 倒序排,再按照 Name 倒序排
	var orderUser1 = from p in users
					 orderby p.Age descending, p.Name descending
					 select p;

	// 因為有 Lazy 特性,所以需要增加下面這兩行正式取值,否則就不會做任何事情。
	Console.WriteLine($"{orderUser.First().Name} ({orderUser.First().Age})");
	Console.WriteLine($"{orderUser1.First().Name} ({orderUser1.First().Age})");
}

public record User(string Name, int Age);

String Sort

在 C# 中排序字串時,同樣可以使用 Sort() 方法,需要注意的是,預設排序會依照系統的 CultureInfo 進行,並不一定是 ASCII 排序。

因此 C# 提供了 StringComparer 類別,能讓我們統一控制不同文化排序方式,例如:

  1. StringComparer.Ordinal:按 ASCII 值排序,與文化無關。
  2. StringComparer.OrdinalIgnoreCase:按 ASCII 排序,忽略大小寫。
  3. StringComparer.CurrentCulture:根據目前系統文化排序。
  4. StringComparer.CurrentCultureIgnoreCase:根據目前系統文化排序,忽略大小寫。
  5. StringComparer.InvariantCulture:根據固定文化排序,不參考系統文化。
  6. StringComparer.InvariantCultureIgnoreCase:根據固定文化排序,不參考系統文化,忽略大小寫。
void Main()
{
	var words = new List<string>() { "i", "love", "csharp", "i", "love", "sorting" };
	words.Sort(StringComparer.Ordinal);
	foreach (var element in words)
	{
		Console.WriteLine(element);
	}
}

// 輸出
csharp
i
i
love
love
sorting

我們這邊再把第一個 love 改成大寫形式,在進行排序會發現大寫的 Love 變到第一位了,這是因為大寫的 L 為 76 小寫的 c 為 99 所以這時就會把 Love 拉到第一位

void Main()
{
	var words = new List<string>(){ "i", "Love", "csharp", "i", "love", "sorting"};
	words.Sort(StringComparer.Ordinal);
	foreach (var element in words)
	{
		Console.WriteLine(element);
	}
}

// 輸出
Love
csharp
i
i
love
sorting

當然也可使用 StringComparer.OrdinalIgnoreCase 也是以 ASCII 為基礎但是會忽略掉大小寫

void Main()
{
	var words = new List<string>() { "i", "love", "csharp", "i", "love", "sorting" };
	words.Sort(StringComparer.OrdinalIgnoreCase);
	foreach (var element in words)
	{
		Console.WriteLine(element);
	}
}

// 輸出
csharp
i
i
love
love
sorting

使用多條件排序(單字出現頻率 + ASCII) 搭配 LINQ,最後挑選前兩名。

void Main()
{
	var words = new List<string> { "love", "i", "csharp", "love", "i", "sorting", "sorting", "sorting" };

	// 統計每個單字的出現頻率
	var freqMap = words
		.GroupBy(w => w)
		.ToDictionary(g => g.Key, g => g.Count());

	// 多條件排序:先依頻率再依 ASCII 字母排序
	var topTwo = freqMap
		.OrderByDescending(kv => kv.Value)             // 依頻率排序
		.ThenBy(kv => kv.Key, StringComparer.Ordinal)  // 頻率相同時依 ASCII 排序
		.Take(2)                                       // 取前兩名
		.Select(kv => kv.Key)                          // 只取 Key
		.ToList();

	// 輸出結果
	topTwo.ForEach(Console.WriteLine);
}

在 .Net 10 提供了 CompareOptions 讓建立比較器更加容易,例如 CompareOptions.NumericOrdering, 適合用來排序文字中帶有數字的資料。

void Main()
{
	var words = new List<string> { "file2", "file1", "file4", "file3"};

	var comparer = StringComparer.Create(
        CultureInfo.InvariantCulture,
        CompareOptions.NumericOrdering);
	
	var sorted = words.OrderBy(x => x, comparer);
	sorted.ToList().ForEach(Console.WriteLine);
}